From 19be4a5d6de1356d84ff1ff81a5b72f50b8bb4e7 Mon Sep 17 00:00:00 2001 From: Quentame Date: Wed, 11 Mar 2020 22:15:59 +0100 Subject: [PATCH 001/431] Refactor Freebox : add config flow + temperature sensor + signal dispatch (#30334) * Add config flow to Freebox * Add manufacturer in device_tracker info * Add device_info to sensor + switch * Add device_info: connections * Add config_flow test + update .coveragerc * Typing * Add device_type icon * Remove one error log * Fix pylint * Add myself as CODEOWNER * Handle sync in one place * Separate the Freebox[Router/Device/Sensor] from __init__.py * Add link step to config flow * Make temperature sensors auto-discovered * Use device activity instead of reachablility for device_tracker * Store token file in .storage Depending on host if list of Freebox integration on the future without breaking change * Remove IP sensors + add Freebox router as a device with attrs : IPs, conection type, uptime, version & serial * Add sensor should_poll=False * Test typing * Handle devices with no name * None is the default for data * Fix comment * Use config_entry.unique_id * Add async_unload_entry with asyncio * Add and use bunch of data size and rate related constants (#31781) * Review * Remove useless "already_configured" error string * Review : merge 2 device & 2 sensor classes * Entities from platforms * Fix unload + add device after setup + clean loggers * async_add_entities True * Review * Use pathlib + refactor get_api * device_tracker set + tests with CoroutineMock() * Removing active & reachable from tracker attrs * Review * Fix pipeline * typing * typing * typing * Raise ConfigEntryNotReady when HttpRequestError at setup * Review * Multiple Freebox s * Review: store sensors in router * Freebox: a sensor story --- .coveragerc | 6 +- CODEOWNERS | 2 +- .../components/freebox/.translations/en.json | 26 +++ homeassistant/components/freebox/__init__.py | 115 ++++++----- .../components/freebox/config_flow.py | 110 ++++++++++ homeassistant/components/freebox/const.py | 75 +++++++ .../components/freebox/device_tracker.py | 171 ++++++++++++---- .../components/freebox/manifest.json | 3 +- homeassistant/components/freebox/router.py | 193 ++++++++++++++++++ homeassistant/components/freebox/sensor.py | 146 ++++++++----- homeassistant/components/freebox/strings.json | 26 +++ homeassistant/components/freebox/switch.py | 49 +++-- homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/freebox/__init__.py | 1 + tests/components/freebox/conftest.py | 11 + tests/components/freebox/test_config_flow.py | 144 +++++++++++++ 17 files changed, 917 insertions(+), 165 deletions(-) create mode 100644 homeassistant/components/freebox/.translations/en.json create mode 100644 homeassistant/components/freebox/config_flow.py create mode 100644 homeassistant/components/freebox/const.py create mode 100644 homeassistant/components/freebox/router.py create mode 100644 homeassistant/components/freebox/strings.json create mode 100644 tests/components/freebox/__init__.py create mode 100644 tests/components/freebox/conftest.py create mode 100644 tests/components/freebox/test_config_flow.py diff --git a/.coveragerc b/.coveragerc index 2716a1fed44..c94199d6451 100644 --- a/.coveragerc +++ b/.coveragerc @@ -242,7 +242,11 @@ omit = homeassistant/components/foscam/const.py homeassistant/components/foursquare/* homeassistant/components/free_mobile/notify.py - homeassistant/components/freebox/* + homeassistant/components/freebox/__init__.py + homeassistant/components/freebox/device_tracker.py + homeassistant/components/freebox/router.py + homeassistant/components/freebox/sensor.py + homeassistant/components/freebox/switch.py homeassistant/components/fritz/device_tracker.py homeassistant/components/fritzbox/* homeassistant/components/fritzbox_callmonitor/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 97b347b8415..8b85278b4bb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -122,7 +122,7 @@ homeassistant/components/fortigate/* @kifeo homeassistant/components/fortios/* @kimfrellsen homeassistant/components/foscam/* @skgsergio homeassistant/components/foursquare/* @robbiet480 -homeassistant/components/freebox/* @snoof85 +homeassistant/components/freebox/* @snoof85 @Quentame homeassistant/components/fronius/* @nielstron homeassistant/components/frontend/* @home-assistant/frontend homeassistant/components/garmin_connect/* @cyberjunky diff --git a/homeassistant/components/freebox/.translations/en.json b/homeassistant/components/freebox/.translations/en.json new file mode 100644 index 00000000000..75d925e2f7a --- /dev/null +++ b/homeassistant/components/freebox/.translations/en.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Host already configured" + }, + "error": { + "connection_failed": "Failed to connect, please try again", + "register_failed": "Failed to register, please try again", + "unknown": "Unknown error: please retry later" + }, + "step": { + "link": { + "description": "Click \"Submit\", then touch the right arrow on the router to register Freebox with Home Assistant.\n\n![Location of button on the router](/static/images/config_freebox.png)", + "title": "Link Freebox router" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/__init__.py b/homeassistant/components/freebox/__init__.py index 58426334dea..9e303c75e7a 100644 --- a/homeassistant/components/freebox/__init__.py +++ b/homeassistant/components/freebox/__init__.py @@ -1,29 +1,26 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" +import asyncio import logging -import socket -from aiofreepybox import Freepybox -from aiofreepybox.exceptions import HttpRequestError import voluptuous as vol from homeassistant.components.discovery import SERVICE_FREEBOX +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DOMAIN, PLATFORMS +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -DOMAIN = "freebox" -DATA_FREEBOX = DOMAIN - -FREEBOX_CONFIG_FILE = "freebox.conf" +FREEBOX_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} +) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - {vol.Required(CONF_HOST): cv.string, vol.Required(CONF_PORT): cv.port} - ) - }, + {DOMAIN: vol.Schema(vol.All(cv.ensure_list, [FREEBOX_SCHEMA]))}, extra=vol.ALLOW_EXTRA, ) @@ -37,54 +34,70 @@ async def async_setup(hass, config): host = discovery_info.get("properties", {}).get("api_domain") port = discovery_info.get("properties", {}).get("https_port") _LOGGER.info("Discovered Freebox server: %s:%s", host, port) - await async_setup_freebox(hass, config, host, port) + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={CONF_HOST: host, CONF_PORT: port}, + ) + ) discovery.async_listen(hass, SERVICE_FREEBOX, discovery_dispatch) - if conf is not None: - host = conf.get(CONF_HOST) - port = conf.get(CONF_PORT) - await async_setup_freebox(hass, config, host, port) + if conf is None: + return True + + for freebox_conf in conf: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=freebox_conf, + ) + ) return True -async def async_setup_freebox(hass, config, host, port): - """Start up the Freebox component platforms.""" +async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Set up Freebox component.""" + router = FreeboxRouter(hass, entry) + await router.setup() - app_desc = { - "app_id": "hass", - "app_name": "Home Assistant", - "app_version": "0.65", - "device_name": socket.gethostname(), - } + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.unique_id] = router - token_file = hass.config.path(FREEBOX_CONFIG_FILE) - api_version = "v6" - - fbx = Freepybox(app_desc=app_desc, token_file=token_file, api_version=api_version) - - try: - await fbx.open(host, port) - except HttpRequestError: - _LOGGER.exception("Failed to connect to Freebox") - else: - hass.data[DATA_FREEBOX] = fbx - - async def async_freebox_reboot(call): - """Handle reboot service call.""" - await fbx.system.reboot() - - hass.services.async_register(DOMAIN, "reboot", async_freebox_reboot) - - hass.async_create_task(async_load_platform(hass, "sensor", DOMAIN, {}, config)) + for platform in PLATFORMS: hass.async_create_task( - async_load_platform(hass, "device_tracker", DOMAIN, {}, config) + hass.config_entries.async_forward_entry_setup(entry, platform) ) - hass.async_create_task(async_load_platform(hass, "switch", DOMAIN, {}, config)) - async def close_fbx(event): - """Close Freebox connection on HA Stop.""" - await fbx.close() + # Services + async def async_reboot(call): + """Handle reboot service call.""" + await router.reboot() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, close_fbx) + hass.services.async_register(DOMAIN, "reboot", async_reboot) + + async def async_close_connection(event): + """Close Freebox connection on HA Stop.""" + await router.close() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_close_connection) + + return True + + +async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, platform) + for platform in PLATFORMS + ] + ) + ) + if unload_ok: + router = hass.data[DOMAIN].pop(entry.unique_id) + await router.close() + + return unload_ok diff --git a/homeassistant/components/freebox/config_flow.py b/homeassistant/components/freebox/config_flow.py new file mode 100644 index 00000000000..b2d1a0ab771 --- /dev/null +++ b/homeassistant/components/freebox/config_flow.py @@ -0,0 +1,110 @@ +"""Config flow to configure the Freebox integration.""" +import logging + +from aiofreepybox.exceptions import AuthorizationError, HttpRequestError +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT + +from .const import DOMAIN # pylint: disable=unused-import +from .router import get_api + +_LOGGER = logging.getLogger(__name__) + + +class FreeboxFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize Freebox config flow.""" + self._host = None + self._port = None + + def _show_setup_form(self, user_input=None, errors=None): + """Show the setup form to the user.""" + + if user_input is None: + user_input = {} + + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST, default=user_input.get(CONF_HOST, "")): str, + vol.Required(CONF_PORT, default=user_input.get(CONF_PORT, "")): int, + } + ), + errors=errors or {}, + ) + + async def async_step_user(self, user_input=None): + """Handle a flow initiated by the user.""" + errors = {} + + if user_input is None: + return self._show_setup_form(user_input, errors) + + self._host = user_input[CONF_HOST] + self._port = user_input[CONF_PORT] + + # Check if already configured + await self.async_set_unique_id(self._host) + self._abort_if_unique_id_configured() + + return await self.async_step_link() + + async def async_step_link(self, user_input=None): + """Attempt to link with the Freebox router. + + Given a configured host, will ask the user to press the button + to connect to the router. + """ + if user_input is None: + return self.async_show_form(step_id="link") + + errors = {} + + fbx = await get_api(self.hass, self._host) + try: + # Open connection and check authentication + await fbx.open(self._host, self._port) + + # Check permissions + await fbx.system.get_config() + await fbx.lan.get_hosts_list() + await self.hass.async_block_till_done() + + # Close connection + await fbx.close() + + return self.async_create_entry( + title=self._host, data={CONF_HOST: self._host, CONF_PORT: self._port}, + ) + + except AuthorizationError as error: + _LOGGER.error(error) + errors["base"] = "register_failed" + + except HttpRequestError: + _LOGGER.error("Error connecting to the Freebox router at %s", self._host) + errors["base"] = "connection_failed" + + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Unknown error connecting with Freebox router at %s", self._host + ) + errors["base"] = "unknown" + + return self.async_show_form(step_id="link", errors=errors) + + async def async_step_import(self, user_input=None): + """Import a config entry.""" + return await self.async_step_user(user_input) + + async def async_step_discovery(self, user_input=None): + """Initialize step from discovery.""" + return await self.async_step_user(user_input) diff --git a/homeassistant/components/freebox/const.py b/homeassistant/components/freebox/const.py new file mode 100644 index 00000000000..0612e4e76f1 --- /dev/null +++ b/homeassistant/components/freebox/const.py @@ -0,0 +1,75 @@ +"""Freebox component constants.""" +import socket + +from homeassistant.const import ( + DATA_RATE_KILOBYTES_PER_SECOND, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, +) + +DOMAIN = "freebox" + +APP_DESC = { + "app_id": "hass", + "app_name": "Home Assistant", + "app_version": "0.106", + "device_name": socket.gethostname(), +} +API_VERSION = "v6" + +PLATFORMS = ["device_tracker", "sensor", "switch"] + +DEFAULT_DEVICE_NAME = "Unknown device" + +# to store the cookie +STORAGE_KEY = DOMAIN +STORAGE_VERSION = 1 + +# Sensor +SENSOR_NAME = "name" +SENSOR_UNIT = "unit" +SENSOR_ICON = "icon" +SENSOR_DEVICE_CLASS = "device_class" + +CONNECTION_SENSORS = { + "rate_down": { + SENSOR_NAME: "Freebox download speed", + SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, + SENSOR_ICON: "mdi:download-network", + SENSOR_DEVICE_CLASS: None, + }, + "rate_up": { + SENSOR_NAME: "Freebox upload speed", + SENSOR_UNIT: DATA_RATE_KILOBYTES_PER_SECOND, + SENSOR_ICON: "mdi:upload-network", + SENSOR_DEVICE_CLASS: None, + }, +} + +TEMPERATURE_SENSOR_TEMPLATE = { + SENSOR_NAME: None, + SENSOR_UNIT: TEMP_CELSIUS, + SENSOR_ICON: "mdi:thermometer", + SENSOR_DEVICE_CLASS: DEVICE_CLASS_TEMPERATURE, +} + +# Icons +DEVICE_ICONS = { + "freebox_delta": "mdi:television-guide", + "freebox_hd": "mdi:television-guide", + "freebox_mini": "mdi:television-guide", + "freebox_player": "mdi:television-guide", + "ip_camera": "mdi:cctv", + "ip_phone": "mdi:phone-voip", + "laptop": "mdi:laptop", + "multimedia_device": "mdi:play-network", + "nas": "mdi:nas", + "networking_device": "mdi:network", + "printer": "mdi:printer", + "router": "mdi:router-wireless", + "smartphone": "mdi:cellphone", + "tablet": "mdi:tablet", + "television": "mdi:television", + "vg_console": "mdi:gamepad-variant", + "workstation": "mdi:desktop-tower-monitor", +} diff --git a/homeassistant/components/freebox/device_tracker.py b/homeassistant/components/freebox/device_tracker.py index 63cf869990d..ea9919f5742 100644 --- a/homeassistant/components/freebox/device_tracker.py +++ b/homeassistant/components/freebox/device_tracker.py @@ -1,65 +1,148 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" -from collections import namedtuple +from datetime import datetime import logging +from typing import Dict -from homeassistant.components.device_tracker import DeviceScanner +from homeassistant.components.device_tracker import SOURCE_TYPE_ROUTER +from homeassistant.components.device_tracker.config_entry import ScannerEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import callback +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_FREEBOX +from .const import DEFAULT_DEVICE_NAME, DEVICE_ICONS, DOMAIN +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -async def async_get_scanner(hass, config): - """Validate the configuration and return a Freebox scanner.""" - scanner = FreeboxDeviceScanner(hass.data[DATA_FREEBOX]) - await scanner.async_connect() - return scanner if scanner.success_init else None +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: + """Set up device tracker for Freebox component.""" + router = hass.data[DOMAIN][entry.unique_id] + tracked = set() + @callback + def update_router(): + """Update the values of the router.""" + add_entities(router, async_add_entities, tracked) -Device = namedtuple("Device", ["id", "name", "ip"]) - - -def _build_device(device_dict): - return Device( - device_dict["l2ident"]["id"], - device_dict["primary_name"], - device_dict["l3connectivities"][0]["addr"], + router.listeners.append( + async_dispatcher_connect(hass, router.signal_device_new, update_router) ) + update_router() -class FreeboxDeviceScanner(DeviceScanner): - """Queries the Freebox device.""" - def __init__(self, fbx): - """Initialize the scanner.""" - self.last_results = {} - self.success_init = False - self.connection = fbx +@callback +def add_entities(router, async_add_entities, tracked): + """Add new tracker entities from the router.""" + new_tracked = [] - async def async_connect(self): - """Initialize connection to the router.""" - # Test the router is accessible. - data = await self.connection.lan.get_hosts_list() - self.success_init = data is not None + for mac, device in router.devices.items(): + if mac in tracked: + continue - async def async_scan_devices(self): - """Scan for new devices and return a list with found device IDs.""" - await self.async_update_info() - return [device.id for device in self.last_results] + new_tracked.append(FreeboxDevice(router, device)) + tracked.add(mac) - async def get_device_name(self, device): - """Return the name of the given device or None if we don't know.""" - name = next( - (result.name for result in self.last_results if result.id == device), None + if new_tracked: + async_add_entities(new_tracked, True) + + +class FreeboxDevice(ScannerEntity): + """Representation of a Freebox device.""" + + def __init__(self, router: FreeboxRouter, device: Dict[str, any]) -> None: + """Initialize a Freebox device.""" + self._router = router + self._name = device["primary_name"].strip() or DEFAULT_DEVICE_NAME + self._mac = device["l2ident"]["id"] + self._manufacturer = device["vendor_name"] + self._icon = icon_for_freebox_device(device) + self._active = False + self._attrs = {} + + self._unsub_dispatcher = None + + def update(self) -> None: + """Update the Freebox device.""" + device = self._router.devices[self._mac] + self._active = device["active"] + if device.get("attrs") is None: + # device + self._attrs = { + "last_time_reachable": datetime.fromtimestamp( + device["last_time_reachable"] + ), + "last_time_activity": datetime.fromtimestamp(device["last_activity"]), + } + else: + # router + self._attrs = device["attrs"] + + @property + def unique_id(self) -> str: + """Return a unique ID.""" + return self._mac + + @property + def name(self) -> str: + """Return the name.""" + return self._name + + @property + def is_connected(self): + """Return true if the device is connected to the network.""" + return self._active + + @property + def source_type(self) -> str: + """Return the source type.""" + return SOURCE_TYPE_ROUTER + + @property + def icon(self) -> str: + """Return the icon.""" + return self._icon + + @property + def device_state_attributes(self) -> Dict[str, any]: + """Return the attributes.""" + return self._attrs + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self._mac)}, + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": self._manufacturer, + } + + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False + + async def async_on_demand_update(self): + """Update state.""" + self.async_schedule_update_ha_state(True) + + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, self._router.signal_device_update, self.async_on_demand_update ) - return name - async def async_update_info(self): - """Ensure the information from the Freebox router is up to date.""" - _LOGGER.debug("Checking Devices") + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() - hosts = await self.connection.lan.get_hosts_list() - last_results = [_build_device(device) for device in hosts if device["active"]] - - self.last_results = last_results +def icon_for_freebox_device(device) -> str: + """Return a host icon from his type.""" + return DEVICE_ICONS.get(device["host_type"], "mdi:help-network") diff --git a/homeassistant/components/freebox/manifest.json b/homeassistant/components/freebox/manifest.json index 7a66490c90d..1bfb4924a78 100644 --- a/homeassistant/components/freebox/manifest.json +++ b/homeassistant/components/freebox/manifest.json @@ -1,9 +1,10 @@ { "domain": "freebox", "name": "Freebox", + "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/freebox", "requirements": ["aiofreepybox==0.0.8"], "dependencies": [], "after_dependencies": ["discovery"], - "codeowners": ["@snoof85"] + "codeowners": ["@snoof85", "@Quentame"] } diff --git a/homeassistant/components/freebox/router.py b/homeassistant/components/freebox/router.py new file mode 100644 index 00000000000..7b4784c6ca4 --- /dev/null +++ b/homeassistant/components/freebox/router.py @@ -0,0 +1,193 @@ +"""Represent the Freebox router and its devices and sensors.""" +from datetime import datetime, timedelta +import logging +from pathlib import Path +from typing import Dict, Optional + +from aiofreepybox import Freepybox +from aiofreepybox.api.wifi import Wifi +from aiofreepybox.exceptions import HttpRequestError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import slugify + +from .const import ( + API_VERSION, + APP_DESC, + CONNECTION_SENSORS, + DOMAIN, + STORAGE_KEY, + STORAGE_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(seconds=30) + + +class FreeboxRouter: + """Representation of a Freebox router.""" + + def __init__(self, hass: HomeAssistantType, entry: ConfigEntry) -> None: + """Initialize a Freebox router.""" + self.hass = hass + self._entry = entry + self._host = entry.data[CONF_HOST] + self._port = entry.data[CONF_PORT] + + self._api: Freepybox = None + self._name = None + self.mac = None + self._sw_v = None + self._attrs = {} + + self.devices: Dict[str, any] = {} + self.sensors_temperature: Dict[str, int] = {} + self.sensors_connection: Dict[str, float] = {} + + self.listeners = [] + + async def setup(self) -> None: + """Set up a Freebox router.""" + self._api = await get_api(self.hass, self._host) + + try: + await self._api.open(self._host, self._port) + except HttpRequestError: + _LOGGER.exception("Failed to connect to Freebox") + return ConfigEntryNotReady + + # System + fbx_config = await self._api.system.get_config() + self.mac = fbx_config["mac"] + self._name = fbx_config["model_info"]["pretty_name"] + self._sw_v = fbx_config["firmware_version"] + + # Devices & sensors + await self.update_all() + async_track_time_interval(self.hass, self.update_all, SCAN_INTERVAL) + + async def update_all(self, now: Optional[datetime] = None) -> None: + """Update all Freebox platforms.""" + await self.update_sensors() + await self.update_devices() + + async def update_devices(self) -> None: + """Update Freebox devices.""" + new_device = False + fbx_devices: Dict[str, any] = await self._api.lan.get_hosts_list() + + # Adds the Freebox itself + fbx_devices.append( + { + "primary_name": self._name, + "l2ident": {"id": self.mac}, + "vendor_name": "Freebox SAS", + "host_type": "router", + "active": True, + "attrs": self._attrs, + } + ) + + for fbx_device in fbx_devices: + device_mac = fbx_device["l2ident"]["id"] + + if self.devices.get(device_mac) is None: + new_device = True + + self.devices[device_mac] = fbx_device + + async_dispatcher_send(self.hass, self.signal_device_update) + + if new_device: + async_dispatcher_send(self.hass, self.signal_device_new) + + async def update_sensors(self) -> None: + """Update Freebox sensors.""" + # System sensors + syst_datas: Dict[str, any] = await self._api.system.get_config() + + # According to the doc `syst_datas["sensors"]` is temperature sensors in celsius degree. + # Name and id of sensors may vary under Freebox devices. + for sensor in syst_datas["sensors"]: + self.sensors_temperature[sensor["name"]] = sensor["value"] + + # Connection sensors + connection_datas: Dict[str, any] = await self._api.connection.get_status() + for sensor_key in CONNECTION_SENSORS: + self.sensors_connection[sensor_key] = connection_datas[sensor_key] + + self._attrs = { + "IPv4": connection_datas.get("ipv4"), + "IPv6": connection_datas.get("ipv6"), + "connection_type": connection_datas["media"], + "uptime": datetime.fromtimestamp( + round(datetime.now().timestamp()) - syst_datas["uptime_val"] + ), + "firmware_version": self._sw_v, + "serial": syst_datas["serial"], + } + + async_dispatcher_send(self.hass, self.signal_sensor_update) + + async def reboot(self) -> None: + """Reboot the Freebox.""" + await self._api.system.reboot() + + async def close(self) -> None: + """Close the connection.""" + if self._api is not None: + await self._api.close() + self._api = None + + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return { + "connections": {(CONNECTION_NETWORK_MAC, self.mac)}, + "identifiers": {(DOMAIN, self.mac)}, + "name": self._name, + "manufacturer": "Freebox SAS", + "sw_version": self._sw_v, + } + + @property + def signal_device_new(self) -> str: + """Event specific per Freebox entry to signal new device.""" + return f"{DOMAIN}-{self._host}-device-new" + + @property + def signal_device_update(self) -> str: + """Event specific per Freebox entry to signal updates in devices.""" + return f"{DOMAIN}-{self._host}-device-update" + + @property + def signal_sensor_update(self) -> str: + """Event specific per Freebox entry to signal updates in sensors.""" + return f"{DOMAIN}-{self._host}-sensor-update" + + @property + def sensors(self) -> Wifi: + """Return the wifi.""" + return {**self.sensors_temperature, **self.sensors_connection} + + @property + def wifi(self) -> Wifi: + """Return the wifi.""" + return self._api.wifi + + +async def get_api(hass: HomeAssistantType, host: str) -> Freepybox: + """Get the Freebox API.""" + freebox_path = Path(hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY).path) + freebox_path.mkdir(exist_ok=True) + + token_file = Path(f"{freebox_path}/{slugify(host)}.conf") + + return Freepybox(APP_DESC, token_file, API_VERSION) diff --git a/homeassistant/components/freebox/sensor.py b/homeassistant/components/freebox/sensor.py index 0653120b49c..a3c5c32901c 100644 --- a/homeassistant/components/freebox/sensor.py +++ b/homeassistant/components/freebox/sensor.py @@ -1,81 +1,127 @@ """Support for Freebox devices (Freebox v6 and Freebox mini 4K).""" import logging +from typing import Dict +from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_RATE_KILOBYTES_PER_SECOND +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_FREEBOX +from .const import ( + CONNECTION_SENSORS, + DOMAIN, + SENSOR_DEVICE_CLASS, + SENSOR_ICON, + SENSOR_NAME, + SENSOR_UNIT, + TEMPERATURE_SENSOR_TEMPLATE, +) +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the sensors.""" - fbx = hass.data[DATA_FREEBOX] - async_add_entities([FbxRXSensor(fbx), FbxTXSensor(fbx)], True) + router = hass.data[DOMAIN][entry.unique_id] + entities = [] + + for sensor_name in router.sensors_temperature: + entities.append( + FreeboxSensor( + router, + sensor_name, + {**TEMPERATURE_SENSOR_TEMPLATE, SENSOR_NAME: f"Freebox {sensor_name}"}, + ) + ) + + for sensor_key in CONNECTION_SENSORS: + entities.append( + FreeboxSensor(router, sensor_key, CONNECTION_SENSORS[sensor_key]) + ) + + async_add_entities(entities, True) -class FbxSensor(Entity): - """Representation of a freebox sensor.""" +class FreeboxSensor(Entity): + """Representation of a Freebox sensor.""" - _name = "generic" - _unit = None - _icon = None - - def __init__(self, fbx): - """Initialize the sensor.""" - self._fbx = fbx + def __init__( + self, router: FreeboxRouter, sensor_type: str, sensor: Dict[str, any] + ) -> None: + """Initialize a Freebox sensor.""" self._state = None - self._datas = None + self._router = router + self._sensor_type = sensor_type + self._name = sensor[SENSOR_NAME] + self._unit = sensor[SENSOR_UNIT] + self._icon = sensor[SENSOR_ICON] + self._device_class = sensor[SENSOR_DEVICE_CLASS] + self._unique_id = f"{self._router.mac} {self._name}" + + self._unsub_dispatcher = None + + def update(self) -> None: + """Update the Freebox sensor.""" + state = self._router.sensors[self._sensor_type] + if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: + self._state = round(state / 1000, 2) + else: + self._state = state @property - def name(self): - """Return the name of the sensor.""" + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: + """Return the name.""" return self._name @property - def unit_of_measurement(self): - """Return the unit of the sensor.""" + def state(self) -> str: + """Return the state.""" + return self._state + + @property + def unit_of_measurement(self) -> str: + """Return the unit.""" return self._unit @property - def icon(self): - """Return the icon of the sensor.""" + def icon(self) -> str: + """Return the icon.""" return self._icon @property - def state(self): - """Return the state of the sensor.""" - return self._state + def device_class(self) -> str: + """Return the device_class.""" + return self._device_class - async def async_update(self): - """Fetch status from freebox.""" - self._datas = await self._fbx.connection.get_status() + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return self._router.device_info + @property + def should_poll(self) -> bool: + """No polling needed.""" + return False -class FbxRXSensor(FbxSensor): - """Update the Freebox RxSensor.""" + async def async_on_demand_update(self): + """Update state.""" + self.async_schedule_update_ha_state(True) - _name = "Freebox download speed" - _unit = DATA_RATE_KILOBYTES_PER_SECOND - _icon = "mdi:download-network" + async def async_added_to_hass(self): + """Register state update callback.""" + self._unsub_dispatcher = async_dispatcher_connect( + self.hass, self._router.signal_sensor_update, self.async_on_demand_update + ) - async def async_update(self): - """Get the value from fetched datas.""" - await super().async_update() - if self._datas is not None: - self._state = round(self._datas["rate_down"] / 1000, 2) - - -class FbxTXSensor(FbxSensor): - """Update the Freebox TxSensor.""" - - _name = "Freebox upload speed" - _unit = DATA_RATE_KILOBYTES_PER_SECOND - _icon = "mdi:upload-network" - - async def async_update(self): - """Get the value from fetched datas.""" - await super().async_update() - if self._datas is not None: - self._state = round(self._datas["rate_up"] / 1000, 2) + async def async_will_remove_from_hass(self): + """Clean up after entity before removal.""" + self._unsub_dispatcher() diff --git a/homeassistant/components/freebox/strings.json b/homeassistant/components/freebox/strings.json new file mode 100644 index 00000000000..867a497d02f --- /dev/null +++ b/homeassistant/components/freebox/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Freebox", + "step": { + "user": { + "title": "Freebox", + "data": { + "host": "Host", + "port": "Port" + } + }, + "link": { + "title": "Link Freebox router", + "description": "Click \"Submit\", then touch the right arrow on the router to register Freebox with Home Assistant.\n\n![Location of button on the router](/static/images/config_freebox.png)" + } + }, + "error":{ + "register_failed": "Failed to register, please try again", + "connection_failed": "Failed to connect, please try again", + "unknown": "Unknown error: please retry later" + }, + "abort":{ + "already_configured": "Host already configured" + } + } +} diff --git a/homeassistant/components/freebox/switch.py b/homeassistant/components/freebox/switch.py index 062d6a699fe..9e1011d5d3c 100644 --- a/homeassistant/components/freebox/switch.py +++ b/homeassistant/components/freebox/switch.py @@ -1,50 +1,65 @@ """Support for Freebox Delta, Revolution and Mini 4K.""" import logging +from typing import Dict + +from aiofreepybox.exceptions import InsufficientPermissionsError from homeassistant.components.switch import SwitchDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType -from . import DATA_FREEBOX +from .const import DOMAIN +from .router import FreeboxRouter _LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry( + hass: HomeAssistantType, entry: ConfigEntry, async_add_entities +) -> None: """Set up the switch.""" - fbx = hass.data[DATA_FREEBOX] - async_add_entities([FbxWifiSwitch(fbx)], True) + router = hass.data[DOMAIN][entry.unique_id] + async_add_entities([FreeboxWifiSwitch(router)], True) -class FbxWifiSwitch(SwitchDevice): +class FreeboxWifiSwitch(SwitchDevice): """Representation of a freebox wifi switch.""" - def __init__(self, fbx): + def __init__(self, router: FreeboxRouter) -> None: """Initialize the Wifi switch.""" self._name = "Freebox WiFi" self._state = None - self._fbx = fbx + self._router = router + self._unique_id = f"{self._router.mac} {self._name}" @property - def name(self): + def unique_id(self) -> str: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self) -> str: """Return the name of the switch.""" return self._name @property - def is_on(self): + def is_on(self) -> bool: """Return true if device is on.""" return self._state - async def _async_set_state(self, enabled): - """Turn the switch on or off.""" - from aiofreepybox.exceptions import InsufficientPermissionsError + @property + def device_info(self) -> Dict[str, any]: + """Return the device information.""" + return self._router.device_info + async def _async_set_state(self, enabled: bool): + """Turn the switch on or off.""" wifi_config = {"enabled": enabled} try: - await self._fbx.wifi.set_global_config(wifi_config) + await self._router.wifi.set_global_config(wifi_config) except InsufficientPermissionsError: _LOGGER.warning( - "Home Assistant does not have permissions to" - " modify the Freebox settings. Please refer" - " to documentation." + "Home Assistant does not have permissions to modify the Freebox settings. Please refer to documentation." ) async def async_turn_on(self, **kwargs): @@ -57,6 +72,6 @@ class FbxWifiSwitch(SwitchDevice): async def async_update(self): """Get the state and update it.""" - datas = await self._fbx.wifi.get_global_config() + datas = await self._router.wifi.get_global_config() active = datas["enabled"] self._state = bool(active) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index b281a322b23..a7e9b63c1a5 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -29,6 +29,7 @@ FLOWS = [ "elgato", "emulated_roku", "esphome", + "freebox", "garmin_connect", "gdacs", "geofency", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71aa2004eef..23caf750147 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -61,6 +61,9 @@ aiobotocore==0.11.1 # homeassistant.components.esphome aioesphomeapi==2.6.1 +# homeassistant.components.freebox +aiofreepybox==0.0.8 + # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.29 diff --git a/tests/components/freebox/__init__.py b/tests/components/freebox/__init__.py new file mode 100644 index 00000000000..727b60ae78a --- /dev/null +++ b/tests/components/freebox/__init__.py @@ -0,0 +1 @@ +"""Tests for the Freebox component.""" diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py new file mode 100644 index 00000000000..e813469cbbf --- /dev/null +++ b/tests/components/freebox/conftest.py @@ -0,0 +1,11 @@ +"""Test helpers for Freebox.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_path(): + """Mock path lib.""" + with patch("homeassistant.components.freebox.router.Path"): + yield diff --git a/tests/components/freebox/test_config_flow.py b/tests/components/freebox/test_config_flow.py new file mode 100644 index 00000000000..68e787e1ba0 --- /dev/null +++ b/tests/components/freebox/test_config_flow.py @@ -0,0 +1,144 @@ +"""Tests for the Freebox config flow.""" +from aiofreepybox.exceptions import ( + AuthorizationError, + HttpRequestError, + InvalidTokenError, +) +from asynctest import CoroutineMock, patch +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.freebox.const import DOMAIN +from homeassistant.config_entries import SOURCE_DISCOVERY, SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +HOST = "myrouter.freeboxos.fr" +PORT = 1234 + + +@pytest.fixture(name="connect") +def mock_controller_connect(): + """Mock a successful connection.""" + with patch("homeassistant.components.freebox.router.Freepybox") as service_mock: + service_mock.return_value.open = CoroutineMock() + service_mock.return_value.system.get_config = CoroutineMock() + service_mock.return_value.lan.get_hosts_list = CoroutineMock() + service_mock.return_value.connection.get_status = CoroutineMock() + service_mock.return_value.close = CoroutineMock() + yield service_mock + + +async def test_user(hass): + """Test user config.""" + 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" + + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + +async def test_import(hass): + """Test import step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + +async def test_discovery(hass): + """Test discovery step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DISCOVERY}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "link" + + +async def test_link(hass, connect): + """Test linking.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == HOST + assert result["title"] == HOST + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_PORT] == PORT + + +async def test_abort_if_already_setup(hass): + """Test we abort if component is already setup.""" + MockConfigEntry( + domain=DOMAIN, data={CONF_HOST: HOST, CONF_PORT: PORT}, unique_id=HOST + ).add_to_hass(hass) + + # Should fail, same HOST (import) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Should fail, same HOST (flow) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_on_link_failed(hass): + """Test when we have errors during linking the router.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: HOST, CONF_PORT: PORT}, + ) + + with patch( + "homeassistant.components.freebox.router.Freepybox.open", + side_effect=AuthorizationError(), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "register_failed"} + + with patch( + "homeassistant.components.freebox.router.Freepybox.open", + side_effect=HttpRequestError(), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "connection_failed"} + + with patch( + "homeassistant.components.freebox.router.Freepybox.open", + side_effect=InvalidTokenError(), + ): + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "unknown"} From 22415ce49ab262edebf84e00178e1737e075ec18 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 11 Mar 2020 22:41:30 +0100 Subject: [PATCH 002/431] Upgrade slacker to 0.14.0 (#32698) --- homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/slack/notify.py | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 2d78409e21a..72e2b0267d2 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -2,7 +2,7 @@ "domain": "slack", "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", - "requirements": ["slacker==0.13.0"], + "requirements": ["slacker==0.14.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index b645a590c3c..20daa261b8f 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -163,7 +163,7 @@ class SlackNotificationService(BaseNotificationService): return open(local_path, "rb") _LOGGER.warning("'%s' is not secure to load data from!", local_path) else: - _LOGGER.warning("Neither URL nor local path found in params!") + _LOGGER.warning("Neither URL nor local path found in parameters!") except OSError as error: _LOGGER.error("Can't load from URL or local path: %s", error) diff --git a/requirements_all.txt b/requirements_all.txt index 2d7434f6293..c1ea32f0517 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1864,7 +1864,7 @@ sisyphus-control==2.2.1 skybellpy==0.4.0 # homeassistant.components.slack -slacker==0.13.0 +slacker==0.14.0 # homeassistant.components.sleepiq sleepyq==0.7 From da761fdd397d6397f12e9ab39d3e31894c9e7e65 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 11 Mar 2020 23:06:35 +0100 Subject: [PATCH 003/431] Upgrade pylast to 3.2.1 (#32700) --- homeassistant/components/lastfm/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lastfm/manifest.json b/homeassistant/components/lastfm/manifest.json index d00d7d352f2..681047a2431 100644 --- a/homeassistant/components/lastfm/manifest.json +++ b/homeassistant/components/lastfm/manifest.json @@ -2,7 +2,7 @@ "domain": "lastfm", "name": "Last.fm", "documentation": "https://www.home-assistant.io/integrations/lastfm", - "requirements": ["pylast==3.2.0"], + "requirements": ["pylast==3.2.1"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index c1ea32f0517..85f292757e6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1345,7 +1345,7 @@ pykwb==0.0.8 pylacrosse==0.4.0 # homeassistant.components.lastfm -pylast==3.2.0 +pylast==3.2.1 # homeassistant.components.launch_library pylaunches==0.2.0 From 5f5cb8bea8b92ebb0f30ed30b680b82d5f441250 Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Wed, 11 Mar 2020 18:34:50 -0500 Subject: [PATCH 004/431] Add support for simultaneous runs of Script helper - Part 2 (#32442) * Add limit parameter to service call methods * Break out prep part of async_call_from_config for use elsewhere * Minor cleanup * Fix improper use of asyncio.wait * Fix state update Call change listener immediately if its a callback * Fix exception handling and logging * Merge Script helper if_running/run_mode parameters into script_mode - Remove background/blocking _ScriptRun subclasses which are no longer needed. * Add queued script mode * Disable timeout when making fully blocking script call * Don't call change listener when restarting script This makes restart mode behavior consistent with parallel & queue modes. * Changes per review - Call all script services (except script.turn_off) with no time limit. - Fix handling of lock in _QueuedScriptRun and add comments to make it clearer how this code works. * Changes per review 2 - Move cancel shielding "up" from _ScriptRun.async_run to Script.async_run (and apply to new style scripts only.) This makes sure Script class also properly handles cancellation which it wasn't doing before. - In _ScriptRun._async_call_service_step, instead of using script.turn_off service, just cancel service call and let it handle the cancellation accordingly. * Fix bugs - Add missing call to change listener in Script.async_run in cancelled path. - Cancel service task if ServiceRegistry.async_call cancelled. * Revert last changes to ServiceRegistry.async_call * Minor Script helper fixes & test improvements - Don't log asyncio.CancelledError exceptions. - Make change_listener a public attribute. - Test overhaul - Parametrize tests. - Use common test functions. - Mock timeout so tests don't need to wait for real time to elapse. - Add common function for waiting for script action step. --- homeassistant/core.py | 37 +- homeassistant/helpers/script.py | 362 ++++-- homeassistant/helpers/service.py | 44 +- tests/helpers/test_script.py | 1949 ++++++++++-------------------- 4 files changed, 948 insertions(+), 1444 deletions(-) diff --git a/homeassistant/core.py b/homeassistant/core.py index a1d9a83d1ad..afd1e4daa1a 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -93,7 +93,7 @@ SOURCE_DISCOVERED = "discovered" SOURCE_STORAGE = "storage" SOURCE_YAML = "yaml" -# How long to wait till things that run on startup have to finish. +# How long to wait until things that run on startup have to finish. TIMEOUT_EVENT_START = 15 _LOGGER = logging.getLogger(__name__) @@ -249,7 +249,7 @@ class HomeAssistant: try: # Only block for EVENT_HOMEASSISTANT_START listener self.async_stop_track_tasks() - with timeout(TIMEOUT_EVENT_START): + async with timeout(TIMEOUT_EVENT_START): await self.async_block_till_done() except asyncio.TimeoutError: _LOGGER.warning( @@ -374,13 +374,13 @@ class HomeAssistant: self.async_add_job(target, *args) def block_till_done(self) -> None: - """Block till all pending work is done.""" + """Block until all pending work is done.""" asyncio.run_coroutine_threadsafe( self.async_block_till_done(), self.loop ).result() async def async_block_till_done(self) -> None: - """Block till all pending work is done.""" + """Block until all pending work is done.""" # To flush out any call_soon_threadsafe await asyncio.sleep(0) @@ -1150,25 +1150,15 @@ class ServiceRegistry: service_data: Optional[Dict] = None, blocking: bool = False, context: Optional[Context] = None, + limit: Optional[float] = SERVICE_CALL_LIMIT, ) -> Optional[bool]: """ Call a service. - Specify blocking=True to wait till service is executed. - Waits a maximum of SERVICE_CALL_LIMIT. - - If blocking = True, will return boolean if service executed - successfully within SERVICE_CALL_LIMIT. - - This method will fire an event to call the service. - This event will be picked up by this ServiceRegistry and any - other ServiceRegistry that is listening on the EventBus. - - Because the service is sent as an event you are not allowed to use - the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. + See description of async_call for details. """ return asyncio.run_coroutine_threadsafe( - self.async_call(domain, service, service_data, blocking, context), + self.async_call(domain, service, service_data, blocking, context, limit), self._hass.loop, ).result() @@ -1179,19 +1169,18 @@ class ServiceRegistry: service_data: Optional[Dict] = None, blocking: bool = False, context: Optional[Context] = None, + limit: Optional[float] = SERVICE_CALL_LIMIT, ) -> Optional[bool]: """ Call a service. - Specify blocking=True to wait till service is executed. - Waits a maximum of SERVICE_CALL_LIMIT. + Specify blocking=True to wait until service is executed. + Waits a maximum of limit, which may be None for no timeout. If blocking = True, will return boolean if service executed - successfully within SERVICE_CALL_LIMIT. + successfully within limit. - This method will fire an event to call the service. - This event will be picked up by this ServiceRegistry and any - other ServiceRegistry that is listening on the EventBus. + This method will fire an event to indicate the service has been called. Because the service is sent as an event you are not allowed to use the keys ATTR_DOMAIN and ATTR_SERVICE in your service_data. @@ -1230,7 +1219,7 @@ class ServiceRegistry: return None try: - with timeout(SERVICE_CALL_LIMIT): + async with timeout(limit): await asyncio.shield(self._execute_service(handler, service_call)) return True except asyncio.TimeoutError: diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 937a675aada..7d1088eebe4 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -7,6 +7,7 @@ from itertools import islice import logging from typing import Any, Callable, Dict, List, Optional, Sequence, Set, Tuple, cast +from async_timeout import timeout import voluptuous as vol from homeassistant import exceptions @@ -14,6 +15,7 @@ import homeassistant.components.device_automation as device_automation import homeassistant.components.scene as scene from homeassistant.const import ( ATTR_ENTITY_ID, + CONF_ALIAS, CONF_CONDITION, CONF_CONTINUE_ON_TIMEOUT, CONF_DELAY, @@ -25,47 +27,53 @@ from homeassistant.const import ( CONF_SCENE, CONF_TIMEOUT, CONF_WAIT_TEMPLATE, + SERVICE_TURN_OFF, SERVICE_TURN_ON, ) -from homeassistant.core import CALLBACK_TYPE, Context, HomeAssistant, callback +from homeassistant.core import ( + CALLBACK_TYPE, + SERVICE_CALL_LIMIT, + Context, + HomeAssistant, + callback, + is_callback, +) from homeassistant.helpers import ( condition, config_validation as cv, - service, template as template, ) from homeassistant.helpers.event import ( async_track_point_in_utc_time, async_track_template, ) +from homeassistant.helpers.service import ( + CONF_SERVICE_DATA, + async_prepare_call_from_config, +) from homeassistant.helpers.typing import ConfigType +from homeassistant.util import slugify from homeassistant.util.dt import utcnow # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs -CONF_ALIAS = "alias" - -IF_RUNNING_ERROR = "error" -IF_RUNNING_IGNORE = "ignore" -IF_RUNNING_PARALLEL = "parallel" -IF_RUNNING_RESTART = "restart" -# First choice is default -IF_RUNNING_CHOICES = [ - IF_RUNNING_PARALLEL, - IF_RUNNING_ERROR, - IF_RUNNING_IGNORE, - IF_RUNNING_RESTART, +SCRIPT_MODE_ERROR = "error" +SCRIPT_MODE_IGNORE = "ignore" +SCRIPT_MODE_LEGACY = "legacy" +SCRIPT_MODE_PARALLEL = "parallel" +SCRIPT_MODE_QUEUE = "queue" +SCRIPT_MODE_RESTART = "restart" +SCRIPT_MODE_CHOICES = [ + SCRIPT_MODE_ERROR, + SCRIPT_MODE_IGNORE, + SCRIPT_MODE_LEGACY, + SCRIPT_MODE_PARALLEL, + SCRIPT_MODE_QUEUE, + SCRIPT_MODE_RESTART, ] +DEFAULT_SCRIPT_MODE = SCRIPT_MODE_LEGACY -RUN_MODE_BACKGROUND = "background" -RUN_MODE_BLOCKING = "blocking" -RUN_MODE_LEGACY = "legacy" -# First choice is default -RUN_MODE_CHOICES = [ - RUN_MODE_BLOCKING, - RUN_MODE_BACKGROUND, - RUN_MODE_LEGACY, -] +DEFAULT_QUEUE_MAX = 10 _LOG_EXCEPTION = logging.ERROR + 1 _TIMEOUT_MSG = "Timeout reached, abort script." @@ -102,6 +110,14 @@ class _SuspendScript(Exception): """Throw if script needs to suspend.""" +class AlreadyRunning(exceptions.HomeAssistantError): + """Throw if script already running and user wants error.""" + + +class QueueFull(exceptions.HomeAssistantError): + """Throw if script already running, user wants new run queued, but queue is full.""" + + class _ScriptRunBase(ABC): """Common data & methods for managing Script sequence run.""" @@ -137,11 +153,11 @@ class _ScriptRunBase(ABC): await getattr( self, f"_async_{cv.determine_script_action(self._action)}_step" )() - except Exception as err: - if not isinstance(err, (_SuspendScript, _StopScript)) and ( - self._log_exceptions or log_exceptions - ): - self._log_exception(err) + except Exception as ex: + if not isinstance( + ex, (_SuspendScript, _StopScript, asyncio.CancelledError) + ) and (self._log_exceptions or log_exceptions): + self._log_exception(ex) raise @abstractmethod @@ -166,6 +182,12 @@ class _ScriptRunBase(ABC): elif isinstance(exception, exceptions.ServiceNotFound): error_desc = "Service not found" + elif isinstance(exception, AlreadyRunning): + error_desc = "Already running" + + elif isinstance(exception, QueueFull): + error_desc = "Run queue is full" + else: error_desc = "Unexpected error" level = _LOG_EXCEPTION @@ -189,12 +211,13 @@ class _ScriptRunBase(ABC): template.render_complex(self._action[CONF_DELAY], self._variables) ) except (exceptions.TemplateError, vol.Invalid) as ex: - self._raise( + self._log( "Error rendering %s delay template: %s", self._script.name, ex, - exception=_StopScript, + level=logging.ERROR, ) + raise _StopScript self._script.last_action = self._action.get(CONF_ALIAS, f"delay {delay}") self._log("Executing step %s", self._script.last_action) @@ -220,18 +243,14 @@ class _ScriptRunBase(ABC): self._hass, wait_template, async_script_wait, self._variables ) + @abstractmethod async def _async_call_service_step(self): """Call the service specified in the action.""" + + def _prep_call_service_step(self): self._script.last_action = self._action.get(CONF_ALIAS, "call service") self._log("Executing step %s", self._script.last_action) - await service.async_call_from_config( - self._hass, - self._action, - blocking=True, - variables=self._variables, - validate_config=False, - context=self._context, - ) + return async_prepare_call_from_config(self._hass, self._action, self._variables) async def _async_device_step(self): """Perform the device automation specified in the action.""" @@ -298,10 +317,6 @@ class _ScriptRunBase(ABC): def _log(self, msg, *args, level=logging.INFO): self._script._log(msg, *args, level=level) # pylint: disable=protected-access - def _raise(self, msg, *args, exception=None): - # pylint: disable=protected-access - self._script._raise(msg, *args, exception=exception) - class _ScriptRun(_ScriptRunBase): """Manage Script sequence run.""" @@ -318,24 +333,33 @@ class _ScriptRun(_ScriptRunBase): self._stop = asyncio.Event() self._stopped = asyncio.Event() - async def _async_run(self, propagate_exceptions=True): - self._log("Running script") + def _changed(self): + if not self._stop.is_set(): + super()._changed() + + async def async_run(self) -> None: + """Run script.""" try: + if self._stop.is_set(): + return + self._script.last_triggered = utcnow() + self._changed() + self._log("Running script") for self._step, self._action in enumerate(self._script.sequence): if self._stop.is_set(): break - await self._async_step(not propagate_exceptions) + await self._async_step(log_exceptions=False) except _StopScript: pass - except Exception: # pylint: disable=broad-except - if propagate_exceptions: - raise finally: - if not self._stop.is_set(): - self._changed() + self._finish() + + def _finish(self): + self._script._runs.remove(self) # pylint: disable=protected-access + if not self._script.is_running: self._script.last_action = None - self._script._runs.remove(self) # pylint: disable=protected-access - self._stopped.set() + self._changed() + self._stopped.set() async def async_stop(self) -> None: """Stop script run.""" @@ -344,10 +368,13 @@ class _ScriptRun(_ScriptRunBase): async def _async_delay_step(self): """Handle delay.""" - timeout = self._prep_delay_step().total_seconds() - if not self._stop.is_set(): - self._changed() - await asyncio.wait({self._stop.wait()}, timeout=timeout) + delay = self._prep_delay_step().total_seconds() + self._changed() + try: + async with timeout(delay): + await self._stop.wait() + except asyncio.TimeoutError: + pass async def _async_wait_template_step(self): """Handle a wait template.""" @@ -361,21 +388,20 @@ class _ScriptRun(_ScriptRunBase): if not unsub: return - if not self._stop.is_set(): - self._changed() + self._changed() try: - timeout = self._action[CONF_TIMEOUT].total_seconds() + delay = self._action[CONF_TIMEOUT].total_seconds() except KeyError: - timeout = None + delay = None done = asyncio.Event() try: - await asyncio.wait_for( - asyncio.wait( + async with timeout(delay): + _, pending = await asyncio.wait( {self._stop.wait(), done.wait()}, return_when=asyncio.FIRST_COMPLETED, - ), - timeout, - ) + ) + for pending_task in pending: + pending_task.cancel() except asyncio.TimeoutError: if not self._action.get(CONF_CONTINUE_ON_TIMEOUT, True): self._log(_TIMEOUT_MSG) @@ -383,25 +409,78 @@ class _ScriptRun(_ScriptRunBase): finally: unsub() + async def _async_call_service_step(self): + """Call the service specified in the action.""" + domain, service, service_data = self._prep_call_service_step() -class _BackgroundScriptRun(_ScriptRun): - """Manage background Script sequence run.""" + # If this might start a script then disable the call timeout. + # Otherwise use the normal service call limit. + if domain == "script" and service != SERVICE_TURN_OFF: + limit = None + else: + limit = SERVICE_CALL_LIMIT + + coro = self._hass.services.async_call( + domain, + service, + service_data, + blocking=True, + context=self._context, + limit=limit, + ) + + if limit is not None: + # There is a call limit, so just wait for it to finish. + await coro + return + + # No call limit (i.e., potentially starting one or more fully blocking scripts) + # so watch for a stop request. + done, pending = await asyncio.wait( + {self._stop.wait(), coro}, return_when=asyncio.FIRST_COMPLETED, + ) + # Note that cancelling the service call, if it has not yet returned, will also + # stop any non-background script runs that it may have started. + for pending_task in pending: + pending_task.cancel() + # Propagate any exceptions that might have happened. + for done_task in done: + done_task.result() + + +class _QueuedScriptRun(_ScriptRun): + """Manage queued Script sequence run.""" + + lock_acquired = False async def async_run(self) -> None: """Run script.""" - self._hass.async_create_task(self._async_run(False)) + # Wait for previous run, if any, to finish by attempting to acquire the script's + # shared lock. At the same time monitor if we've been told to stop. + lock_task = self._hass.async_create_task( + self._script._queue_lck.acquire() # pylint: disable=protected-access + ) + done, pending = await asyncio.wait( + {self._stop.wait(), lock_task}, return_when=asyncio.FIRST_COMPLETED + ) + for pending_task in pending: + pending_task.cancel() + self.lock_acquired = lock_task in done + # If we've been told to stop, then just finish up. Otherwise, we've acquired the + # lock so we can go ahead and start the run. + if self._stop.is_set(): + self._finish() + else: + await super().async_run() -class _BlockingScriptRun(_ScriptRun): - """Manage blocking Script sequence run.""" - - async def async_run(self) -> None: - """Run script.""" - try: - await asyncio.shield(self._async_run()) - except asyncio.CancelledError: - await self.async_stop() - raise + def _finish(self): + # pylint: disable=protected-access + self._script._queue_len -= 1 + if self.lock_acquired: + self._script._queue_lck.release() + self.lock_acquired = False + super()._finish() class _LegacyScriptRun(_ScriptRunBase): @@ -445,6 +524,7 @@ class _LegacyScriptRun(_ScriptRunBase): async def _async_run(self, propagate_exceptions=True): if self._cur == -1: + self._script.last_triggered = utcnow() self._log("Running script") self._cur = 0 @@ -457,7 +537,7 @@ class _LegacyScriptRun(_ScriptRunBase): for self._step, self._action in islice( enumerate(self._script.sequence), self._cur, None ): - await self._async_step(not propagate_exceptions) + await self._async_step(log_exceptions=not propagate_exceptions) except _StopScript: pass except _SuspendScript: @@ -469,11 +549,12 @@ class _LegacyScriptRun(_ScriptRunBase): if propagate_exceptions: raise finally: - if self._cur != -1: - self._changed() + _cur_was = self._cur if not suspended: self._script.last_action = None await self.async_stop() + if _cur_was != -1: + self._changed() async def async_stop(self) -> None: """Stop script run.""" @@ -512,9 +593,9 @@ class _LegacyScriptRun(_ScriptRunBase): @callback def async_script_timeout(now): - """Call after timeout is retrieve.""" + """Call after timeout has expired.""" with suppress(ValueError): - self._async_listener.remove(unsub) + self._async_listener.remove(unsub_timeout) # Check if we want to continue to execute # the script after the timeout @@ -530,13 +611,19 @@ class _LegacyScriptRun(_ScriptRunBase): self._async_listener.append(unsub_wait) if CONF_TIMEOUT in self._action: - unsub = async_track_point_in_utc_time( + unsub_timeout = async_track_point_in_utc_time( self._hass, async_script_timeout, utcnow() + self._action[CONF_TIMEOUT] ) - self._async_listener.append(unsub) + self._async_listener.append(unsub_timeout) raise _SuspendScript + async def _async_call_service_step(self): + """Call the service specified in the action.""" + await self._hass.services.async_call( + *self._prep_call_service_step(), blocking=True, context=self._context + ) + def _async_remove_listener(self): """Remove listeners, if any.""" for unsub in self._async_listener: @@ -553,47 +640,60 @@ class Script: sequence: Sequence[Dict[str, Any]], name: Optional[str] = None, change_listener: Optional[Callable[..., Any]] = None, - if_running: Optional[str] = None, - run_mode: Optional[str] = None, + script_mode: str = DEFAULT_SCRIPT_MODE, + queue_max: int = DEFAULT_QUEUE_MAX, logger: Optional[logging.Logger] = None, log_exceptions: bool = True, ) -> None: """Initialize the script.""" - self._logger = logger or logging.getLogger(__name__) self._hass = hass self.sequence = sequence template.attach(hass, self.sequence) self.name = name - self._change_listener = change_listener + self.change_listener = change_listener + self._script_mode = script_mode + if logger: + self._logger = logger + else: + logger_name = __name__ + if name: + logger_name = ".".join([logger_name, slugify(name)]) + self._logger = logging.getLogger(logger_name) + self._log_exceptions = log_exceptions + self.last_action = None self.last_triggered: Optional[datetime] = None - self.can_cancel = any( + self.can_cancel = not self.is_legacy or any( CONF_DELAY in action or CONF_WAIT_TEMPLATE in action for action in self.sequence ) - if not if_running and not run_mode: - self._if_running = IF_RUNNING_PARALLEL - self._run_mode = RUN_MODE_LEGACY - elif if_running and run_mode == RUN_MODE_LEGACY: - self._raise('Cannot use if_running if run_mode is "legacy"') - else: - self._if_running = if_running or IF_RUNNING_CHOICES[0] - self._run_mode = run_mode or RUN_MODE_CHOICES[0] + self._runs: List[_ScriptRunBase] = [] - self._log_exceptions = log_exceptions + if script_mode == SCRIPT_MODE_QUEUE: + self._queue_max = queue_max + self._queue_len = 0 + self._queue_lck = asyncio.Lock() self._config_cache: Dict[Set[Tuple], Callable[..., bool]] = {} self._referenced_entities: Optional[Set[str]] = None self._referenced_devices: Optional[Set[str]] = None def _changed(self): - if self._change_listener: - self._hass.async_add_job(self._change_listener) + if self.change_listener: + if is_callback(self.change_listener): + self.change_listener() + else: + self._hass.async_add_job(self.change_listener) @property def is_running(self) -> bool: """Return true if script is on.""" return len(self._runs) > 0 + @property + def is_legacy(self) -> bool: + """Return if using legacy mode.""" + return self._script_mode == SCRIPT_MODE_LEGACY + @property def referenced_devices(self): """Return a set of referenced devices.""" @@ -626,7 +726,7 @@ class Script: action = cv.determine_script_action(step) if action == cv.SCRIPT_ACTION_CALL_SERVICE: - data = step.get(service.CONF_SERVICE_DATA) + data = step.get(CONF_SERVICE_DATA) if not data: continue @@ -661,18 +761,26 @@ class Script: ) -> None: """Run script.""" if self.is_running: - if self._if_running == IF_RUNNING_IGNORE: + if self._script_mode == SCRIPT_MODE_IGNORE: self._log("Skipping script") return - if self._if_running == IF_RUNNING_ERROR: - self._raise("Already running") - if self._if_running == IF_RUNNING_RESTART: - self._log("Restarting script") - await self.async_stop() + if self._script_mode == SCRIPT_MODE_ERROR: + raise AlreadyRunning - self.last_triggered = utcnow() - if self._run_mode == RUN_MODE_LEGACY: + if self._script_mode == SCRIPT_MODE_RESTART: + self._log("Restarting script") + await self.async_stop(update_state=False) + elif self._script_mode == SCRIPT_MODE_QUEUE: + self._log( + "Queueing script behind %i run%s", + self._queue_len, + "s" if self._queue_len > 1 else "", + ) + if self._queue_len >= self._queue_max: + raise QueueFull + + if self.is_legacy: if self._runs: shared = cast(Optional[_LegacyScriptRun], self._runs[0]) else: @@ -681,23 +789,31 @@ class Script: self._hass, self, variables, context, self._log_exceptions, shared ) else: - if self._run_mode == RUN_MODE_BACKGROUND: - run = _BackgroundScriptRun( - self._hass, self, variables, context, self._log_exceptions - ) + if self._script_mode != SCRIPT_MODE_QUEUE: + cls = _ScriptRun else: - run = _BlockingScriptRun( - self._hass, self, variables, context, self._log_exceptions - ) + cls = _QueuedScriptRun + self._queue_len += 1 + run = cls(self._hass, self, variables, context, self._log_exceptions) self._runs.append(run) - await run.async_run() - async def async_stop(self) -> None: + try: + if self.is_legacy: + await run.async_run() + else: + await asyncio.shield(run.async_run()) + except asyncio.CancelledError: + await run.async_stop() + self._changed() + raise + + async def async_stop(self, update_state: bool = True) -> None: """Stop running script.""" if not self.is_running: return await asyncio.shield(asyncio.gather(*(run.async_stop() for run in self._runs))) - self._changed() + if update_state: + self._changed() def _log(self, msg, *args, level=logging.INFO): if self.name: @@ -706,9 +822,3 @@ class Script: self._logger.exception(msg, *args) else: self._logger.log(level, msg, *args) - - def _raise(self, msg, *args, exception=None): - if not exception: - exception = exceptions.HomeAssistantError - self._log(msg, *args, level=logging.ERROR) - raise exception(msg % args) diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index 578d5368314..7a352b4e8d1 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -56,12 +56,27 @@ async def async_call_from_config( hass, config, blocking=False, variables=None, validate_config=True, context=None ): """Call a service based on a config hash.""" + try: + parms = async_prepare_call_from_config(hass, config, variables, validate_config) + except HomeAssistantError as ex: + if blocking: + raise + _LOGGER.error(ex) + else: + await hass.services.async_call(*parms, blocking, context) + + +@ha.callback +@bind_hass +def async_prepare_call_from_config(hass, config, variables=None, validate_config=False): + """Prepare to call a service based on a config hash.""" if validate_config: try: config = cv.SERVICE_SCHEMA(config) except vol.Invalid as ex: - _LOGGER.error("Invalid config for calling service: %s", ex) - return + raise HomeAssistantError( + f"Invalid config for calling service: {ex}" + ) from ex if CONF_SERVICE in config: domain_service = config[CONF_SERVICE] @@ -71,17 +86,15 @@ async def async_call_from_config( domain_service = config[CONF_SERVICE_TEMPLATE].async_render(variables) domain_service = cv.service(domain_service) except TemplateError as ex: - if blocking: - raise - _LOGGER.error("Error rendering service name template: %s", ex) - return - except vol.Invalid: - if blocking: - raise - _LOGGER.error("Template rendered invalid service: %s", domain_service) - return + raise HomeAssistantError( + f"Error rendering service name template: {ex}" + ) from ex + except vol.Invalid as ex: + raise HomeAssistantError( + f"Template rendered invalid service: {domain_service}" + ) from ex - domain, service_name = domain_service.split(".", 1) + domain, service = domain_service.split(".", 1) service_data = dict(config.get(CONF_SERVICE_DATA, {})) if CONF_SERVICE_DATA_TEMPLATE in config: @@ -91,15 +104,12 @@ async def async_call_from_config( template.render_complex(config[CONF_SERVICE_DATA_TEMPLATE], variables) ) except TemplateError as ex: - _LOGGER.error("Error rendering data template: %s", ex) - return + raise HomeAssistantError(f"Error rendering data template: {ex}") from ex if CONF_SERVICE_ENTITY_ID in config: service_data[ATTR_ENTITY_ID] = config[CONF_SERVICE_ENTITY_ID] - await hass.services.async_call( - domain, service_name, service_data, blocking=blocking, context=context - ) + return domain, service, service_data @bind_hass diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 443b131b2aa..eb1d5e15020 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1,6 +1,7 @@ """The tests for the Script component.""" # pylint: disable=protected-access import asyncio +from contextlib import contextmanager from datetime import timedelta import logging from unittest import mock @@ -15,63 +16,106 @@ import homeassistant.components.scene as scene from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON from homeassistant.core import Context, callback from homeassistant.helpers import config_validation as cv, script +from homeassistant.helpers.event import async_call_later import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import ( + async_capture_events, + async_fire_time_changed, + async_mock_service, +) ENTITY_ID = "script.test" -_ALL_RUN_MODES = [None, "background", "blocking"] +_BASIC_SCRIPT_MODES = ("legacy", "parallel") -async def test_firing_event_basic(hass): +@pytest.fixture +def mock_timeout(hass, monkeypatch): + """Mock async_timeout.timeout.""" + + class MockTimeout: + def __init__(self, timeout): + self._timeout = timeout + self._loop = asyncio.get_event_loop() + self._task = None + self._cancelled = False + self._unsub = None + + async def __aenter__(self): + if self._timeout is None: + return self + self._task = asyncio.Task.current_task() + if self._timeout <= 0: + self._loop.call_soon(self._cancel_task) + return self + # Wait for a time_changed event instead of real time passing. + self._unsub = async_call_later(hass, self._timeout, self._cancel_task) + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + if exc_type is asyncio.CancelledError and self._cancelled: + self._unsub = None + self._task = None + raise asyncio.TimeoutError + if self._timeout is not None and self._unsub: + self._unsub() + self._unsub = None + self._task = None + return None + + @callback + def _cancel_task(self, now=None): + if self._task is not None: + self._task.cancel() + self._cancelled = True + + monkeypatch.setattr(script, "timeout", MockTimeout) + + +def async_watch_for_action(script_obj, message): + """Watch for message in last_action.""" + flag = asyncio.Event() + + @callback + def check_action(): + if script_obj.last_action and message in script_obj.last_action: + flag.set() + + script_obj.change_listener = check_action + return flag + + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_firing_event_basic(hass, script_mode): """Test the firing of events.""" event = "test_event" context = Context() + events = async_capture_events(hass, event) - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) + sequence = cv.SCRIPT_SCHEMA({"event": event, "event_data": {"hello": "world"}}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - hass.bus.async_listen(event, record_event) + assert script_obj.is_legacy == (script_mode == "legacy") + assert script_obj.can_cancel == (script_mode != "legacy") - schema = cv.SCRIPT_SCHEMA({"event": event, "event_data": {"hello": "world"}}) + await script_obj.async_run(context=context) + await hass.async_block_till_done() - # For this one test we'll make sure "legacy" works the same as None. - for run_mode in _ALL_RUN_MODES + ["legacy"]: - events = [] - - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - assert not script_obj.can_cancel - - await script_obj.async_run(context=context) - - await hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].context is context - assert events[0].data.get("hello") == "world" - assert not script_obj.can_cancel + assert len(events) == 1 + assert events[0].context is context + assert events[0].data.get("hello") == "world" + assert script_obj.can_cancel == (script_mode != "legacy") -async def test_firing_event_template(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_firing_event_template(hass, script_mode): """Test the firing of events.""" event = "test_event" context = Context() + events = async_capture_events(hass, event) - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - schema = cv.SCRIPT_SCHEMA( + sequence = cv.SCRIPT_SCHEMA( { "event": event, "event_data_template": { @@ -84,152 +128,47 @@ async def test_firing_event_template(hass): }, } ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - for run_mode in _ALL_RUN_MODES: - events = [] + assert script_obj.can_cancel == (script_mode != "legacy") - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) + await script_obj.async_run({"is_world": "yes"}, context=context) + await hass.async_block_till_done() - assert not script_obj.can_cancel - - await script_obj.async_run({"is_world": "yes"}, context=context) - - await hass.async_block_till_done() - - assert len(events) == 1 - assert events[0].context is context - assert events[0].data == { - "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, - "list": ["yes", "yesyes"], - } + assert len(events) == 1 + assert events[0].context is context + assert events[0].data == { + "dict": {1: "yes", 2: "yesyes", 3: "yesyesyes"}, + "list": ["yes", "yesyes"], + } -async def test_calling_service_basic(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_calling_service_basic(hass, script_mode): """Test the calling of a service.""" context = Context() + calls = async_mock_service(hass, "test", "script") - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) + sequence = cv.SCRIPT_SCHEMA({"service": "test.script", "data": {"hello": "world"}}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - hass.services.async_register("test", "script", record_call) + assert script_obj.can_cancel == (script_mode != "legacy") - schema = cv.SCRIPT_SCHEMA({"service": "test.script", "data": {"hello": "world"}}) + await script_obj.async_run(context=context) + await hass.async_block_till_done() - for run_mode in _ALL_RUN_MODES: - calls = [] - - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - assert not script_obj.can_cancel - - await script_obj.async_run(context=context) - - await hass.async_block_till_done() - - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data.get("hello") == "world" + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get("hello") == "world" -async def test_cancel_no_wait(hass, caplog): - """Test stopping script.""" - event = "test_event" - - async def async_simulate_long_service(service): - """Simulate a service that takes a not insignificant time.""" - await asyncio.sleep(0.01) - - hass.services.async_register("test", "script", async_simulate_long_service) - - @callback - def monitor_event(event): - """Signal event happened.""" - event_sem.release() - - hass.bus.async_listen(event, monitor_event) - - schema = cv.SCRIPT_SCHEMA([{"event": event}, {"service": "test.script"}]) - - for run_mode in _ALL_RUN_MODES: - event_sem = asyncio.Semaphore(0) - - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - tasks = [] - for _ in range(3): - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - tasks.append(hass.async_create_task(event_sem.acquire())) - await asyncio.wait_for(asyncio.gather(*tasks), 1) - - # Can't assert just yet because we haven't verified stopping works yet. - # If assert fails we can hang test if async_stop doesn't work. - script_was_runing = script_obj.is_running - - await script_obj.async_stop() - await hass.async_block_till_done() - - assert script_was_runing - assert not script_obj.is_running - - -async def test_activating_scene(hass): - """Test the activation of a scene.""" - context = Context() - - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - hass.services.async_register(scene.DOMAIN, SERVICE_TURN_ON, record_call) - - schema = cv.SCRIPT_SCHEMA({"scene": "scene.hello"}) - - for run_mode in _ALL_RUN_MODES: - calls = [] - - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - assert not script_obj.can_cancel - - await script_obj.async_run(context=context) - - await hass.async_block_till_done() - - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" - - -async def test_calling_service_template(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_calling_service_template(hass, script_mode): """Test the calling of a service.""" context = Context() + calls = async_mock_service(hass, "test", "script") - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - hass.services.async_register("test", "script", record_call) - - schema = cv.SCRIPT_SCHEMA( + sequence = cv.SCRIPT_SCHEMA( { "service_template": """ {% if True %} @@ -248,32 +187,30 @@ async def test_calling_service_template(hass): }, } ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - for run_mode in _ALL_RUN_MODES: - calls = [] + assert script_obj.can_cancel == (script_mode != "legacy") - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) + await script_obj.async_run({"is_world": "yes"}, context=context) + await hass.async_block_till_done() - assert not script_obj.can_cancel - - await script_obj.async_run({"is_world": "yes"}, context=context) - - await hass.async_block_till_done() - - assert len(calls) == 1 - assert calls[0].context is context - assert calls[0].data.get("hello") == "world" + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get("hello") == "world" -async def test_multiple_runs_no_wait(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_multiple_runs_no_wait(hass, script_mode): """Test multiple runs with no wait in script.""" logger = logging.getLogger("TEST") + calls = [] + heard_event = asyncio.Event() async def async_simulate_long_service(service): """Simulate a service that takes a not insignificant time.""" + fire = service.data.get("fire") + listen = service.data.get("listen") + service_done = asyncio.Event() @callback def service_done_cb(event): @@ -281,29 +218,20 @@ async def test_multiple_runs_no_wait(hass): service_done.set() calls.append(service) - - fire = service.data.get("fire") - listen = service.data.get("listen") logger.debug("simulated service (%s:%s) started", fire, listen) - - service_done = asyncio.Event() unsub = hass.bus.async_listen(listen, service_done_cb) - hass.bus.async_fire(fire) - await service_done.wait() unsub() hass.services.async_register("test", "script", async_simulate_long_service) - heard_event = asyncio.Event() - @callback def heard_event_cb(event): logger.debug("heard: %s", event) heard_event.set() - schema = cv.SCRIPT_SCHEMA( + sequence = cv.SCRIPT_SCHEMA( [ { "service": "test.script", @@ -315,209 +243,199 @@ async def test_multiple_runs_no_wait(hass): }, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - for run_mode in _ALL_RUN_MODES: - calls = [] - heard_event.clear() + # Start script twice in such a way that second run will be started while first run + # is in the middle of the first service call. - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - # Start script twice in such a way that second run will be started while first - # run is in the middle of the first service call. - - unsub = hass.bus.async_listen("1", heard_event_cb) - - logger.debug("starting 1st script") - coro = script_obj.async_run( + unsub = hass.bus.async_listen("1", heard_event_cb) + logger.debug("starting 1st script") + hass.async_create_task( + script_obj.async_run( {"fire1": "1", "listen1": "2", "fire2": "3", "listen2": "4"} ) - if run_mode == "background": - await coro - else: - hass.async_create_task(coro) - await asyncio.wait_for(heard_event.wait(), 1) + ) + await asyncio.wait_for(heard_event.wait(), 1) + unsub() - unsub() + logger.debug("starting 2nd script") + await script_obj.async_run( + {"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"} + ) + await hass.async_block_till_done() - logger.debug("starting 2nd script") - await script_obj.async_run( - {"fire1": "2", "listen1": "3", "fire2": "4", "listen2": "4"} - ) - - await hass.async_block_till_done() - - assert len(calls) == 4 + assert len(calls) == 4 -async def test_delay_basic(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_activating_scene(hass, script_mode): + """Test the activation of a scene.""" + context = Context() + calls = async_mock_service(hass, scene.DOMAIN, SERVICE_TURN_ON) + + sequence = cv.SCRIPT_SCHEMA({"scene": "scene.hello"}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + + assert script_obj.can_cancel == (script_mode != "legacy") + + await script_obj.async_run(context=context) + await hass.async_block_till_done() + + assert len(calls) == 1 + assert calls[0].context is context + assert calls[0].data.get(ATTR_ENTITY_ID) == "scene.hello" + + +@pytest.mark.parametrize("count", [1, 3]) +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_stop_no_wait(hass, caplog, script_mode, count): + """Test stopping script.""" + service_started_sem = asyncio.Semaphore(0) + finish_service_event = asyncio.Event() + event = "test_event" + events = async_capture_events(hass, event) + + async def async_simulate_long_service(service): + """Simulate a service that takes a not insignificant time.""" + service_started_sem.release() + await finish_service_event.wait() + + hass.services.async_register("test", "script", async_simulate_long_service) + + sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + + # Get script started specified number of times and wait until the test.script + # service has started for each run. + tasks = [] + for _ in range(count): + hass.async_create_task(script_obj.async_run()) + tasks.append(hass.async_create_task(service_started_sem.acquire())) + await asyncio.wait_for(asyncio.gather(*tasks), 1) + + # Can't assert just yet because we haven't verified stopping works yet. + # If assert fails we can hang test if async_stop doesn't work. + script_was_runing = script_obj.is_running + were_no_events = len(events) == 0 + + # Begin the process of stopping the script (which should stop all runs), and then + # let the service calls complete. + hass.async_create_task(script_obj.async_stop()) + finish_service_event.set() + + await hass.async_block_till_done() + + assert script_was_runing + assert were_no_events + assert not script_obj.is_running + assert len(events) == (count if script_mode == "legacy" else 0) + + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_delay_basic(hass, mock_timeout, script_mode): """Test the delay.""" delay_alias = "delay step" - delay_started_flag = asyncio.Event() + sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": 5}, "alias": delay_alias}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + delay_started_flag = async_watch_for_action(script_obj, delay_alias) - @callback - def delay_started_cb(): - delay_started_flag.set() + assert script_obj.can_cancel - delay = timedelta(milliseconds=10) - schema = cv.SCRIPT_SCHEMA({"delay": delay, "alias": delay_alias}) + try: + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(delay_started_flag.wait(), 1) - for run_mode in _ALL_RUN_MODES: - delay_started_flag.clear() + assert script_obj.is_running + assert script_obj.last_action == delay_alias + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=delay_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=delay_started_cb, run_mode=run_mode - ) - - assert script_obj.can_cancel - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(delay_started_flag.wait(), 1) - - assert script_obj.is_running - assert script_obj.last_action == delay_alias - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + delay - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - assert script_obj.last_action is None + assert not script_obj.is_running + assert script_obj.last_action is None -async def test_multiple_runs_delay(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_multiple_runs_delay(hass, mock_timeout, script_mode): """Test multiple runs with delay in script.""" event = "test_event" - delay_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def delay_started_cb(): - delay_started_flag.set() - - delay = timedelta(milliseconds=10) - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + delay = timedelta(seconds=5) + sequence = cv.SCRIPT_SCHEMA( [ {"event": event, "event_data": {"value": 1}}, {"delay": delay}, {"event": event, "event_data": {"value": 2}}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + delay_started_flag = async_watch_for_action(script_obj, "delay") - for run_mode in _ALL_RUN_MODES: - events = [] - delay_started_flag.clear() + try: + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(delay_started_flag.wait(), 1) - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=delay_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=delay_started_cb, run_mode=run_mode - ) - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(delay_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 1 - assert events[-1].data["value"] == 1 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - # Start second run of script while first run is in a delay. + assert script_obj.is_running + assert len(events) == 1 + assert events[-1].data["value"] == 1 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + # Start second run of script while first run is in a delay. + if script_mode == "legacy": await script_obj.async_run() - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + delay - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - if run_mode in (None, "legacy"): - assert len(events) == 2 - else: - assert len(events) == 4 - assert events[-3].data["value"] == 1 - assert events[-2].data["value"] == 2 - assert events[-1].data["value"] == 2 - - -async def test_delay_template_ok(hass): - """Test the delay as a template.""" - delay_started_flag = asyncio.Event() - - @callback - def delay_started_cb(): - delay_started_flag.set() - - schema = cv.SCRIPT_SCHEMA({"delay": "00:00:{{ 1 }}"}) - - for run_mode in _ALL_RUN_MODES: - delay_started_flag.clear() - - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=delay_started_cb) else: - script_obj = script.Script( - hass, schema, change_listener=delay_started_cb, run_mode=run_mode - ) - - assert script_obj.can_cancel - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) + script_obj.sequence[1]["alias"] = "delay run 2" + delay_started_flag = async_watch_for_action(script_obj, "delay run 2") + hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(delay_started_flag.wait(), 1) - assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise + async_fire_time_changed(hass, dt_util.utcnow() + delay) + await hass.async_block_till_done() + + assert not script_obj.is_running + if script_mode == "legacy": + assert len(events) == 2 else: - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + timedelta(seconds=1) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running + assert len(events) == 4 + assert events[-3].data["value"] == 1 + assert events[-2].data["value"] == 2 + assert events[-1].data["value"] == 2 -async def test_delay_template_invalid(hass, caplog): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_delay_template_ok(hass, mock_timeout, script_mode): + """Test the delay as a template.""" + sequence = cv.SCRIPT_SCHEMA({"delay": "00:00:{{ 5 }}"}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + delay_started_flag = async_watch_for_action(script_obj, "delay") + + assert script_obj.can_cancel + + try: + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(delay_started_flag.wait(), 1) + + assert script_obj.is_running + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + assert not script_obj.is_running + + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_delay_template_invalid(hass, caplog, script_mode): """Test the delay as a template that fails.""" event = "test_event" - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( [ {"event": event}, {"delay": "{{ invalid_delay }}"}, @@ -525,83 +443,50 @@ async def test_delay_template_invalid(hass, caplog): {"event": event}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + start_idx = len(caplog.records) - for run_mode in _ALL_RUN_MODES: - events = [] + await script_obj.async_run() + await hass.async_block_till_done() - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - start_idx = len(caplog.records) + assert any( + rec.levelname == "ERROR" and "Error rendering" in rec.message + for rec in caplog.records[start_idx:] + ) - await script_obj.async_run() + assert not script_obj.is_running + assert len(events) == 1 + + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_delay_template_complex_ok(hass, mock_timeout, script_mode): + """Test the delay with a working complex template.""" + sequence = cv.SCRIPT_SCHEMA({"delay": {"seconds": "{{ 5 }}"}}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + delay_started_flag = async_watch_for_action(script_obj, "delay") + + assert script_obj.can_cancel + + try: + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(delay_started_flag.wait(), 1) + assert script_obj.is_running + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() - assert any( - rec.levelname == "ERROR" and "Error rendering" in rec.message - for rec in caplog.records[start_idx:] - ) - assert not script_obj.is_running - assert len(events) == 1 -async def test_delay_template_complex_ok(hass): - """Test the delay with a working complex template.""" - delay_started_flag = asyncio.Event() - - @callback - def delay_started_cb(): - delay_started_flag.set() - - milliseconds = 10 - schema = cv.SCRIPT_SCHEMA({"delay": {"milliseconds": "{{ milliseconds }}"}}) - - for run_mode in _ALL_RUN_MODES: - delay_started_flag.clear() - - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=delay_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=delay_started_cb, run_mode=run_mode - ) - - assert script_obj.can_cancel - - try: - coro = script_obj.async_run({"milliseconds": milliseconds}) - if run_mode == "background": - await coro - else: - hass.async_create_task(coro) - await asyncio.wait_for(delay_started_flag.wait(), 1) - assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + timedelta(milliseconds=milliseconds) - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - - -async def test_delay_template_complex_invalid(hass, caplog): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_delay_template_complex_invalid(hass, caplog, script_mode): """Test the delay with a complex template that fails.""" event = "test_event" - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( [ {"event": event}, {"delay": {"seconds": "{{ invalid_delay }}"}}, @@ -609,543 +494,260 @@ async def test_delay_template_complex_invalid(hass, caplog): {"event": event}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + start_idx = len(caplog.records) - for run_mode in _ALL_RUN_MODES: - events = [] + await script_obj.async_run() + await hass.async_block_till_done() - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - start_idx = len(caplog.records) + assert any( + rec.levelname == "ERROR" and "Error rendering" in rec.message + for rec in caplog.records[start_idx:] + ) - await script_obj.async_run() - await hass.async_block_till_done() + assert not script_obj.is_running + assert len(events) == 1 - assert any( - rec.levelname == "ERROR" and "Error rendering" in rec.message - for rec in caplog.records[start_idx:] - ) + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_cancel_delay(hass, script_mode): + """Test the cancelling while the delay is present.""" + event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA([{"delay": {"seconds": 5}}, {"event": event}]) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + delay_started_flag = async_watch_for_action(script_obj, "delay") + + try: + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(delay_started_flag.wait(), 1) + + assert script_obj.is_running + assert len(events) == 0 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + await script_obj.async_stop() assert not script_obj.is_running - assert len(events) == 1 + + # Make sure the script is really stopped. + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) + await hass.async_block_till_done() + + assert not script_obj.is_running + assert len(events) == 0 -async def test_cancel_delay(hass): - """Test the cancelling while the delay is present.""" - delay_started_flag = asyncio.Event() - event = "test_event" - - @callback - def delay_started_cb(): - delay_started_flag.set() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - delay = timedelta(milliseconds=10) - schema = cv.SCRIPT_SCHEMA([{"delay": delay}, {"event": event}]) - - for run_mode in _ALL_RUN_MODES: - delay_started_flag.clear() - events = [] - - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=delay_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=delay_started_cb, run_mode=run_mode - ) - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(delay_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - await script_obj.async_stop() - - assert not script_obj.is_running - - # Make sure the script is really stopped. - - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + delay - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - assert len(events) == 0 - - -async def test_wait_template_basic(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_wait_template_basic(hass, script_mode): """Test the wait template.""" wait_alias = "wait step" - wait_started_flag = asyncio.Event() - - @callback - def wait_started_cb(): - wait_started_flag.set() - - schema = cv.SCRIPT_SCHEMA( + sequence = cv.SCRIPT_SCHEMA( { "wait_template": "{{ states.switch.test.state == 'off' }}", "alias": wait_alias, } ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + wait_started_flag = async_watch_for_action(script_obj, wait_alias) - for run_mode in _ALL_RUN_MODES: - wait_started_flag.clear() + assert script_obj.can_cancel + + try: hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(wait_started_flag.wait(), 1) - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) + assert script_obj.is_running + assert script_obj.last_action == wait_alias + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() - assert script_obj.can_cancel - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - assert script_obj.last_action == wait_alias - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - hass.states.async_set("switch.test", "off") - await hass.async_block_till_done() - - assert not script_obj.is_running - assert script_obj.last_action is None + assert not script_obj.is_running + assert script_obj.last_action is None -async def test_multiple_runs_wait_template(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_multiple_runs_wait_template(hass, script_mode): """Test multiple runs with wait_template in script.""" event = "test_event" - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( [ {"event": event, "event_data": {"value": 1}}, {"wait_template": "{{ states.switch.test.state == 'off' }}"}, {"event": event, "event_data": {"value": 2}}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + wait_started_flag = async_watch_for_action(script_obj, "wait") - for run_mode in _ALL_RUN_MODES: - events = [] - wait_started_flag.clear() + try: hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(wait_started_flag.wait(), 1) - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) + assert script_obj.is_running + assert len(events) == 1 + assert events[-1].data["value"] == 1 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + # Start second run of script while first run is in wait_template. + if script_mode == "legacy": + await script_obj.async_run() else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) + hass.async_create_task(script_obj.async_run()) + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 1 - assert events[-1].data["value"] == 1 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise + assert not script_obj.is_running + if script_mode == "legacy": + assert len(events) == 2 else: - # Start second run of script while first run is in wait_template. - if run_mode == "blocking": - hass.async_create_task(script_obj.async_run()) - else: - await script_obj.async_run() - hass.states.async_set("switch.test", "off") - await hass.async_block_till_done() - - assert not script_obj.is_running - if run_mode in (None, "legacy"): - assert len(events) == 2 - else: - assert len(events) == 4 - assert events[-3].data["value"] == 1 - assert events[-2].data["value"] == 2 - assert events[-1].data["value"] == 2 + assert len(events) == 4 + assert events[-3].data["value"] == 1 + assert events[-2].data["value"] == 2 + assert events[-1].data["value"] == 2 -async def test_cancel_wait_template(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_cancel_wait_template(hass, script_mode): """Test the cancelling while wait_template is present.""" - wait_started_flag = asyncio.Event() event = "test_event" - - @callback - def wait_started_cb(): - wait_started_flag.set() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( [ {"wait_template": "{{ states.switch.test.state == 'off' }}"}, {"event": event}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + wait_started_flag = async_watch_for_action(script_obj, "wait") - for run_mode in _ALL_RUN_MODES: - wait_started_flag.clear() - events = [] + try: hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(wait_started_flag.wait(), 1) - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) + assert script_obj.is_running + assert len(events) == 0 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + await script_obj.async_stop() - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(wait_started_flag.wait(), 1) + assert not script_obj.is_running - assert script_obj.is_running - assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - await script_obj.async_stop() + # Make sure the script is really stopped. - assert not script_obj.is_running + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() - # Make sure the script is really stopped. - - hass.states.async_set("switch.test", "off") - await hass.async_block_till_done() - - assert not script_obj.is_running - assert len(events) == 0 + assert not script_obj.is_running + assert len(events) == 0 -async def test_wait_template_not_schedule(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_wait_template_not_schedule(hass, script_mode): """Test the wait template with correct condition.""" event = "test_event" - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - hass.states.async_set("switch.test", "on") - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( [ {"event": event}, {"wait_template": "{{ states.switch.test.state == 'on' }}"}, {"event": event}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - for run_mode in _ALL_RUN_MODES: - events = [] + hass.states.async_set("switch.test", "on") + await script_obj.async_run() + await hass.async_block_till_done() - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) + assert not script_obj.is_running + assert len(events) == 2 - await script_obj.async_run() + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +@pytest.mark.parametrize( + "continue_on_timeout,n_events", [(False, 0), (True, 1), (None, 1)] +) +async def test_wait_template_timeout( + hass, mock_timeout, continue_on_timeout, n_events, script_mode +): + """Test the wait template, halt on timeout.""" + event = "test_event" + events = async_capture_events(hass, event) + sequence = [ + {"wait_template": "{{ states.switch.test.state == 'off' }}", "timeout": 5}, + {"event": event}, + ] + if continue_on_timeout is not None: + sequence[0]["continue_on_timeout"] = continue_on_timeout + sequence = cv.SCRIPT_SCHEMA(sequence) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + wait_started_flag = async_watch_for_action(script_obj, "wait") + + try: + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) + await asyncio.wait_for(wait_started_flag.wait(), 1) + + assert script_obj.is_running + assert len(events) == 0 + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=5)) await hass.async_block_till_done() assert not script_obj.is_running - assert len(events) == 2 + assert len(events) == n_events -async def test_wait_template_timeout_halt(hass): - """Test the wait template, halt on timeout.""" - event = "test_event" - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - timeout = timedelta(milliseconds=10) - schema = cv.SCRIPT_SCHEMA( - [ - { - "wait_template": "{{ states.switch.test.state == 'off' }}", - "continue_on_timeout": False, - "timeout": timeout, - }, - {"event": event}, - ] - ) - - for run_mode in _ALL_RUN_MODES: - events = [] - wait_started_flag.clear() - - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + timeout - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - assert len(events) == 0 - - -async def test_wait_template_timeout_continue(hass): - """Test the wait template with continuing the script.""" - event = "test_event" - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - timeout = timedelta(milliseconds=10) - schema = cv.SCRIPT_SCHEMA( - [ - { - "wait_template": "{{ states.switch.test.state == 'off' }}", - "continue_on_timeout": True, - "timeout": timeout, - }, - {"event": event}, - ] - ) - - for run_mode in _ALL_RUN_MODES: - events = [] - wait_started_flag.clear() - - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + timeout - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - assert len(events) == 1 - - -async def test_wait_template_timeout_default(hass): - """Test the wait template with default continue.""" - event = "test_event" - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - timeout = timedelta(milliseconds=10) - schema = cv.SCRIPT_SCHEMA( - [ - { - "wait_template": "{{ states.switch.test.state == 'off' }}", - "timeout": timeout, - }, - {"event": event}, - ] - ) - - for run_mode in _ALL_RUN_MODES: - events = [] - wait_started_flag.clear() - - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) - - try: - if run_mode == "background": - await script_obj.async_run() - else: - hass.async_create_task(script_obj.async_run()) - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 0 - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - if run_mode in (None, "legacy"): - future = dt_util.utcnow() + timeout - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - assert not script_obj.is_running - assert len(events) == 1 - - -async def test_wait_template_variables(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_wait_template_variables(hass, script_mode): """Test the wait template with variables.""" - wait_started_flag = asyncio.Event() + sequence = cv.SCRIPT_SCHEMA({"wait_template": "{{ is_state(data, 'off') }}"}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + wait_started_flag = async_watch_for_action(script_obj, "wait") - @callback - def wait_started_cb(): - wait_started_flag.set() + assert script_obj.can_cancel - schema = cv.SCRIPT_SCHEMA({"wait_template": "{{ is_state(data, 'off') }}"}) - - for run_mode in _ALL_RUN_MODES: - wait_started_flag.clear() + try: hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run({"data": "switch.test"})) + await asyncio.wait_for(wait_started_flag.wait(), 1) - if run_mode is None: - script_obj = script.Script(hass, schema, change_listener=wait_started_cb) - else: - script_obj = script.Script( - hass, schema, change_listener=wait_started_cb, run_mode=run_mode - ) + assert script_obj.is_running + except (AssertionError, asyncio.TimeoutError): + await script_obj.async_stop() + raise + else: + hass.states.async_set("switch.test", "off") + await hass.async_block_till_done() - assert script_obj.can_cancel - - try: - coro = script_obj.async_run({"data": "switch.test"}) - if run_mode == "background": - await coro - else: - hass.async_create_task(coro) - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - hass.states.async_set("switch.test", "off") - await hass.async_block_till_done() - - assert not script_obj.is_running + assert not script_obj.is_running -async def test_condition_basic(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_condition_basic(hass, script_mode): """Test if we can use conditions in a script.""" event = "test_event" - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( [ {"event": event}, { @@ -1155,208 +757,127 @@ async def test_condition_basic(hass): {"event": event}, ] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - for run_mode in _ALL_RUN_MODES: - events = [] - hass.states.async_set("test.entity", "hello") - - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - assert not script_obj.can_cancel - - await script_obj.async_run() - await hass.async_block_till_done() - - assert len(events) == 2 - - hass.states.async_set("test.entity", "goodbye") - - await script_obj.async_run() - await hass.async_block_till_done() - - assert len(events) == 3 - - -@asynctest.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.""" - event = "test_event" - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) + assert script_obj.can_cancel == (script_mode != "legacy") hass.states.async_set("test.entity", "hello") + await script_obj.async_run() + await hass.async_block_till_done() - script_obj = script.Script( - hass, - cv.SCRIPT_SCHEMA( - [ - {"event": event}, - { - "condition": "template", - "value_template": '{{ states.test.entity.state == "hello" }}', - }, - {"event": event}, - ] - ), + assert len(events) == 2 + + hass.states.async_set("test.entity", "goodbye") + + await script_obj.async_run() + await hass.async_block_till_done() + + assert len(events) == 3 + + +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +@asynctest.patch("homeassistant.helpers.script.condition.async_from_config") +async def test_condition_created_once(async_from_config, hass, script_mode): + """Test that the conditions do not get created multiple times.""" + sequence = cv.SCRIPT_SCHEMA( + { + "condition": "template", + "value_template": '{{ states.test.entity.state == "hello" }}', + } ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) + async_from_config.reset_mock() + + hass.states.async_set("test.entity", "hello") await script_obj.async_run() await script_obj.async_run() await hass.async_block_till_done() - assert async_from_config.call_count == 1 + + async_from_config.assert_called_once() assert len(script_obj._config_cache) == 1 -async def test_condition_all_cached(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_condition_all_cached(hass, script_mode): """Test that multiple conditions get cached.""" - event = "test_event" - events = [] - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) + sequence = cv.SCRIPT_SCHEMA( + [ + { + "condition": "template", + "value_template": '{{ states.test.entity.state == "hello" }}', + }, + { + "condition": "template", + "value_template": '{{ states.test.entity.state != "hello" }}', + }, + ] + ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) hass.states.async_set("test.entity", "hello") - - script_obj = script.Script( - hass, - cv.SCRIPT_SCHEMA( - [ - {"event": event}, - { - "condition": "template", - "value_template": '{{ states.test.entity.state == "hello" }}', - }, - { - "condition": "template", - "value_template": '{{ states.test.entity.state != "hello" }}', - }, - {"event": event}, - ] - ), - ) - await script_obj.async_run() await hass.async_block_till_done() + assert len(script_obj._config_cache) == 2 -async def test_last_triggered(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_last_triggered(hass, script_mode): """Test the last_triggered.""" event = "test_event" + sequence = cv.SCRIPT_SCHEMA({"event": event}) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - schema = cv.SCRIPT_SCHEMA({"event": event}) + assert script_obj.last_triggered is None - for run_mode in _ALL_RUN_MODES: - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) + time = dt_util.utcnow() + with mock.patch("homeassistant.helpers.script.utcnow", return_value=time): + await script_obj.async_run() + await hass.async_block_till_done() - assert script_obj.last_triggered is None - - time = dt_util.utcnow() - with mock.patch("homeassistant.helpers.script.utcnow", return_value=time): - await script_obj.async_run() - await hass.async_block_till_done() - - assert script_obj.last_triggered == time + assert script_obj.last_triggered == time -async def test_propagate_error_service_not_found(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_propagate_error_service_not_found(hass, script_mode): """Test that a script aborts when a service is not found.""" event = "test_event" + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - @callback - def record_event(event): - events.append(event) + with pytest.raises(exceptions.ServiceNotFound): + await script_obj.async_run() - hass.bus.async_listen(event, record_event) - - schema = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) - - run_modes = _ALL_RUN_MODES - if "background" in run_modes: - run_modes.remove("background") - for run_mode in run_modes: - events = [] - - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - with pytest.raises(exceptions.ServiceNotFound): - await script_obj.async_run() - - assert len(events) == 0 - assert not script_obj.is_running + assert len(events) == 0 + assert not script_obj.is_running -async def test_propagate_error_invalid_service_data(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_propagate_error_invalid_service_data(hass, script_mode): """Test that a script aborts when we send invalid service data.""" event = "test_event" - - @callback - def record_event(event): - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def record_call(service): - """Add recorded event to set.""" - calls.append(service) - - hass.services.async_register( - "test", "script", record_call, schema=vol.Schema({"text": str}) - ) - - schema = cv.SCRIPT_SCHEMA( + events = async_capture_events(hass, event) + calls = async_mock_service(hass, "test", "script", vol.Schema({"text": str})) + sequence = cv.SCRIPT_SCHEMA( [{"service": "test.script", "data": {"text": 1}}, {"event": event}] ) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - run_modes = _ALL_RUN_MODES - if "background" in run_modes: - run_modes.remove("background") - for run_mode in run_modes: - events = [] - calls = [] + with pytest.raises(vol.Invalid): + await script_obj.async_run() - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - with pytest.raises(vol.Invalid): - await script_obj.async_run() - - assert len(events) == 0 - assert len(calls) == 0 - assert not script_obj.is_running + assert len(events) == 0 + assert len(calls) == 0 + assert not script_obj.is_running -async def test_propagate_error_service_exception(hass): +@pytest.mark.parametrize("script_mode", _BASIC_SCRIPT_MODES) +async def test_propagate_error_service_exception(hass, script_mode): """Test that a script aborts when a service throws an exception.""" event = "test_event" - - @callback - def record_event(event): - events.append(event) - - hass.bus.async_listen(event, record_event) + events = async_capture_events(hass, event) @callback def record_call(service): @@ -1365,24 +886,14 @@ async def test_propagate_error_service_exception(hass): hass.services.async_register("test", "script", record_call) - schema = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + sequence = cv.SCRIPT_SCHEMA([{"service": "test.script"}, {"event": event}]) + script_obj = script.Script(hass, sequence, script_mode=script_mode) - run_modes = _ALL_RUN_MODES - if "background" in run_modes: - run_modes.remove("background") - for run_mode in run_modes: - events = [] + with pytest.raises(ValueError): + await script_obj.async_run() - if run_mode is None: - script_obj = script.Script(hass, schema) - else: - script_obj = script.Script(hass, schema, run_mode=run_mode) - - with pytest.raises(ValueError): - await script_obj.async_run() - - assert len(events) == 0 - assert not script_obj.is_running + assert len(events) == 0 + assert not script_obj.is_running async def test_referenced_entities(): @@ -1441,68 +952,37 @@ async def test_referenced_devices(): assert script_obj.referenced_devices is script_obj.referenced_devices -async def test_if_running_with_legacy_run_mode(hass, caplog): - """Test using if_running with run_mode='legacy'.""" - # TODO: REMOVE - if _ALL_RUN_MODES == [None]: - return - - with pytest.raises(exceptions.HomeAssistantError): - script.Script( - hass, - [], - if_running="ignore", - run_mode="legacy", - logger=logging.getLogger("TEST"), - ) - assert any( - rec.levelname == "ERROR" - and rec.name == "TEST" - and all(text in rec.message for text in ("if_running", "legacy")) - for rec in caplog.records - ) +@contextmanager +def does_not_raise(): + """Indicate no exception is expected.""" + yield -async def test_if_running_ignore(hass, caplog): - """Test overlapping runs with if_running='ignore'.""" - # TODO: REMOVE - if _ALL_RUN_MODES == [None]: - return - +@pytest.mark.parametrize( + "script_mode,expectation,messages", + [ + ("ignore", does_not_raise(), ["Skipping"]), + ("error", pytest.raises(exceptions.HomeAssistantError), []), + ], +) +async def test_script_mode_1(hass, caplog, script_mode, expectation, messages): + """Test overlapping runs with script_mode='ignore'.""" event = "test_event" - events = [] - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - script_obj = script.Script( - hass, - cv.SCRIPT_SCHEMA( - [ - {"event": event, "event_data": {"value": 1}}, - {"wait_template": "{{ states.switch.test.state == 'off' }}"}, - {"event": event, "event_data": {"value": 2}}, - ] - ), - change_listener=wait_started_cb, - if_running="ignore", - run_mode="background", - logger=logging.getLogger("TEST"), + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + ] ) + logger = logging.getLogger("TEST") + script_obj = script.Script(hass, sequence, script_mode=script_mode, logger=logger) + wait_started_flag = async_watch_for_action(script_obj, "wait") try: - await script_obj.async_run() + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running @@ -1510,85 +990,19 @@ async def test_if_running_ignore(hass, caplog): assert events[0].data["value"] == 1 # Start second run of script while first run is suspended in wait_template. - # This should ignore second run. - await script_obj.async_run() - - assert script_obj.is_running - assert any( - rec.levelname == "INFO" and rec.name == "TEST" and "Skipping" in rec.message - for rec in caplog.records - ) - except (AssertionError, asyncio.TimeoutError): - await script_obj.async_stop() - raise - else: - hass.states.async_set("switch.test", "off") - await hass.async_block_till_done() - - assert not script_obj.is_running - assert len(events) == 2 - assert events[1].data["value"] == 2 - - -async def test_if_running_error(hass, caplog): - """Test overlapping runs with if_running='error'.""" - # TODO: REMOVE - if _ALL_RUN_MODES == [None]: - return - - event = "test_event" - events = [] - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - script_obj = script.Script( - hass, - cv.SCRIPT_SCHEMA( - [ - {"event": event, "event_data": {"value": 1}}, - {"wait_template": "{{ states.switch.test.state == 'off' }}"}, - {"event": event, "event_data": {"value": 2}}, - ] - ), - change_listener=wait_started_cb, - if_running="error", - run_mode="background", - logger=logging.getLogger("TEST"), - ) - - try: - await script_obj.async_run() - await asyncio.wait_for(wait_started_flag.wait(), 1) - - assert script_obj.is_running - assert len(events) == 1 - assert events[0].data["value"] == 1 - - # Start second run of script while first run is suspended in wait_template. - # This should cause an error. - - with pytest.raises(exceptions.HomeAssistantError): + with expectation: await script_obj.async_run() assert script_obj.is_running - assert any( - rec.levelname == "ERROR" - and rec.name == "TEST" - and "Already running" in rec.message - for rec in caplog.records + assert all( + any( + rec.levelname == "INFO" + and rec.name == "TEST" + and message in rec.message + for rec in caplog.records + ) + for message in messages ) except (AssertionError, asyncio.TimeoutError): await script_obj.async_stop() @@ -1602,46 +1016,28 @@ async def test_if_running_error(hass, caplog): assert events[1].data["value"] == 2 -async def test_if_running_restart(hass, caplog): - """Test overlapping runs with if_running='restart'.""" - # TODO: REMOVE - if _ALL_RUN_MODES == [None]: - return - +@pytest.mark.parametrize( + "script_mode,messages,last_events", + [("restart", ["Restarting"], [2]), ("parallel", [], [2, 2])], +) +async def test_script_mode_2(hass, caplog, script_mode, messages, last_events): + """Test overlapping runs with script_mode='restart'.""" event = "test_event" - events = [] - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - script_obj = script.Script( - hass, - cv.SCRIPT_SCHEMA( - [ - {"event": event, "event_data": {"value": 1}}, - {"wait_template": "{{ states.switch.test.state == 'off' }}"}, - {"event": event, "event_data": {"value": 2}}, - ] - ), - change_listener=wait_started_cb, - if_running="restart", - run_mode="background", - logger=logging.getLogger("TEST"), + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + ] ) + logger = logging.getLogger("TEST") + script_obj = script.Script(hass, sequence, script_mode=script_mode, logger=logger) + wait_started_flag = async_watch_for_action(script_obj, "wait") try: - await script_obj.async_run() + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running @@ -1652,17 +1048,20 @@ async def test_if_running_restart(hass, caplog): # This should stop first run then start a new run. wait_started_flag.clear() - await script_obj.async_run() + hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running assert len(events) == 2 assert events[1].data["value"] == 1 - assert any( - rec.levelname == "INFO" - and rec.name == "TEST" - and "Restarting" in rec.message - for rec in caplog.records + assert all( + any( + rec.levelname == "INFO" + and rec.name == "TEST" + and message in rec.message + for rec in caplog.records + ) + for message in messages ) except (AssertionError, asyncio.TimeoutError): await script_obj.async_stop() @@ -1672,50 +1071,30 @@ async def test_if_running_restart(hass, caplog): await hass.async_block_till_done() assert not script_obj.is_running - assert len(events) == 3 - assert events[2].data["value"] == 2 + assert len(events) == 2 + len(last_events) + for idx, value in enumerate(last_events, start=2): + assert events[idx].data["value"] == value -async def test_if_running_parallel(hass): - """Test overlapping runs with if_running='parallel'.""" - # TODO: REMOVE - if _ALL_RUN_MODES == [None]: - return - +async def test_script_mode_queue(hass): + """Test overlapping runs with script_mode='queue'.""" event = "test_event" - events = [] - wait_started_flag = asyncio.Event() - - @callback - def record_event(event): - """Add recorded event to set.""" - events.append(event) - - hass.bus.async_listen(event, record_event) - - @callback - def wait_started_cb(): - wait_started_flag.set() - - hass.states.async_set("switch.test", "on") - - script_obj = script.Script( - hass, - cv.SCRIPT_SCHEMA( - [ - {"event": event, "event_data": {"value": 1}}, - {"wait_template": "{{ states.switch.test.state == 'off' }}"}, - {"event": event, "event_data": {"value": 2}}, - ] - ), - change_listener=wait_started_cb, - if_running="parallel", - run_mode="background", - logger=logging.getLogger("TEST"), + events = async_capture_events(hass, event) + sequence = cv.SCRIPT_SCHEMA( + [ + {"event": event, "event_data": {"value": 1}}, + {"wait_template": "{{ states.switch.test.state == 'off' }}"}, + {"event": event, "event_data": {"value": 2}}, + {"wait_template": "{{ states.switch.test.state == 'on' }}"}, + ] ) + logger = logging.getLogger("TEST") + script_obj = script.Script(hass, sequence, script_mode="queue", logger=logger) + wait_started_flag = async_watch_for_action(script_obj, "wait") try: - await script_obj.async_run() + hass.states.async_set("switch.test", "on") + hass.async_create_task(script_obj.async_run()) await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running @@ -1723,23 +1102,39 @@ async def test_if_running_parallel(hass): assert events[0].data["value"] == 1 # Start second run of script while first run is suspended in wait_template. - # This should start a new, independent run. + # This second run should not start until the first run has finished. + + hass.async_create_task(script_obj.async_run()) + + await asyncio.sleep(0) + assert script_obj.is_running + assert len(events) == 1 wait_started_flag.clear() - await script_obj.async_run() + hass.states.async_set("switch.test", "off") await asyncio.wait_for(wait_started_flag.wait(), 1) assert script_obj.is_running assert len(events) == 2 - assert events[1].data["value"] == 1 + assert events[1].data["value"] == 2 + + wait_started_flag.clear() + hass.states.async_set("switch.test", "on") + await asyncio.wait_for(wait_started_flag.wait(), 1) + + await asyncio.sleep(0) + assert script_obj.is_running + assert len(events) == 3 + assert events[2].data["value"] == 1 except (AssertionError, asyncio.TimeoutError): await script_obj.async_stop() raise else: hass.states.async_set("switch.test", "off") + await asyncio.sleep(0) + hass.states.async_set("switch.test", "on") await hass.async_block_till_done() assert not script_obj.is_running assert len(events) == 4 - assert events[2].data["value"] == 2 assert events[3].data["value"] == 2 From 8bc542776b6ff4bcf607f4e48e7a909c878ac75f Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 12 Mar 2020 02:00:47 +0100 Subject: [PATCH 005/431] Cleanup entity and device registry on MQTT discovery removal (#32693) * Cleanup entity and device registry on MQTT discovery removal. * Review comments --- homeassistant/components/mqtt/__init__.py | 60 ++++- .../components/mqtt/device_trigger.py | 2 + tests/components/mqtt/common.py | 24 ++ .../mqtt/test_alarm_control_panel.py | 16 ++ tests/components/mqtt/test_binary_sensor.py | 15 ++ tests/components/mqtt/test_climate.py | 14 ++ tests/components/mqtt/test_cover.py | 14 ++ tests/components/mqtt/test_device_trigger.py | 212 +++++++++++++++++- tests/components/mqtt/test_fan.py | 14 ++ tests/components/mqtt/test_legacy_vacuum.py | 13 ++ tests/components/mqtt/test_light.py | 14 ++ tests/components/mqtt/test_light_json.py | 15 ++ tests/components/mqtt/test_light_template.py | 14 ++ tests/components/mqtt/test_lock.py | 14 ++ tests/components/mqtt/test_sensor.py | 13 ++ tests/components/mqtt/test_state_vacuum.py | 15 ++ tests/components/mqtt/test_switch.py | 14 ++ 17 files changed, 466 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index fbbf4f42d7a..90dd21ae307 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -37,7 +37,6 @@ from homeassistant.exceptions import ( Unauthorized, ) from homeassistant.helpers import config_validation as cv, event, template -from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceDataType @@ -1157,6 +1156,23 @@ class MqttAvailability(Entity): return availability_topic is None or self._available +async def cleanup_device_registry(hass, device_id): + """Remove device registry entry if there are no entities or triggers.""" + # Local import to avoid circular dependencies + from . import device_trigger + + device_registry = await hass.helpers.device_registry.async_get_registry() + entity_registry = await hass.helpers.entity_registry.async_get_registry() + if ( + device_id + and not hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id + ) + and not await device_trigger.async_get_triggers(hass, device_id) + ): + device_registry.async_remove_device(device_id) + + class MqttDiscoveryUpdate(Entity): """Mixin used to handle updated discovery message.""" @@ -1173,8 +1189,18 @@ class MqttDiscoveryUpdate(Entity): self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None ) + async def async_remove_from_registry(self) -> None: + """Remove entity from entity registry.""" + entity_registry = ( + await self.hass.helpers.entity_registry.async_get_registry() + ) + if entity_registry.async_is_registered(self.entity_id): + entity_entry = entity_registry.async_get(self.entity_id) + entity_registry.async_remove(self.entity_id) + await cleanup_device_registry(self.hass, entity_entry.device_id) + @callback - def discovery_callback(payload): + async def discovery_callback(payload): """Handle discovery update.""" _LOGGER.info( "Got update for entity with hash: %s '%s'", discovery_hash, payload, @@ -1182,13 +1208,13 @@ class MqttDiscoveryUpdate(Entity): if not payload: # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) - self.hass.async_create_task(self.async_remove()) - clear_discovery_hash(self.hass, discovery_hash) - self._remove_signal() + self._cleanup_on_remove() + await async_remove_from_registry(self) + await self.async_remove() elif self._discovery_update: # Non-empty payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) - self.hass.async_create_task(self._discovery_update(payload)) + await self._discovery_update(payload) if discovery_hash: self._remove_signal = async_dispatcher_connect( @@ -1199,15 +1225,25 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" - discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] - publish( - self.hass, discovery_topic, "", retain=True, - ) + if self._discovery_data: + discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] + publish( + self.hass, discovery_topic, "", retain=True, + ) async def async_will_remove_from_hass(self) -> None: - """Stop listening to signal.""" + """Stop listening to signal and cleanup discovery data..""" + self._cleanup_on_remove() + + def _cleanup_on_remove(self) -> None: + """Stop listening to signal and cleanup discovery data.""" + if self._discovery_data: + clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) + self._discovery_data = None + if self._remove_signal: self._remove_signal() + self._remove_signal = None def device_info_from_config(config): @@ -1270,7 +1306,7 @@ class MqttEntityDeviceInfo(Entity): async def websocket_remove_device(hass, connection, msg): """Delete device.""" device_id = msg["device_id"] - dev_registry = await get_dev_reg(hass) + dev_registry = await hass.helpers.device_registry.async_get_registry() device = dev_registry.async_get(device_id) if not device: diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 5bb5ccbd9d4..88c635ae3a8 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -25,6 +25,7 @@ from . import ( CONF_PAYLOAD, CONF_QOS, DOMAIN, + cleanup_device_registry, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash @@ -187,6 +188,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) remove_signal() + await cleanup_device_registry(hass, device.id) else: # Non-empty payload: Update trigger _LOGGER.info("Updating trigger: %s", discovery_hash) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index f8de0faf82f..a29891d0b36 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -192,6 +192,30 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, assert device.sw_version == "0.1-beta" +async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): + """Test device registry remove.""" + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + dev_registry = await hass.helpers.device_registry.async_get_registry() + ent_registry = await hass.helpers.entity_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + assert ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") + + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") + await hass.async_block_till_done() + + device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is None + assert not ent_registry.async_get_entity_id(domain, mqtt.DOMAIN, "veryunique") + + async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): """Test device registry update. diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 9a0df0bcd8d..ffc1755afda 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -19,6 +19,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -509,6 +510,21 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove( + hass, mqtt_mock, alarm_control_panel.DOMAIN, config + ) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 2cc917c527b..9a20b9a3282 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -20,6 +20,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -556,6 +557,20 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove( + hass, mqtt_mock, binary_sensor.DOMAIN, config + ) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index a6fb5f2cc66..481b43002a0 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -30,6 +30,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -893,6 +894,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "power_state_topic": "test-topic", + "power_command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, CLIMATE_DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 78f7dc72a24..d9b49a1fde6 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -28,6 +28,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -1819,6 +1820,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, cover.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index c7d1f636c02..ebdbabae83b 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -233,8 +233,8 @@ async def test_update_remove_triggers(hass, device_reg, entity_reg, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") await hass.async_block_till_done() - triggers = await async_get_device_automations(hass, "trigger", device_entry.id) - assert_lists_same(triggers, []) + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is None async def test_if_fires_on_mqtt_message(hass, device_reg, calls, mqtt_mock): @@ -833,8 +833,8 @@ async def test_entity_device_info_update(hass, mqtt_mock): assert device.name == "Milk" -async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): - """Test discovered device is cleaned up when removed from registry.""" +async def test_cleanup_trigger(hass, device_reg, entity_reg, mqtt_mock): + """Test trigger discovery topic is cleaned when device is removed from registry.""" config_entry = MockConfigEntry(domain=DOMAIN) config_entry.add_to_hass(hass) await async_start(hass, "homeassistant", {}, config_entry) @@ -863,10 +863,212 @@ async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): await hass.async_block_till_done() # Verify device registry entry is cleared - device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) assert device_entry is None # Verify retained discovery topic has been cleared mqtt_mock.async_publish.assert_called_once_with( "homeassistant/device_automation/bla/config", "", 0, True ) + + +async def test_cleanup_device(hass, device_reg, entity_reg, mqtt_mock): + """Test removal from device registry when trigger is removed.""" + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": {"identifiers": ["helloworld"]}, + } + + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + await hass.async_block_till_done() + + # Verify device registry entry is created + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert triggers[0]["type"] == "foo" + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is None + + +async def test_cleanup_device_several_triggers(hass, device_reg, entity_reg, mqtt_mock): + """Test removal from device registry when the last trigger is removed.""" + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config1 = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": {"identifiers": ["helloworld"]}, + } + + config2 = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo2", + "subtype": "bar", + "device": {"identifiers": ["helloworld"]}, + } + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data2) + await hass.async_block_till_done() + + # Verify device registry entry is created + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 2 + assert triggers[0]["type"] == "foo" + assert triggers[1]["type"] == "foo2" + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is not cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 1 + assert triggers[0]["type"] == "foo2" + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is None + + +async def test_cleanup_device_with_entity1(hass, device_reg, entity_reg, mqtt_mock): + """Test removal from device registry for device with entity. + + Trigger removed first, then entity. + """ + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config1 = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": {"identifiers": ["helloworld"]}, + } + + config2 = { + "name": "test_binary_sensor", + "state_topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla2/config", data2) + await hass.async_block_till_done() + + # Verify device registry entry is created + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is not cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 2 # 2 binary_sensor triggers + + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla2/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is None + + +async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mock): + """Test removal from device registry for device with entity. + + Entity removed first, then trigger. + """ + config_entry = MockConfigEntry(domain=DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config1 = { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": {"identifiers": ["helloworld"]}, + } + + config2 = { + "name": "test_binary_sensor", + "state_topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + + data1 = json.dumps(config1) + data2 = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data1) + await hass.async_block_till_done() + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla2/config", data2) + await hass.async_block_till_done() + + # Verify device registry entry is created + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 3 # 2 binary_sensor triggers + device trigger + + async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla2/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is not cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is not None + + triggers = await async_get_device_automations(hass, "trigger", device_entry.id) + assert len(triggers) == 1 # device trigger + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") + await hass.async_block_till_done() + + # Verify device registry entry is cleared + device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) + assert device_entry is None diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 512dddd4fc6..37c48fbcc93 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -13,6 +13,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -563,6 +564,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, fan.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index c3500e6ac6a..86c111bf0cd 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -30,6 +30,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -643,6 +644,18 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, vacuum.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index f2bde3d3b43..1296915039a 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -172,6 +172,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -1226,6 +1227,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, light.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 71ced8f1db2..860da1e1f30 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -108,6 +108,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -1080,6 +1081,20 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "schema": "json", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, light.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 9d4d3fcba25..f7e4e10bf04 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -44,6 +44,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -704,6 +705,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, light.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index d636eb1534d..f4b7431b0ae 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -13,6 +13,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -442,6 +443,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, lock.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0cf24894bcb..2666b3bbdb0 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -16,6 +16,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -559,6 +560,18 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, sensor.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 52c101d138c..6aa61fdc7ef 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -36,6 +36,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -475,6 +476,20 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "schema": "state", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, vacuum.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 983d91f08a2..b923e3431c1 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -17,6 +17,7 @@ from .common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_identifier, help_test_entity_id_update, @@ -407,6 +408,19 @@ async def test_entity_device_info_update(hass, mqtt_mock): ) +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + config = { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "command_topic": "test-command-topic", + "device": {"identifiers": ["helloworld"]}, + "unique_id": "veryunique", + } + await help_test_entity_device_info_remove(hass, mqtt_mock, switch.DOMAIN, config) + + async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { From 76b0302c7fc2eb60a58cd5978c376ec9201ef42f Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Wed, 11 Mar 2020 19:34:54 -0600 Subject: [PATCH 006/431] Broaden exception handling for IQVIA (#32708) --- homeassistant/components/iqvia/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index 3e62eb9b1ee..a33dabeadeb 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from pyiqvia import Client -from pyiqvia.errors import InvalidZipError, IQVIAError +from pyiqvia.errors import InvalidZipError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -172,7 +172,7 @@ class IQVIAData: results = await asyncio.gather(*tasks.values(), return_exceptions=True) for key, result in zip(tasks, results): - if isinstance(result, IQVIAError): + if isinstance(result, Exception): _LOGGER.error("Unable to get %s data: %s", key, result) self.data[key] = {} continue From 233568ac296d4aae16488d55e40a38e49b536a01 Mon Sep 17 00:00:00 2001 From: Barry Williams Date: Thu, 12 Mar 2020 08:54:25 +0000 Subject: [PATCH 007/431] If device has volume disabled, the volume will be `None`. However in these (#32702) instances whenever the volume was requested a division calculation was made resulting in a TypeError. The volume adjustment from `0-100` to `0-1` is now calculated during the `update()` method. --- homeassistant/components/openhome/media_player.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openhome/media_player.py b/homeassistant/components/openhome/media_player.py index 5d6ee47c3eb..967bce6007e 100644 --- a/homeassistant/components/openhome/media_player.py +++ b/homeassistant/components/openhome/media_player.py @@ -85,7 +85,7 @@ class OpenhomeDevice(MediaPlayerDevice): self._supported_features |= ( SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_MUTE | SUPPORT_VOLUME_SET ) - self._volume_level = self._device.VolumeLevel() + self._volume_level = self._device.VolumeLevel() / 100.0 self._volume_muted = self._device.IsMuted() for source in self._device.Sources(): @@ -222,7 +222,7 @@ class OpenhomeDevice(MediaPlayerDevice): @property def volume_level(self): """Volume level of the media player (0..1).""" - return self._volume_level / 100.0 + return self._volume_level @property def is_volume_muted(self): From f9a0b4b3cf9fbee7a75528ee4556aa07cc0817e9 Mon Sep 17 00:00:00 2001 From: escoand Date: Thu, 12 Mar 2020 10:29:11 +0100 Subject: [PATCH 008/431] Fix legacy Samsung TV (#32719) * Update bridge.py * Update test_init.py --- homeassistant/components/samsungtv/bridge.py | 5 +++-- tests/components/samsungtv/test_init.py | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 5203c61a978..31f102a62a4 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -130,10 +130,11 @@ class SamsungTVLegacyBridge(SamsungTVBridge): super().__init__(method, host, None) self.config = { CONF_NAME: VALUE_CONF_NAME, - CONF_ID: VALUE_CONF_ID, CONF_DESCRIPTION: VALUE_CONF_NAME, - CONF_METHOD: method, + CONF_ID: VALUE_CONF_ID, CONF_HOST: host, + CONF_METHOD: method, + CONF_PORT: None, CONF_TIMEOUT: 1, } diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index 064a870931f..232a04416d5 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -34,6 +34,7 @@ REMOTE_CALL = { "id": "ha.component.samsung", "method": "legacy", "host": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_HOST], + "port": None, "timeout": 1, } From ac30e5799c7553e0d81fb0dab716072e5b8e4366 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 12 Mar 2020 04:31:55 -0500 Subject: [PATCH 009/431] =?UTF-8?q?Resolve=20Home=20Assistant=20fails=20to?= =?UTF-8?q?=20start=20when=20Sense=20integration=20i=E2=80=A6=20(#32716)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Bump sense_energy 0.7.1 which also fixes throwing ConfigEntryNotReady --- homeassistant/components/sense/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sense/manifest.json b/homeassistant/components/sense/manifest.json index 61f09fb444b..c07e1e4f5c3 100644 --- a/homeassistant/components/sense/manifest.json +++ b/homeassistant/components/sense/manifest.json @@ -3,11 +3,11 @@ "name": "Sense", "documentation": "https://www.home-assistant.io/integrations/sense", "requirements": [ - "sense_energy==0.7.0" + "sense_energy==0.7.1" ], "dependencies": [], "codeowners": [ "@kbickar" ], "config_flow": true -} \ No newline at end of file +} diff --git a/requirements_all.txt b/requirements_all.txt index 85f292757e6..bfee6d76d32 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1837,7 +1837,7 @@ sendgrid==6.1.1 sense-hat==2.2.0 # homeassistant.components.sense -sense_energy==0.7.0 +sense_energy==0.7.1 # homeassistant.components.sentry sentry-sdk==0.13.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 23caf750147..4523aa52401 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -635,7 +635,7 @@ samsungctl[websocket]==0.7.1 samsungtvws[websocket]==1.4.0 # homeassistant.components.sense -sense_energy==0.7.0 +sense_energy==0.7.1 # homeassistant.components.sentry sentry-sdk==0.13.5 From 77ebda0c2069e2ad969f8edfc883737a5d6e7d8b Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 12 Mar 2020 06:01:05 -0400 Subject: [PATCH 010/431] =?UTF-8?q?Update=20Vizio=20`source`=20property=20?= =?UTF-8?q?to=20only=20return=20current=20app=20if=20i=E2=80=A6=20(#32713)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * only return current app for source if current app is set * check for None specifically * make sure current app isn't called for speaker --- homeassistant/components/vizio/media_player.py | 2 +- tests/components/vizio/test_media_player.py | 6 +++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 63918737411..69a430bb997 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -304,7 +304,7 @@ class VizioDevice(MediaPlayerDevice): @property def source(self) -> str: """Return current input of the device.""" - if self._current_input in INPUT_APPS: + if self._current_app is not None and self._current_input in INPUT_APPS: return self._current_app return self._current_input diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 19696af73a2..68366e8e98b 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -112,7 +112,9 @@ async def _test_setup( ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", return_value=vizio_power_state, - ): + ), patch( + "homeassistant.components.vizio.media_player.VizioAsync.get_current_app", + ) as service_call: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -125,6 +127,8 @@ async def _test_setup( if ha_power_state == STATE_ON: assert attr["source_list"] == INPUT_LIST assert attr["source"] == CURRENT_INPUT + if ha_device_class == DEVICE_CLASS_SPEAKER: + assert not service_call.called assert ( attr["volume_level"] == float(int(MAX_VOLUME[vizio_device_class] / 2)) From 221d5205e4614573eb7a9cf5919e78ed29e123da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Thu, 12 Mar 2020 12:52:20 +0200 Subject: [PATCH 011/431] Upgrade mypy to 0.770, tighten config a bit (#32715) * Upgrade mypy to 0.770, related cleanups https://mypy-lang.blogspot.com/2020/03/mypy-0770-released.html * Clean up config and make it a notch stricter, address findings --- homeassistant/helpers/config_entry_oauth2_flow.py | 10 ++++++---- homeassistant/helpers/config_validation.py | 2 +- homeassistant/helpers/logging.py | 4 ++-- homeassistant/helpers/temperature.py | 3 +-- homeassistant/helpers/template.py | 2 +- homeassistant/requirements.py | 2 +- homeassistant/scripts/benchmark/__init__.py | 6 ++++-- homeassistant/util/distance.py | 5 ++--- homeassistant/util/pressure.py | 3 +-- homeassistant/util/ruamel_yaml.py | 5 ++--- homeassistant/util/unit_system.py | 5 +---- homeassistant/util/volume.py | 5 ++--- requirements_test.txt | 2 +- setup.cfg | 12 ++++-------- 14 files changed, 29 insertions(+), 37 deletions(-) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 9baed41dd20..5214c8cbc3c 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -196,7 +196,9 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """Extra data that needs to be appended to the authorize url.""" return {} - async def async_step_pick_implementation(self, user_input: dict = None) -> dict: + async def async_step_pick_implementation( + self, user_input: Optional[dict] = None + ) -> dict: """Handle a flow start.""" assert self.hass implementations = await async_get_implementations(self.hass, self.DOMAIN) @@ -224,7 +226,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): ), ) - async def async_step_auth(self, user_input: dict = None) -> dict: + async def async_step_auth(self, user_input: Optional[dict] = None) -> dict: """Create an entry for auth.""" # Flow has been triggered by external data if user_input: @@ -241,7 +243,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): return self.async_external_step(step_id="auth", url=url) - async def async_step_creation(self, user_input: dict = None) -> dict: + async def async_step_creation(self, user_input: Optional[dict] = None) -> dict: """Create config entry from external data.""" token = await self.flow_impl.async_resolve_external_data(self.external_data) token["expires_at"] = time.time() + token["expires_in"] @@ -259,7 +261,7 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) - async def async_step_discovery(self, user_input: dict = None) -> dict: + async def async_step_discovery(self, user_input: Optional[dict] = None) -> dict: """Handle a flow initialized by discovery.""" await self.async_set_unique_id(self.DOMAIN) diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index db966d93412..7bb3223f3d7 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -338,7 +338,7 @@ def date(value: Any) -> date_sys: def time_period_str(value: str) -> timedelta: """Validate and transform time offset.""" - if isinstance(value, int): + if isinstance(value, int): # type: ignore raise vol.Invalid("Make sure you wrap time values in quotes") if not isinstance(value, str): raise vol.Invalid(TIME_PERIOD_ERROR.format(value)) diff --git a/homeassistant/helpers/logging.py b/homeassistant/helpers/logging.py index 2e3270879f0..49b9bfcffec 100644 --- a/homeassistant/helpers/logging.py +++ b/homeassistant/helpers/logging.py @@ -35,7 +35,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): """Log the message provided at the appropriate level.""" if self.isEnabledFor(level): msg, log_kwargs = self.process(msg, kwargs) - self.logger._log( # type: ignore # pylint: disable=protected-access + self.logger._log( # pylint: disable=protected-access level, KeywordMessage(msg, args, kwargs), (), **log_kwargs ) @@ -48,7 +48,7 @@ class KeywordStyleAdapter(logging.LoggerAdapter): { k: kwargs[k] for k in inspect.getfullargspec( - self.logger._log # type: ignore # pylint: disable=protected-access + self.logger._log # pylint: disable=protected-access ).args[1:] if k in kwargs }, diff --git a/homeassistant/helpers/temperature.py b/homeassistant/helpers/temperature.py index e0846d6f893..18ca8355159 100644 --- a/homeassistant/helpers/temperature.py +++ b/homeassistant/helpers/temperature.py @@ -22,8 +22,7 @@ def display_temp( if not isinstance(temperature, Number): raise TypeError(f"Temperature is not a number: {temperature}") - # type ignore: https://github.com/python/mypy/issues/7207 - if temperature_unit != ha_unit: # type: ignore + if temperature_unit != ha_unit: temperature = convert_temperature(temperature, temperature_unit, ha_unit) # Round in the units appropriate diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index e7f89b482e2..5cd15fefd99 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -192,7 +192,7 @@ class Template: raise TemplateError(err) def extract_entities( - self, variables: Dict[str, Any] = None + self, variables: Optional[Dict[str, Any]] = None ) -> Union[str, List[str]]: """Extract all entities for state_changed listener.""" return extract_entities(self.template, variables) diff --git a/homeassistant/requirements.py b/homeassistant/requirements.py index 7b2d3fe9bc3..317fffe84bf 100644 --- a/homeassistant/requirements.py +++ b/homeassistant/requirements.py @@ -32,7 +32,7 @@ class RequirementsNotFound(HomeAssistantError): async def async_get_integration_with_requirements( - hass: HomeAssistant, domain: str, done: Set[str] = None + hass: HomeAssistant, domain: str, done: Optional[Set[str]] = None ) -> Integration: """Get an integration with all requirements installed, including the dependencies. diff --git a/homeassistant/scripts/benchmark/__init__.py b/homeassistant/scripts/benchmark/__init__.py index 2c885dd1713..2bc821c8495 100644 --- a/homeassistant/scripts/benchmark/__init__.py +++ b/homeassistant/scripts/benchmark/__init__.py @@ -5,7 +5,7 @@ from contextlib import suppress from datetime import datetime import logging from timeit import default_timer as timer -from typing import Callable, Dict +from typing import Callable, Dict, TypeVar from homeassistant import core from homeassistant.components.websocket_api.const import JSON_DUMP @@ -15,6 +15,8 @@ from homeassistant.util import dt as dt_util # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs # mypy: no-warn-return-any +CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) # pylint: disable=invalid-name + BENCHMARKS: Dict[str, Callable] = {} @@ -44,7 +46,7 @@ def run(args): loop.close() -def benchmark(func): +def benchmark(func: CALLABLE_T) -> CALLABLE_T: """Decorate to mark a benchmark.""" BENCHMARKS[func.__name__] = func return func diff --git a/homeassistant/util/distance.py b/homeassistant/util/distance.py index 4fdc40bde2f..70fc9ad4eaa 100644 --- a/homeassistant/util/distance.py +++ b/homeassistant/util/distance.py @@ -27,11 +27,10 @@ def convert(value: float, unit_1: str, unit_2: str) -> float: if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") - # type ignore: https://github.com/python/mypy/issues/7207 - if unit_1 == unit_2 or unit_1 not in VALID_UNITS: # type: ignore + if unit_1 == unit_2 or unit_1 not in VALID_UNITS: return value - meters = value + meters: float = value if unit_1 == LENGTH_MILES: meters = __miles_to_meters(value) diff --git a/homeassistant/util/pressure.py b/homeassistant/util/pressure.py index df791fd0235..046b65122a9 100644 --- a/homeassistant/util/pressure.py +++ b/homeassistant/util/pressure.py @@ -36,8 +36,7 @@ def convert(value: float, unit_1: str, unit_2: str) -> float: if not isinstance(value, Number): raise TypeError(f"{value} is not of numeric type") - # type ignore: https://github.com/python/mypy/issues/7207 - if unit_1 == unit_2 or unit_1 not in VALID_UNITS: # type: ignore + if unit_1 == unit_2 or unit_1 not in VALID_UNITS: return value pascals = value / UNIT_CONVERSION[unit_1] diff --git a/homeassistant/util/ruamel_yaml.py b/homeassistant/util/ruamel_yaml.py index 68a357d91f6..71d3ab2cc43 100644 --- a/homeassistant/util/ruamel_yaml.py +++ b/homeassistant/util/ruamel_yaml.py @@ -6,7 +6,7 @@ from os import O_CREAT, O_TRUNC, O_WRONLY, stat_result from typing import Dict, List, Optional, Union import ruamel.yaml -from ruamel.yaml import YAML +from ruamel.yaml import YAML # type: ignore from ruamel.yaml.compat import StringIO from ruamel.yaml.constructor import SafeConstructor from ruamel.yaml.error import YAMLError @@ -89,8 +89,7 @@ def load_yaml(fname: str, round_trip: bool = False) -> JSON_TYPE: """Load a YAML file.""" if round_trip: yaml = YAML(typ="rt") - # type ignore: https://bitbucket.org/ruamel/yaml/pull-requests/42 - yaml.preserve_quotes = True # type: ignore + yaml.preserve_quotes = True else: if ExtSafeConstructor.name is None: ExtSafeConstructor.name = fname diff --git a/homeassistant/util/unit_system.py b/homeassistant/util/unit_system.py index 5540936c8b4..8b276da432d 100644 --- a/homeassistant/util/unit_system.py +++ b/homeassistant/util/unit_system.py @@ -109,10 +109,7 @@ class UnitSystem: if not isinstance(temperature, Number): raise TypeError(f"{temperature!s} is not a numeric value.") - # type ignore: https://github.com/python/mypy/issues/7207 - return temperature_util.convert( # type: ignore - temperature, from_unit, self.temperature_unit - ) + return temperature_util.convert(temperature, from_unit, self.temperature_unit) def length(self, length: Optional[float], from_unit: str) -> float: """Convert the given length to this unit system.""" diff --git a/homeassistant/util/volume.py b/homeassistant/util/volume.py index 2e033beb35c..30185e4346c 100644 --- a/homeassistant/util/volume.py +++ b/homeassistant/util/volume.py @@ -37,11 +37,10 @@ def convert(volume: float, from_unit: str, to_unit: str) -> float: if not isinstance(volume, Number): raise TypeError(f"{volume} is not of numeric type") - # type ignore: https://github.com/python/mypy/issues/7207 - if from_unit == to_unit: # type: ignore + if from_unit == to_unit: return volume - result = volume + result: float = volume if from_unit == VOLUME_LITERS and to_unit == VOLUME_GALLONS: result = __liter_to_gallon(volume) elif from_unit == VOLUME_GALLONS and to_unit == VOLUME_LITERS: diff --git a/requirements_test.txt b/requirements_test.txt index 6fc7e10a78d..dbc756ff7c8 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -6,7 +6,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 -mypy==0.761 +mypy==0.770 pre-commit==2.1.1 pylint==2.4.4 astroid==2.3.3 diff --git a/setup.cfg b/setup.cfg index f9e9852812c..7df396df528 100644 --- a/setup.cfg +++ b/setup.cfg @@ -65,13 +65,9 @@ warn_redundant_casts = true warn_unused_configs = true [mypy-homeassistant.bootstrap,homeassistant.components,homeassistant.config_entries,homeassistant.config,homeassistant.const,homeassistant.core,homeassistant.data_entry_flow,homeassistant.exceptions,homeassistant.loader,homeassistant.__main__,homeassistant.requirements,homeassistant.setup,homeassistant.util,homeassistant.auth.*,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zone.*,homeassistant.helpers.*,homeassistant.scripts.*,homeassistant.util.*] +strict = true ignore_errors = false -check_untyped_defs = true -disallow_incomplete_defs = true -disallow_untyped_calls = true -disallow_untyped_defs = true -no_implicit_optional = true -strict_equality = true -warn_return_any = true warn_unreachable = true -warn_unused_ignores = true +# TODO: turn these off, address issues +allow_any_generics = true +implicit_reexport = true From b8fab33e6934b0e42c46afb8e35472e99dfd0aa9 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Mar 2020 11:55:18 +0100 Subject: [PATCH 012/431] Remove deprecated hide_if_away from device trackers (#32705) --- .../components/device_tracker/__init__.py | 12 +------ .../components/device_tracker/const.py | 3 -- .../components/device_tracker/legacy.py | 14 +------- .../device_sun_light_trigger/test_init.py | 2 -- tests/components/device_tracker/test_init.py | 34 ------------------- .../unifi_direct/test_device_tracker.py | 3 +- 6 files changed, 3 insertions(+), 65 deletions(-) diff --git a/homeassistant/components/device_tracker/__init__.py b/homeassistant/components/device_tracker/__init__.py index c66bb621ad4..6d8e2307145 100644 --- a/homeassistant/components/device_tracker/__init__.py +++ b/homeassistant/components/device_tracker/__init__.py @@ -25,12 +25,10 @@ from .const import ( ATTR_LOCATION_NAME, ATTR_MAC, ATTR_SOURCE_TYPE, - CONF_AWAY_HIDE, CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, CONF_SCAN_INTERVAL, CONF_TRACK_NEW, - DEFAULT_AWAY_HIDE, DEFAULT_CONSIDER_HOME, DEFAULT_TRACK_NEW, DOMAIN, @@ -53,15 +51,7 @@ SOURCE_TYPES = ( NEW_DEVICE_DEFAULTS_SCHEMA = vol.Any( None, - vol.All( - cv.deprecated(CONF_AWAY_HIDE, invalidation_version="0.107.0"), - vol.Schema( - { - vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean, - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, - } - ), - ), + vol.Schema({vol.Optional(CONF_TRACK_NEW, default=DEFAULT_TRACK_NEW): cv.boolean}), ) PLATFORM_SCHEMA = cv.PLATFORM_SCHEMA.extend( { diff --git a/homeassistant/components/device_tracker/const.py b/homeassistant/components/device_tracker/const.py index 06313deccb6..c9ce9f2024a 100644 --- a/homeassistant/components/device_tracker/const.py +++ b/homeassistant/components/device_tracker/const.py @@ -20,9 +20,6 @@ SCAN_INTERVAL = timedelta(seconds=12) CONF_TRACK_NEW = "track_new_devices" DEFAULT_TRACK_NEW = True -CONF_AWAY_HIDE = "hide_if_away" -DEFAULT_AWAY_HIDE = False - CONF_CONSIDER_HOME = "consider_home" DEFAULT_CONSIDER_HOME = timedelta(seconds=180) diff --git a/homeassistant/components/device_tracker/legacy.py b/homeassistant/components/device_tracker/legacy.py index 68908f8c79f..515b7cbc614 100644 --- a/homeassistant/components/device_tracker/legacy.py +++ b/homeassistant/components/device_tracker/legacy.py @@ -37,11 +37,9 @@ from .const import ( ATTR_HOST_NAME, ATTR_MAC, ATTR_SOURCE_TYPE, - CONF_AWAY_HIDE, CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, CONF_TRACK_NEW, - DEFAULT_AWAY_HIDE, DEFAULT_CONSIDER_HOME, DEFAULT_TRACK_NEW, DOMAIN, @@ -198,7 +196,6 @@ class DeviceTracker: mac, picture=picture, icon=icon, - hide_if_away=self.defaults.get(CONF_AWAY_HIDE, DEFAULT_AWAY_HIDE), ) self.devices[dev_id] = device if mac is not None: @@ -303,7 +300,6 @@ class Device(RestoreEntity): picture: str = None, gravatar: str = None, icon: str = None, - hide_if_away: bool = False, ) -> None: """Initialize a device.""" self.hass = hass @@ -331,8 +327,6 @@ class Device(RestoreEntity): self.icon = icon - self.away_hide = hide_if_away - self.source_type = None self._attributes = {} @@ -372,11 +366,6 @@ class Device(RestoreEntity): """Return device state attributes.""" return self._attributes - @property - def hidden(self): - """If device should be hidden.""" - return self.away_hide and self.state != STATE_HOME - async def async_seen( self, host_name: str = None, @@ -524,7 +513,6 @@ async def async_load_config( vol.Optional(CONF_MAC, default=None): vol.Any( None, vol.All(cv.string, vol.Upper) ), - vol.Optional(CONF_AWAY_HIDE, default=DEFAULT_AWAY_HIDE): cv.boolean, vol.Optional("gravatar", default=None): vol.Any(None, cv.string), vol.Optional("picture", default=None): vol.Any(None, cv.string), vol.Optional(CONF_CONSIDER_HOME, default=consider_home): vol.All( @@ -544,6 +532,7 @@ async def async_load_config( for dev_id, device in devices.items(): # Deprecated option. We just ignore it to avoid breaking change device.pop("vendor", None) + device.pop("hide_if_away", None) try: device = dev_schema(device) device["dev_id"] = cv.slugify(dev_id) @@ -564,7 +553,6 @@ def update_config(path: str, dev_id: str, device: Device): ATTR_ICON: device.icon, "picture": device.config_picture, "track": device.track, - CONF_AWAY_HIDE: device.away_hide, } } out.write("\n") diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index bc4d44e1b42..9cc794380f9 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -34,7 +34,6 @@ def scanner(hass): "homeassistant.components.device_tracker.legacy.load_yaml_config_file", return_value={ "device_1": { - "hide_if_away": False, "mac": "DEV1", "name": "Unnamed Device", "picture": "http://example.com/dev1.jpg", @@ -42,7 +41,6 @@ def scanner(hass): "vendor": None, }, "device_2": { - "hide_if_away": False, "mac": "DEV2", "name": "Unnamed Device", "picture": "http://example.com/dev2.jpg", diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 3ad9e741aae..1a21ad4a7a4 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -15,7 +15,6 @@ from homeassistant.const import ( ATTR_ENTITY_PICTURE, ATTR_FRIENDLY_NAME, ATTR_GPS_ACCURACY, - ATTR_HIDDEN, ATTR_ICON, ATTR_LATITUDE, ATTR_LONGITUDE, @@ -107,7 +106,6 @@ async def test_reading_yaml_config(hass, yaml_devices): "AB:CD:EF:GH:IJ", "Test name", picture="http://test.picture", - hide_if_away=True, icon="mdi:kettle", ) await hass.async_add_executor_job( @@ -121,7 +119,6 @@ async def test_reading_yaml_config(hass, yaml_devices): assert device.track == config.track assert device.mac == config.mac assert device.config_picture == config.config_picture - assert device.away_hide == config.away_hide assert device.consider_home == config.consider_home assert device.icon == config.icon @@ -284,7 +281,6 @@ async def test_entity_attributes(hass, mock_device_tracker_conf): None, friendly_name, picture, - hide_if_away=True, icon=icon, ) devices.append(device) @@ -299,25 +295,6 @@ async def test_entity_attributes(hass, mock_device_tracker_conf): assert picture == attrs.get(ATTR_ENTITY_PICTURE) -async def test_device_hidden(hass, mock_device_tracker_conf): - """Test hidden devices.""" - devices = mock_device_tracker_conf - dev_id = "test_entity" - entity_id = f"{const.DOMAIN}.{dev_id}" - device = legacy.Device( - hass, timedelta(seconds=180), True, dev_id, None, hide_if_away=True - ) - devices.append(device) - - scanner = getattr(hass.components, "test.device_tracker").SCANNER - scanner.reset() - - with assert_setup_component(1, device_tracker.DOMAIN): - assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) - - assert hass.states.get(entity_id).attributes.get(ATTR_HIDDEN) - - @patch("homeassistant.components.device_tracker.legacy." "DeviceTracker.async_see") async def test_see_service(mock_see, hass): """Test the see service with a unicode dev_id and NO MAC.""" @@ -609,17 +586,6 @@ async def test_picture_and_icon_on_see_discovery(mock_device_tracker_conf, hass) assert mock_device_tracker_conf[0].entity_picture == "pic_url" -async def test_default_hide_if_away_is_used(mock_device_tracker_conf, hass): - """Test that default track_new is used.""" - tracker = legacy.DeviceTracker( - hass, timedelta(seconds=60), False, {device_tracker.CONF_AWAY_HIDE: True}, [] - ) - await tracker.async_see(dev_id=12) - await hass.async_block_till_done() - assert len(mock_device_tracker_conf) == 1 - assert mock_device_tracker_conf[0].away_hide - - async def test_backward_compatibility_for_track_new(mock_device_tracker_conf, hass): """Test backward compatibility for track new.""" tracker = legacy.DeviceTracker( diff --git a/tests/components/unifi_direct/test_device_tracker.py b/tests/components/unifi_direct/test_device_tracker.py index 84602f438f4..297bf917dbf 100644 --- a/tests/components/unifi_direct/test_device_tracker.py +++ b/tests/components/unifi_direct/test_device_tracker.py @@ -7,7 +7,6 @@ import pytest import voluptuous as vol from homeassistant.components.device_tracker import ( - CONF_AWAY_HIDE, CONF_CONSIDER_HOME, CONF_NEW_DEVICE_DEFAULTS, CONF_TRACK_NEW, @@ -49,7 +48,7 @@ async def test_get_scanner(unifi_mock, hass): CONF_PASSWORD: "fake_pass", CONF_TRACK_NEW: True, CONF_CONSIDER_HOME: timedelta(seconds=180), - CONF_NEW_DEVICE_DEFAULTS: {CONF_TRACK_NEW: True, CONF_AWAY_HIDE: False}, + CONF_NEW_DEVICE_DEFAULTS: {CONF_TRACK_NEW: True}, } } From 40fc72aac289ff03ad07a314b7302623dfec23cd Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 12 Mar 2020 11:56:07 +0100 Subject: [PATCH 013/431] Upgrade psutil to 5.7.0 (#32720) --- homeassistant/components/systemmonitor/__init__.py | 2 +- homeassistant/components/systemmonitor/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/systemmonitor/__init__.py b/homeassistant/components/systemmonitor/__init__.py index 27dc9d367c2..5ab8ac9f930 100644 --- a/homeassistant/components/systemmonitor/__init__.py +++ b/homeassistant/components/systemmonitor/__init__.py @@ -1 +1 @@ -"""The systemmonitor component.""" +"""The systemmonitor integration.""" diff --git a/homeassistant/components/systemmonitor/manifest.json b/homeassistant/components/systemmonitor/manifest.json index 81712edd404..de8228f09a9 100644 --- a/homeassistant/components/systemmonitor/manifest.json +++ b/homeassistant/components/systemmonitor/manifest.json @@ -2,7 +2,7 @@ "domain": "systemmonitor", "name": "System Monitor", "documentation": "https://www.home-assistant.io/integrations/systemmonitor", - "requirements": ["psutil==5.6.7"], + "requirements": ["psutil==5.7.0"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index bfee6d76d32..980cea8f7d2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1064,7 +1064,7 @@ protobuf==3.6.1 proxmoxer==1.0.4 # homeassistant.components.systemmonitor -psutil==5.6.7 +psutil==5.7.0 # homeassistant.components.ptvsd ptvsd==4.2.8 From 374a8157e7e3037675223e5e35e070aa80998985 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 12 Mar 2020 11:56:50 +0100 Subject: [PATCH 014/431] Remove manual configuration support (#32699) --- homeassistant/components/unifi/__init__.py | 49 ++---------------- homeassistant/components/unifi/const.py | 5 -- homeassistant/components/unifi/controller.py | 51 ++----------------- tests/components/unifi/test_controller.py | 26 ---------- tests/components/unifi/test_device_tracker.py | 3 +- tests/components/unifi/test_init.py | 27 ---------- 6 files changed, 9 insertions(+), 152 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index a21ae4ed508..27a11760461 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -1,69 +1,26 @@ """Support for devices connected to UniFi POE.""" import voluptuous as vol -from homeassistant.const import CONF_HOST, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from .config_flow import get_controller_id_from_config_entry -from .const import ( - ATTR_MANUFACTURER, - CONF_BLOCK_CLIENT, - CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, - CONF_SITE_ID, - CONF_SSID_FILTER, - DOMAIN, - UNIFI_CONFIG, - UNIFI_WIRELESS_CLIENTS, -) +from .const import ATTR_MANUFACTURER, DOMAIN, UNIFI_WIRELESS_CLIENTS from .controller import UniFiController SAVE_DELAY = 10 STORAGE_KEY = "unifi_data" STORAGE_VERSION = 1 -CONF_CONTROLLERS = "controllers" - -CONTROLLER_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): cv.string, - vol.Required(CONF_SITE_ID): cv.string, - vol.Optional(CONF_BLOCK_CLIENT, default=[]): vol.All( - cv.ensure_list, [cv.string] - ), - vol.Optional(CONF_DONT_TRACK_CLIENTS): cv.boolean, - vol.Optional(CONF_DONT_TRACK_DEVICES): cv.boolean, - vol.Optional(CONF_DONT_TRACK_WIRED_CLIENTS): cv.boolean, - vol.Optional(CONF_DETECTION_TIME): cv.positive_int, - vol.Optional(CONF_SSID_FILTER): vol.All(cv.ensure_list, [cv.string]), - } -) - CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_CONTROLLERS): vol.All( - cv.ensure_list, [CONTROLLER_SCHEMA] - ) - } - ) - }, - extra=vol.ALLOW_EXTRA, + cv.deprecated(DOMAIN, invalidation_version="0.109"), {DOMAIN: cv.match_all} ) async def async_setup(hass, config): """Component doesn't support configuration through configuration.yaml.""" - hass.data[UNIFI_CONFIG] = [] - - if DOMAIN in config: - hass.data[UNIFI_CONFIG] = config[DOMAIN][CONF_CONTROLLERS] - hass.data[UNIFI_WIRELESS_CLIENTS] = wireless_clients = UnifiWirelessClients(hass) await wireless_clients.async_load() diff --git a/homeassistant/components/unifi/const.py b/homeassistant/components/unifi/const.py index 341364063f2..fd94601db50 100644 --- a/homeassistant/components/unifi/const.py +++ b/homeassistant/components/unifi/const.py @@ -9,7 +9,6 @@ CONTROLLER_ID = "{host}-{site}" CONF_CONTROLLER = "controller" CONF_SITE_ID = "site" -UNIFI_CONFIG = "unifi_config" UNIFI_WIRELESS_CLIENTS = "unifi_wireless_clients" CONF_ALLOW_BANDWIDTH_SENSORS = "allow_bandwidth_sensors" @@ -20,10 +19,6 @@ CONF_TRACK_DEVICES = "track_devices" CONF_TRACK_WIRED_CLIENTS = "track_wired_clients" CONF_SSID_FILTER = "ssid_filter" -CONF_DONT_TRACK_CLIENTS = "dont_track_clients" -CONF_DONT_TRACK_DEVICES = "dont_track_devices" -CONF_DONT_TRACK_WIRED_CLIENTS = "dont_track_wired_clients" - DEFAULT_ALLOW_BANDWIDTH_SENSORS = False DEFAULT_TRACK_CLIENTS = True DEFAULT_TRACK_DEVICES = True diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index a6981aeddee..50b758f01af 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -10,6 +10,9 @@ from aiounifi.events import WIRELESS_CLIENT_CONNECTED, WIRELESS_GUEST_CONNECTED from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout +from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.const import CONF_HOST from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady @@ -21,9 +24,6 @@ from .const import ( CONF_BLOCK_CLIENT, CONF_CONTROLLER, CONF_DETECTION_TIME, - CONF_DONT_TRACK_CLIENTS, - CONF_DONT_TRACK_DEVICES, - CONF_DONT_TRACK_WIRED_CLIENTS, CONF_SITE_ID, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, @@ -37,13 +37,12 @@ from .const import ( DEFAULT_TRACK_WIRED_CLIENTS, DOMAIN, LOGGER, - UNIFI_CONFIG, UNIFI_WIRELESS_CLIENTS, ) from .errors import AuthenticationRequired, CannotConnect RETRY_TIMER = 15 -SUPPORTED_PLATFORMS = ["device_tracker", "sensor", "switch"] +SUPPORTED_PLATFORMS = [DT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] class UniFiController: @@ -225,8 +224,6 @@ class UniFiController: self.wireless_clients = wireless_clients.get_data(self.config_entry) self.update_wireless_clients() - self.import_configuration() - for platform in SUPPORTED_PLATFORMS: self.hass.async_create_task( self.hass.config_entries.async_forward_entry_setup( @@ -251,46 +248,6 @@ class UniFiController: async_dispatcher_send(hass, controller.signal_options_update) - def import_configuration(self): - """Import configuration to config entry options.""" - import_config = {} - - for config in self.hass.data[UNIFI_CONFIG]: - if ( - self.host == config[CONF_HOST] - and self.site_name == config[CONF_SITE_ID] - ): - import_config = config - break - - old_options = dict(self.config_entry.options) - new_options = {} - - for config, option in ( - (CONF_BLOCK_CLIENT, CONF_BLOCK_CLIENT), - (CONF_DONT_TRACK_CLIENTS, CONF_TRACK_CLIENTS), - (CONF_DONT_TRACK_WIRED_CLIENTS, CONF_TRACK_WIRED_CLIENTS), - (CONF_DONT_TRACK_DEVICES, CONF_TRACK_DEVICES), - (CONF_DETECTION_TIME, CONF_DETECTION_TIME), - (CONF_SSID_FILTER, CONF_SSID_FILTER), - ): - if config in import_config: - if config == option and import_config[ - config - ] != self.config_entry.options.get(option): - new_options[option] = import_config[config] - elif config != option and ( - option not in self.config_entry.options - or import_config[config] == self.config_entry.options.get(option) - ): - new_options[option] = not import_config[config] - - if new_options: - options = {**old_options, **new_options} - self.hass.config_entries.async_update_entry( - self.config_entry, options=options - ) - @callback def reconnect(self) -> None: """Prepare to reconnect UniFi session.""" diff --git a/tests/components/unifi/test_controller.py b/tests/components/unifi/test_controller.py index 8bf2225d1f1..d3ff905e7b3 100644 --- a/tests/components/unifi/test_controller.py +++ b/tests/components/unifi/test_controller.py @@ -189,32 +189,6 @@ async def test_controller_mac(hass): assert controller.mac == "10:00:00:00:00:01" -async def test_controller_import_config(hass): - """Test that import configuration.yaml instructions work.""" - controllers = [ - { - CONF_HOST: "1.2.3.4", - CONF_SITE_ID: "Site name", - unifi.CONF_BLOCK_CLIENT: ["random mac"], - unifi.CONF_DONT_TRACK_CLIENTS: True, - unifi.CONF_DONT_TRACK_DEVICES: True, - unifi.CONF_DONT_TRACK_WIRED_CLIENTS: True, - unifi.CONF_DETECTION_TIME: 150, - unifi.CONF_SSID_FILTER: ["SSID"], - } - ] - - controller = await setup_unifi_integration(hass, controllers=controllers) - - assert controller.option_allow_bandwidth_sensors is False - assert controller.option_block_clients == ["random mac"] - assert controller.option_track_clients is False - assert controller.option_track_devices is False - assert controller.option_track_wired_clients is False - assert controller.option_detection_time == timedelta(seconds=150) - assert controller.option_ssid_filter == ["SSID"] - - async def test_controller_not_accessible(hass): """Retry to login gets scheduled when connection fails.""" with patch.object( diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index cbef7c31922..1d314c1fe86 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -10,6 +10,7 @@ from homeassistant import config_entries from homeassistant.components import unifi import homeassistant.components.device_tracker as device_tracker from homeassistant.components.unifi.const import ( + CONF_BLOCK_CLIENT, CONF_SSID_FILTER, CONF_TRACK_CLIENTS, CONF_TRACK_DEVICES, @@ -456,7 +457,7 @@ async def test_restoring_client(hass): await setup_unifi_integration( hass, - options={unifi.CONF_BLOCK_CLIENT: True}, + options={CONF_BLOCK_CLIENT: True}, clients_response=[CLIENT_2], clients_all_response=[CLIENT_1], ) diff --git a/tests/components/unifi/test_init.py b/tests/components/unifi/test_init.py index 12f9c1bfd17..0ccc89cdb89 100644 --- a/tests/components/unifi/test_init.py +++ b/tests/components/unifi/test_init.py @@ -13,33 +13,6 @@ async def test_setup_with_no_config(hass): """Test that we do not discover anything or try to set up a bridge.""" assert await async_setup_component(hass, unifi.DOMAIN, {}) is True assert unifi.DOMAIN not in hass.data - assert hass.data[unifi.UNIFI_CONFIG] == [] - - -async def test_setup_with_config(hass): - """Test that we do not discover anything or try to set up a bridge.""" - config = { - unifi.DOMAIN: { - unifi.CONF_CONTROLLERS: { - unifi.CONF_HOST: "1.2.3.4", - unifi.CONF_SITE_ID: "My site", - unifi.CONF_BLOCK_CLIENT: ["12:34:56:78:90:AB"], - unifi.CONF_DETECTION_TIME: 3, - unifi.CONF_SSID_FILTER: ["ssid"], - } - } - } - assert await async_setup_component(hass, unifi.DOMAIN, config) is True - assert unifi.DOMAIN not in hass.data - assert hass.data[unifi.UNIFI_CONFIG] == [ - { - unifi.CONF_HOST: "1.2.3.4", - unifi.CONF_SITE_ID: "My site", - unifi.CONF_BLOCK_CLIENT: ["12:34:56:78:90:AB"], - unifi.CONF_DETECTION_TIME: 3, - unifi.CONF_SSID_FILTER: ["ssid"], - } - ] async def test_successful_config_entry(hass): From 29533d8d4d0912d52eae20627828ef4607f65717 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 12 Mar 2020 12:06:01 +0100 Subject: [PATCH 015/431] Upgrade sendgrid to 6.1.3 (#32721) --- homeassistant/components/sendgrid/__init__.py | 2 +- homeassistant/components/sendgrid/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/sendgrid/__init__.py b/homeassistant/components/sendgrid/__init__.py index 91fff97c150..164dead5b23 100644 --- a/homeassistant/components/sendgrid/__init__.py +++ b/homeassistant/components/sendgrid/__init__.py @@ -1 +1 @@ -"""The sendgrid component.""" +"""The sendgrid integration.""" diff --git a/homeassistant/components/sendgrid/manifest.json b/homeassistant/components/sendgrid/manifest.json index 900fe9252b4..63a511809d8 100644 --- a/homeassistant/components/sendgrid/manifest.json +++ b/homeassistant/components/sendgrid/manifest.json @@ -2,7 +2,7 @@ "domain": "sendgrid", "name": "SendGrid", "documentation": "https://www.home-assistant.io/integrations/sendgrid", - "requirements": ["sendgrid==6.1.1"], + "requirements": ["sendgrid==6.1.3"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 980cea8f7d2..5012cb07d82 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1831,7 +1831,7 @@ schiene==0.23 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.1.1 +sendgrid==6.1.3 # homeassistant.components.sensehat sense-hat==2.2.0 From af7c01f95750f96dc540cf06629d6f3adeca7b20 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Mar 2020 12:40:31 +0100 Subject: [PATCH 016/431] Bumped version to 0.108.0dev0 (#32697) --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 66db936669b..74095a2583b 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,6 +1,6 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 -MINOR_VERSION = 107 +MINOR_VERSION = 108 PATCH_VERSION = "0.dev0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" From 6e97975ff875c859715f8bedce96f9139518e4fd Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Thu, 12 Mar 2020 10:04:34 -0400 Subject: [PATCH 017/431] Set self._current_app to None when vizio device is off (#32725) --- homeassistant/components/vizio/media_player.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 69a430bb997..d013f41403a 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -190,6 +190,7 @@ class VizioDevice(MediaPlayerDevice): self._is_muted = None self._current_input = None self._available_inputs = None + self._current_app = None self._available_apps = None return From a3dd9979d29e98144583875b5a918b7a19bc8f49 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 12 Mar 2020 22:41:04 +0100 Subject: [PATCH 018/431] Updated frontend to 20200312.0 (#32741) --- 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 ad34c0c112c..c3cf353dba1 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==20200311.1" + "home-assistant-frontend==20200312.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 39c1d76b999..56ab609c5af 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200311.1 +home-assistant-frontend==20200312.0 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 5012cb07d82..fa833a615fb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -696,7 +696,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200311.1 +home-assistant-frontend==20200312.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4523aa52401..8be49910948 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200311.1 +home-assistant-frontend==20200312.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From c968e455a951b0a070b430c9d69f25849c9a0468 Mon Sep 17 00:00:00 2001 From: Emilv2 Date: Thu, 12 Mar 2020 22:43:52 +0100 Subject: [PATCH 019/431] Fix delijn sensor stuck on last passage (#32710) * Fix delijn sensor stuck on last passage If the returned passage is an empty list that means there are no next passages, so the sensor should be updated accordingly * Mark the sensor as unavailable instead of emptying data * use available property * add available property --- homeassistant/components/delijn/sensor.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/homeassistant/components/delijn/sensor.py b/homeassistant/components/delijn/sensor.py index 2cd238d0787..538e071e194 100644 --- a/homeassistant/components/delijn/sensor.py +++ b/homeassistant/components/delijn/sensor.py @@ -66,6 +66,7 @@ class DeLijnPublicTransportSensor(Entity): self._attributes = {ATTR_ATTRIBUTION: ATTRIBUTION} self._name = name self._state = None + self._available = False async def async_update(self): """Get the latest data from the De Lijn API.""" @@ -88,8 +89,15 @@ class DeLijnPublicTransportSensor(Entity): self._attributes["due_at_schedule"] = first["due_at_schedule"] self._attributes["due_at_realtime"] = first["due_at_realtime"] self._attributes["next_passages"] = self.line.passages + self._available = True except (KeyError, IndexError) as error: _LOGGER.debug("Error getting data from De Lijn: %s", error) + self._available = False + + @property + def available(self): + """Return True if entity is available.""" + return self._available @property def device_class(self): From 00d5e5cfb27063474232ede0da2c1bd02db71e0a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Thu, 12 Mar 2020 22:44:14 +0100 Subject: [PATCH 020/431] Upgrade pre-commit to 2.2.0 (#32737) --- requirements_test.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements_test.txt b/requirements_test.txt index dbc756ff7c8..8b4b5d0edcf 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -7,7 +7,7 @@ asynctest==0.13.0 codecov==2.0.15 mock-open==1.3.1 mypy==0.770 -pre-commit==2.1.1 +pre-commit==2.2.0 pylint==2.4.4 astroid==2.3.3 pylint-strict-informational==0.1 From 1fe26c77e0ea39f543d39f9bae196c1733d46de2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 12 Mar 2020 14:47:57 -0700 Subject: [PATCH 021/431] Sonos idle (#32712) * Sonos idle * F-string * Add to properties * Fixes --- .../components/sonos/media_player.py | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 0ab78195cb2..8828c27e9c7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -93,6 +93,8 @@ ATTR_NIGHT_SOUND = "night_sound" ATTR_SPEECH_ENHANCE = "speech_enhance" ATTR_QUEUE_POSITION = "queue_position" +UNAVAILABLE_VALUES = {"", "NOT_IMPLEMENTED", None} + class SonosData: """Storage class for platform global data.""" @@ -330,7 +332,7 @@ def soco_coordinator(funct): def _timespan_secs(timespan): """Parse a time-span into number of seconds.""" - if timespan in ("", "NOT_IMPLEMENTED", None): + if timespan in UNAVAILABLE_VALUES: return None return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) @@ -427,7 +429,11 @@ class SonosEntity(MediaPlayerDevice): @soco_coordinator def state(self): """Return the state of the entity.""" - if self._status in ("PAUSED_PLAYBACK", "STOPPED"): + if self._status in ("PAUSED_PLAYBACK", "STOPPED",): + # Sonos can consider itself "paused" but without having media loaded + # (happens if playing Spotify and via Spotify app you pick another device to play on) + if self._media_title is None: + return STATE_IDLE return STATE_PAUSED if self._status in ("PLAYING", "TRANSITIONING"): return STATE_PLAYING @@ -511,16 +517,14 @@ class SonosEntity(MediaPlayerDevice): def _radio_artwork(self, url): """Return the private URL with artwork for a radio stream.""" - if url not in ("", "NOT_IMPLEMENTED", None): - if url.find("tts_proxy") > 0: - # If the content is a tts don't try to fetch an image from it. - return None - url = "http://{host}:{port}/getaa?s=1&u={uri}".format( - host=self.soco.ip_address, - port=1400, - uri=urllib.parse.quote(url, safe=""), - ) - return url + if url in UNAVAILABLE_VALUES: + return None + + if url.find("tts_proxy") > 0: + # If the content is a tts don't try to fetch an image from it. + return None + + return f"http://{self.soco.ip_address}:1400/getaa?s=1&u={urllib.parse.quote(url, safe='')}" def _attach_player(self): """Get basic information and add event subscriptions.""" @@ -606,9 +610,9 @@ class SonosEntity(MediaPlayerDevice): self._media_image_url = None - self._media_artist = source + self._media_artist = None self._media_album_name = None - self._media_title = None + self._media_title = source self._source_name = source @@ -640,7 +644,7 @@ class SonosEntity(MediaPlayerDevice): # For radio streams we set the radio station name as the title. current_uri_metadata = media_info["CurrentURIMetaData"] - if current_uri_metadata not in ("", "NOT_IMPLEMENTED", None): + if current_uri_metadata not in UNAVAILABLE_VALUES: # currently soco does not have an API for this current_uri_metadata = pysonos.xml.XML.fromstring( pysonos.utils.really_utf8(current_uri_metadata) @@ -650,7 +654,7 @@ class SonosEntity(MediaPlayerDevice): ".//{http://purl.org/dc/elements/1.1/}title" ) - if md_title not in ("", "NOT_IMPLEMENTED", None): + if md_title not in UNAVAILABLE_VALUES: self._media_title = md_title if self._media_artist and self._media_title: @@ -867,25 +871,25 @@ class SonosEntity(MediaPlayerDevice): @soco_coordinator def media_artist(self): """Artist of current playing media, music track only.""" - return self._media_artist + return self._media_artist or None @property @soco_coordinator def media_album_name(self): """Album name of current playing media, music track only.""" - return self._media_album_name + return self._media_album_name or None @property @soco_coordinator def media_title(self): """Title of current playing media.""" - return self._media_title + return self._media_title or None @property @soco_coordinator def source(self): """Name of the current input source.""" - return self._source_name + return self._source_name or None @property @soco_coordinator From ff92a8b2602f38f373a7ed4954626fc08d58c084 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Mar 2020 00:27:19 +0100 Subject: [PATCH 022/431] Add update class method to DataUpdateCoordinator (#32724) * Add update class method to DataUpdateCoordinator * Update homeassistant/helpers/update_coordinator.py Co-Authored-By: Paulus Schoutsen * Move update_method param * Rename async_update_data to _async_update_data * Raise NotImplementedError * Re-raise NotImplementedError * Remove caplog, not needed anymore * Don't set last_update_success on NotImplementedError * Fix test Co-authored-by: Paulus Schoutsen --- homeassistant/helpers/update_coordinator.py | 15 ++++++++++++--- tests/helpers/test_update_coordinator.py | 20 ++++++++++++++++++++ 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index b2fe87148b1..85db657c441 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -30,8 +30,8 @@ class DataUpdateCoordinator: logger: logging.Logger, *, name: str, - update_method: Callable[[], Awaitable], update_interval: timedelta, + update_method: Optional[Callable[[], Awaitable]] = None, request_refresh_debouncer: Optional[Debouncer] = None, ): """Initialize global data updater.""" @@ -104,8 +104,14 @@ class DataUpdateCoordinator: """ await self._debounced_refresh.async_call() + async def _async_update_data(self) -> Optional[Any]: + """Fetch the latest data from the source.""" + if self.update_method is None: + raise NotImplementedError("Update method not implemented") + return await self.update_method() + async def async_refresh(self) -> None: - """Update data.""" + """Refresh data.""" if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None @@ -114,7 +120,7 @@ class DataUpdateCoordinator: try: start = monotonic() - self.data = await self.update_method() + self.data = await self._async_update_data() except asyncio.TimeoutError: if self.last_update_success: @@ -131,6 +137,9 @@ class DataUpdateCoordinator: self.logger.error("Error fetching %s data: %s", self.name, err) self.last_update_success = False + except NotImplementedError as err: + raise err + except Exception as err: # pylint: disable=broad-except self.last_update_success = False self.logger.exception( diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 115e00168fc..c17c79ccbc8 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -104,6 +104,16 @@ async def test_refresh_fail_unknown(crd, caplog): assert "Unexpected error fetching test data" in caplog.text +async def test_refresh_no_update_method(crd): + """Test raising error is no update method is provided.""" + await crd.async_refresh() + + crd.update_method = None + + with pytest.raises(NotImplementedError): + await crd.async_refresh() + + async def test_update_interval(hass, crd): """Test update interval works.""" # Test we don't update without subscriber @@ -132,3 +142,13 @@ async def test_update_interval(hass, crd): # Test we stop updating after we lose last subscriber assert crd.data == 2 + + +async def test_refresh_recover(crd, caplog): + """Test recovery of freshing data.""" + crd.last_update_success = False + + await crd.async_refresh() + + assert crd.last_update_success is True + assert "Fetching test data recovered" in caplog.text From 11a25157c10f27a61902a6c329edf88ceb717c67 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Mar 2020 00:28:26 +0100 Subject: [PATCH 023/431] Upgrade sqlalchemy to 1.3.15 (#32747) --- 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 9fa09f9a478..04fabd28bc6 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.3.13"], + "requirements": ["sqlalchemy==1.3.15"], "dependencies": [], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/sql/manifest.json b/homeassistant/components/sql/manifest.json index de2fce5b1a1..9d6e7f7b62b 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.3.13"], + "requirements": ["sqlalchemy==1.3.15"], "dependencies": [], "codeowners": ["@dgomes"] } diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 56ab609c5af..e7c374f6367 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -22,7 +22,7 @@ pytz>=2019.03 pyyaml==5.3 requests==2.23.0 ruamel.yaml==0.15.100 -sqlalchemy==1.3.13 +sqlalchemy==1.3.15 voluptuous-serialize==2.3.0 voluptuous==0.11.7 zeroconf==0.24.5 diff --git a/requirements_all.txt b/requirements_all.txt index fa833a615fb..3839a21ca24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1927,7 +1927,7 @@ spotipy==2.7.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.13 +sqlalchemy==1.3.15 # homeassistant.components.starline starline==0.1.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8be49910948..475f24266a5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -666,7 +666,7 @@ spotipy==2.7.1 # homeassistant.components.recorder # homeassistant.components.sql -sqlalchemy==1.3.13 +sqlalchemy==1.3.15 # homeassistant.components.starline starline==0.1.3 From 94b6ab286248d5e1903101ae777f84e65eb22fca Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Fri, 13 Mar 2020 02:31:39 +0100 Subject: [PATCH 024/431] Use platform tag to register components on hue SensorManager (#32732) * Use platform tag to register components on hue SensorManager instead of a boolean flag to decide between sensor and binary sensor, so it could be used externally (or to get ready for inclusion of other comps) * Make new item discovery platform agnostic for SensorManager --- homeassistant/components/hue/binary_sensor.py | 4 ++-- homeassistant/components/hue/sensor.py | 6 +++--- homeassistant/components/hue/sensor_base.py | 21 ++++++++----------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/hue/binary_sensor.py b/homeassistant/components/hue/binary_sensor.py index 319f8f5fa19..8a6b5d203a8 100644 --- a/homeassistant/components/hue/binary_sensor.py +++ b/homeassistant/components/hue/binary_sensor.py @@ -17,7 +17,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Defer binary sensor setup to the shared sensor module.""" await hass.data[HUE_DOMAIN][ config_entry.entry_id - ].sensor_manager.async_register_component(True, async_add_entities) + ].sensor_manager.async_register_component("binary_sensor", async_add_entities) class HuePresence(GenericZLLSensor, BinarySensorDevice): @@ -44,7 +44,7 @@ class HuePresence(GenericZLLSensor, BinarySensorDevice): SENSOR_CONFIG_MAP.update( { TYPE_ZLL_PRESENCE: { - "binary": True, + "platform": "binary_sensor", "name_format": PRESENCE_NAME_FORMAT, "class": HuePresence, } diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 5fa2ed68389..61acd097b01 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -19,7 +19,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Defer sensor setup to the shared sensor module.""" await hass.data[HUE_DOMAIN][ config_entry.entry_id - ].sensor_manager.async_register_component(False, async_add_entities) + ].sensor_manager.async_register_component("sensor", async_add_entities) class GenericHueGaugeSensorEntity(GenericZLLSensor, Entity): @@ -82,12 +82,12 @@ class HueTemperature(GenericHueGaugeSensorEntity): SENSOR_CONFIG_MAP.update( { TYPE_ZLL_LIGHTLEVEL: { - "binary": False, + "platform": "sensor", "name_format": LIGHT_LEVEL_NAME_FORMAT, "class": HueLightLevel, }, TYPE_ZLL_TEMPERATURE: { - "binary": False, + "platform": "sensor", "name_format": TEMPERATURE_NAME_FORMAT, "class": HueTemperature, }, diff --git a/homeassistant/components/hue/sensor_base.py b/homeassistant/components/hue/sensor_base.py index 507415963a5..9596d7457aa 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -62,9 +62,9 @@ class SensorManager: except AiohueException as err: raise UpdateFailed(f"Hue error: {err}") - async def async_register_component(self, binary, async_add_entities): + async def async_register_component(self, platform, async_add_entities): """Register async_add_entities methods for components.""" - self._component_add_entities[binary] = async_add_entities + self._component_add_entities[platform] = async_add_entities if len(self._component_add_entities) < 2: return @@ -84,8 +84,7 @@ class SensorManager: if len(self._component_add_entities) < 2: return - new_sensors = [] - new_binary_sensors = [] + to_add = {} primary_sensor_devices = {} current = self.current @@ -129,10 +128,10 @@ class SensorManager: current[api[item_id].uniqueid] = sensor_config["class"]( api[item_id], name, self.bridge, primary_sensor=primary_sensor ) - if sensor_config["binary"]: - new_binary_sensors.append(current[api[item_id].uniqueid]) - else: - new_sensors.append(current[api[item_id].uniqueid]) + + to_add.setdefault(sensor_config["platform"], []).append( + current[api[item_id].uniqueid] + ) self.bridge.hass.async_create_task( remove_devices( @@ -140,10 +139,8 @@ class SensorManager: ) ) - if new_sensors: - self._component_add_entities[False](new_sensors) - if new_binary_sensors: - self._component_add_entities[True](new_binary_sensors) + for platform in to_add: + self._component_add_entities[platform](to_add[platform]) class GenericHueSensor(entity.Entity): From 4f0997f6e963e09d72624c783f3d8e8297bb221d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 12 Mar 2020 23:00:00 -0600 Subject: [PATCH 025/431] Add options flow for SimpliSafe (#32631) * Fix bug where SimpliSafe ignored code from UI * Fix tests * Add options flow * Fix tests * Code review * Code review * Code review --- .../simplisafe/.translations/en.json | 11 +- .../components/simplisafe/__init__.py | 26 +++- .../simplisafe/alarm_control_panel.py | 39 ++--- .../components/simplisafe/config_flow.py | 40 +++++- .../components/simplisafe/strings.json | 13 +- .../components/simplisafe/test_config_flow.py | 135 +++++++++++------- 6 files changed, 186 insertions(+), 78 deletions(-) diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json index 7e9c26291f7..60c3784ee9d 100644 --- a/homeassistant/components/simplisafe/.translations/en.json +++ b/homeassistant/components/simplisafe/.translations/en.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Code (for Home Assistant)", "password": "Password", "username": "Email Address" }, @@ -18,5 +17,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Code (used in Home Assistant UI)" + }, + "title": "Configure SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 8c75ed5d9f5..9f014a44a23 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -201,10 +201,21 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up SimpliSafe as config entry.""" + entry_updates = {} if not config_entry.unique_id: - hass.config_entries.async_update_entry( - config_entry, unique_id=config_entry.data[CONF_USERNAME] - ) + # If the config entry doesn't already have a unique ID, set one: + entry_updates["unique_id"] = config_entry.data[CONF_USERNAME] + if CONF_CODE in config_entry.data: + # If an alarm code was provided as part of configuration.yaml, pop it out of + # the config entry's data and move it to options: + data = {**config_entry.data} + entry_updates["data"] = data + entry_updates["options"] = { + **config_entry.options, + CONF_CODE: data.pop(CONF_CODE), + } + if entry_updates: + hass.config_entries.async_update_entry(config_entry, **entry_updates) _verify_domain_control = verify_domain_control(hass, DOMAIN) @@ -309,6 +320,8 @@ async def async_setup_entry(hass, config_entry): ]: async_register_admin_service(hass, DOMAIN, service, method, schema=schema) + config_entry.add_update_listener(async_update_options) + return True @@ -328,6 +341,12 @@ async def async_unload_entry(hass, entry): return True +async def async_update_options(hass, config_entry): + """Handle an options update.""" + simplisafe = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + simplisafe.options = config_entry.options + + class SimpliSafeWebsocket: """Define a SimpliSafe websocket "manager" object.""" @@ -394,6 +413,7 @@ class SimpliSafe: self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} + self.options = config_entry.options or {} self.initial_event_to_use = {} self.systems = {} self.websocket = SimpliSafeWebsocket(hass, api.websocket) diff --git a/homeassistant/components/simplisafe/alarm_control_panel.py b/homeassistant/components/simplisafe/alarm_control_panel.py index 9166c59bec0..4e2393bd238 100644 --- a/homeassistant/components/simplisafe/alarm_control_panel.py +++ b/homeassistant/components/simplisafe/alarm_control_panel.py @@ -67,10 +67,7 @@ async def async_setup_entry(hass, entry, async_add_entities): """Set up a SimpliSafe alarm control panel based on a config entry.""" simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id] async_add_entities( - [ - SimpliSafeAlarm(simplisafe, system, entry.data.get(CONF_CODE)) - for system in simplisafe.systems.values() - ], + [SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()], True, ) @@ -78,11 +75,10 @@ async def async_setup_entry(hass, entry, async_add_entities): class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): """Representation of a SimpliSafe alarm.""" - def __init__(self, simplisafe, system, code): + def __init__(self, simplisafe, system): """Initialize the SimpliSafe alarm.""" super().__init__(simplisafe, system, "Alarm Control Panel") self._changed_by = None - self._code = code self._last_event = None if system.alarm_going_off: @@ -125,9 +121,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): @property def code_format(self): """Return one or more digits/characters.""" - if not self._code: + if not self._simplisafe.options.get(CONF_CODE): return None - if isinstance(self._code, str) and re.search("^\\d+$", self._code): + if isinstance(self._simplisafe.options[CONF_CODE], str) and re.search( + "^\\d+$", self._simplisafe.options[CONF_CODE] + ): return FORMAT_NUMBER return FORMAT_TEXT @@ -141,16 +139,23 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): """Return the list of supported features.""" return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - def _validate_code(self, code, state): - """Validate given code.""" - check = self._code is None or code == self._code - if not check: - _LOGGER.warning("Wrong code entered for %s", state) - return check + @callback + def _is_code_valid(self, code, state): + """Validate that a code matches the required one.""" + if not self._simplisafe.options.get(CONF_CODE): + return True + + if not code or code != self._simplisafe.options[CONF_CODE]: + _LOGGER.warning( + "Incorrect alarm code entered (target state: %s): %s", state, code + ) + return False + + return True async def async_alarm_disarm(self, code=None): """Send disarm command.""" - if not self._validate_code(code, "disarming"): + if not self._is_code_valid(code, STATE_ALARM_DISARMED): return try: @@ -163,7 +168,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): async def async_alarm_arm_home(self, code=None): """Send arm home command.""" - if not self._validate_code(code, "arming home"): + if not self._is_code_valid(code, STATE_ALARM_ARMED_HOME): return try: @@ -176,7 +181,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel): async def async_alarm_arm_away(self, code=None): """Send arm away command.""" - if not self._validate_code(code, "arming away"): + if not self._is_code_valid(code, STATE_ALARM_ARMED_AWAY): return try: diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 4963f9d2de1..031d5496f9d 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.core import callback from homeassistant.helpers import aiohttp_client from .const import DOMAIN # pylint: disable=unused-import @@ -34,6 +35,12 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors=errors if errors else {}, ) + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return SimpliSafeOptionsFlowHandler(config_entry) + async def async_step_import(self, import_config): """Import a config entry from configuration.yaml.""" return await self.async_step_user(import_config) @@ -46,17 +53,44 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): await self.async_set_unique_id(user_input[CONF_USERNAME]) self._abort_if_unique_id_configured() - username = user_input[CONF_USERNAME] websession = aiohttp_client.async_get_clientsession(self.hass) try: simplisafe = await API.login_via_credentials( - username, user_input[CONF_PASSWORD], websession + user_input[CONF_USERNAME], user_input[CONF_PASSWORD], websession ) except SimplipyError: return await self._show_form(errors={"base": "invalid_credentials"}) return self.async_create_entry( title=user_input[CONF_USERNAME], - data={CONF_USERNAME: username, CONF_TOKEN: simplisafe.refresh_token}, + data={ + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_TOKEN: simplisafe.refresh_token, + CONF_CODE: user_input.get(CONF_CODE), + }, + ) + + +class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a SimpliSafe options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + async def async_step_init(self, user_input=None): + """Manage the 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_CODE, default=self.config_entry.options.get(CONF_CODE), + ): str + } + ), ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 3043bd79104..1c8aadc2192 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -6,8 +6,7 @@ "title": "Fill in your information", "data": { "username": "Email Address", - "password": "Password", - "code": "Code (for Home Assistant)" + "password": "Password" } } }, @@ -18,5 +17,15 @@ "abort": { "already_configured": "This SimpliSafe account is already in use." } + }, + "options": { + "step": { + "init": { + "title": "Configure SimpliSafe", + "data": { + "code": "Code (used in Home Assistant UI)" + } + } + } } } diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index 496c6d88954..f53636fc440 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,15 +1,16 @@ """Define tests for the SimpliSafe config flow.""" import json -from unittest.mock import MagicMock, PropertyMock, mock_open, patch +from unittest.mock import MagicMock, PropertyMock, mock_open +from asynctest import patch from simplipy.errors import SimplipyError from homeassistant import data_entry_flow -from homeassistant.components.simplisafe import DOMAIN, config_flow -from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME +from homeassistant.components.simplisafe import DOMAIN +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME -from tests.common import MockConfigEntry, mock_coro +from tests.common import MockConfigEntry def mock_api(): @@ -39,55 +40,83 @@ async def test_invalid_credentials(hass): """Test that invalid credentials throws an error.""" conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - flow = config_flow.SimpliSafeFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} + with patch( + "simplipy.API.login_via_credentials", side_effect=SimplipyError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) + assert result["errors"] == {"base": "invalid_credentials"} + + +async def test_options_flow(hass): + """Test config flow options.""" + conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} + + config_entry = MockConfigEntry( + domain=DOMAIN, unique_id="abcde12345", data=conf, options={CONF_CODE: "1234"}, + ) + config_entry.add_to_hass(hass) with patch( - "simplipy.API.login_via_credentials", - return_value=mock_coro(exception=SimplipyError), + "homeassistant.components.simplisafe.async_setup_entry", return_value=True ): - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {"base": "invalid_credentials"} + 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_CODE: "4321"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_CODE: "4321"} async def test_show_form(hass): """Test that the form is served with no input.""" - flow = config_flow.SimpliSafeFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} + with patch( + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT} + ) - result = await flow.async_step_user(user_input=None) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" async def test_step_import(hass): """Test that the import step works.""" - conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} - - flow = config_flow.SimpliSafeFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} + conf = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + } mop = mock_open(read_data=json.dumps({"refresh_token": "12345"})) with patch( - "simplipy.API.login_via_credentials", - return_value=mock_coro(return_value=mock_api()), + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch( + "homeassistant.util.json.open", mop, create=True + ), patch( + "homeassistant.util.json.os.open", return_value=0 + ), patch( + "homeassistant.util.json.os.replace" ): - with patch("homeassistant.util.json.open", mop, create=True): - with patch("homeassistant.util.json.os.open", return_value=0): - with patch("homeassistant.util.json.os.replace"): - result = await flow.async_step_import(import_config=conf) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "user@email.com" - assert result["data"] == { - CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", - } + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_TOKEN: "12345abc", + CONF_CODE: "1234", + } async def test_step_user(hass): @@ -95,26 +124,28 @@ async def test_step_user(hass): conf = { CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password", + CONF_CODE: "1234", } - flow = config_flow.SimpliSafeFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} - mop = mock_open(read_data=json.dumps({"refresh_token": "12345"})) with patch( - "simplipy.API.login_via_credentials", - return_value=mock_coro(return_value=mock_api()), + "homeassistant.components.simplisafe.async_setup_entry", return_value=True + ), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch( + "homeassistant.util.json.open", mop, create=True + ), patch( + "homeassistant.util.json.os.open", return_value=0 + ), patch( + "homeassistant.util.json.os.replace" ): - with patch("homeassistant.util.json.open", mop, create=True): - with patch("homeassistant.util.json.os.open", return_value=0): - with patch("homeassistant.util.json.os.replace"): - result = await flow.async_step_user(user_input=conf) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=conf + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "user@email.com" - assert result["data"] == { - CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", - } + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "user@email.com" + assert result["data"] == { + CONF_USERNAME: "user@email.com", + CONF_TOKEN: "12345abc", + CONF_CODE: "1234", + } From 7e6e36db1592c59c06760841748729c596a3cfee Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Thu, 12 Mar 2020 23:34:09 -0700 Subject: [PATCH 026/431] Bump total-connect-client to 0.54.1 #32758) --- homeassistant/components/totalconnect/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/totalconnect/manifest.json b/homeassistant/components/totalconnect/manifest.json index 3ebb319ad07..4675ef0ffaf 100644 --- a/homeassistant/components/totalconnect/manifest.json +++ b/homeassistant/components/totalconnect/manifest.json @@ -2,7 +2,7 @@ "domain": "totalconnect", "name": "Honeywell Total Connect Alarm", "documentation": "https://www.home-assistant.io/integrations/totalconnect", - "requirements": ["total_connect_client==0.53"], + "requirements": ["total_connect_client==0.54.1"], "dependencies": [], "codeowners": ["@austinmroczek"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3839a21ca24..848b3acc38a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2017,7 +2017,7 @@ todoist-python==8.0.0 toonapilib==3.2.4 # homeassistant.components.totalconnect -total_connect_client==0.53 +total_connect_client==0.54.1 # homeassistant.components.tplink_lte tp-connected==0.0.4 From 31d150794dbcc44752afe78ec82c4f80c2e4a24a Mon Sep 17 00:00:00 2001 From: brefra Date: Fri, 13 Mar 2020 08:09:30 +0100 Subject: [PATCH 027/431] Add memo text service (#31222) * Add set_memo_text service * Apply template rendering for memo text * Update constants to comply to naming conventions * Local variable for module address and extended error description * fixed typo --- homeassistant/components/velbus/__init__.py | 30 +++++++++++++++++-- homeassistant/components/velbus/const.py | 4 +++ homeassistant/components/velbus/services.yaml | 16 ++++++++++ 3 files changed, 48 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velbus/__init__.py b/homeassistant/components/velbus/__init__.py index b4fe49a88e7..72ffda48b57 100644 --- a/homeassistant/components/velbus/__init__.py +++ b/homeassistant/components/velbus/__init__.py @@ -6,13 +6,13 @@ import velbus import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_NAME, CONF_PORT +from homeassistant.const import CONF_ADDRESS, CONF_NAME, CONF_PORT from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from .const import DOMAIN +from .const import CONF_MEMO_TEXT, DOMAIN, SERVICE_SET_MEMO_TEXT _LOGGER = logging.getLogger(__name__) @@ -80,6 +80,32 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.services.async_register(DOMAIN, "sync_clock", syn_clock, schema=vol.Schema({})) + def set_memo_text(service): + """Handle Memo Text service call.""" + module_address = service.data[CONF_ADDRESS] + memo_text = service.data[CONF_MEMO_TEXT] + memo_text.hass = hass + try: + controller.get_module(module_address).set_memo_text( + memo_text.async_render() + ) + except velbus.util.VelbusException as err: + _LOGGER.error("An error occurred while setting memo text: %s", err) + + hass.services.async_register( + DOMAIN, + SERVICE_SET_MEMO_TEXT, + set_memo_text, + vol.Schema( + { + vol.Required(CONF_ADDRESS): vol.All( + vol.Coerce(int), vol.Range(min=0, max=255) + ), + vol.Optional(CONF_MEMO_TEXT, default=""): cv.template, + } + ), + ) + return True diff --git a/homeassistant/components/velbus/const.py b/homeassistant/components/velbus/const.py index 0d3a66fa743..d3987295fce 100644 --- a/homeassistant/components/velbus/const.py +++ b/homeassistant/components/velbus/const.py @@ -1,3 +1,7 @@ """Const for Velbus.""" DOMAIN = "velbus" + +CONF_MEMO_TEXT = "memo_text" + +SERVICE_SET_MEMO_TEXT = "set_memo_text" diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml index 273cc8b4caa..ea31b951a18 100644 --- a/homeassistant/components/velbus/services.yaml +++ b/homeassistant/components/velbus/services.yaml @@ -1,2 +1,18 @@ sync_clock: description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink + +set_memo_text: + description: > + Set the memo text to the display of modules like VMBGPO, VMBGPOD + Be sure the page(s) of the module is configured to display the memo text. + fields: + address: + description: > + The module address in decimal format. + The decimal addresses are displayed in front of the modules listed at the integration page. + example: '11' + memo_text: + description: > + The actual text to be displayed. + Text is limited to 64 characters. + example: 'Do not forget trash' \ No newline at end of file From 26d7b2164e8a971506790ae5af06f31abdf278b5 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Fri, 13 Mar 2020 07:16:24 -0400 Subject: [PATCH 028/431] Move apps configuration to options flow for vizio integration (#32543) * move apps configuration to options flow * add additional assertion to new test * add additional assertions for options update * update docstrings, config validation, and tests based on review --- homeassistant/components/vizio/config_flow.py | 110 +++++++++----- .../components/vizio/media_player.py | 30 ++-- homeassistant/components/vizio/strings.json | 17 +-- tests/components/vizio/test_config_flow.py | 143 +++++++++++------- 4 files changed, 188 insertions(+), 112 deletions(-) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 1eb58c96670..7dc4b19fa83 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -43,7 +43,8 @@ def _get_config_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: """ Return schema defaults for init step based on user input/config dict. - Retain info already provided for future form views by setting them as defaults in schema. + Retain info already provided for future form views by setting them + as defaults in schema. """ if input_dict is None: input_dict = {} @@ -70,7 +71,8 @@ def _get_pairing_schema(input_dict: Dict[str, Any] = None) -> vol.Schema: """ Return schema defaults for pairing data based on user input. - Retain info already provided for future form views by setting them as defaults in schema. + Retain info already provided for future form views by setting + them as defaults in schema. """ if input_dict is None: input_dict = {} @@ -97,6 +99,16 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): ) -> Dict[str, Any]: """Manage the vizio options.""" if user_input is not None: + if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): + user_input[CONF_APPS] = { + user_input[CONF_INCLUDE_OR_EXCLUDE]: user_input[ + CONF_APPS_TO_INCLUDE_OR_EXCLUDE + ].copy() + } + + user_input.pop(CONF_INCLUDE_OR_EXCLUDE) + user_input.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE) + return self.async_create_entry(title="", data=user_input) options = { @@ -108,6 +120,30 @@ class VizioOptionsConfigFlow(config_entries.OptionsFlow): ): vol.All(vol.Coerce(int), vol.Range(min=1, max=10)) } + if self.config_entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV: + default_include_or_exclude = ( + CONF_EXCLUDE + if self.config_entry.options + and CONF_EXCLUDE in self.config_entry.options.get(CONF_APPS) + else CONF_EXCLUDE + ) + options.update( + { + vol.Optional( + CONF_INCLUDE_OR_EXCLUDE, + default=default_include_or_exclude.title(), + ): vol.All( + vol.In([CONF_INCLUDE.title(), CONF_EXCLUDE.title()]), vol.Lower + ), + vol.Optional( + CONF_APPS_TO_INCLUDE_OR_EXCLUDE, + default=self.config_entry.options.get(CONF_APPS, {}).get( + default_include_or_exclude, [] + ), + ): cv.multi_select(VizioAsync.get_apps_list()), + } + ) + return self.async_show_form(step_id="init", data_schema=vol.Schema(options)) @@ -135,7 +171,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _create_entry_if_unique( self, input_dict: Dict[str, Any] ) -> Dict[str, Any]: - """Check if unique_id doesn't already exist. If it does, abort. If it doesn't, create entry.""" + """ + Create entry if ID is unique. + + If it is, create entry. If it isn't, abort config flow. + """ # Remove extra keys that will not be used by entry setup input_dict.pop(CONF_APPS_TO_INCLUDE_OR_EXCLUDE, None) input_dict.pop(CONF_INCLUDE_OR_EXCLUDE, None) @@ -195,13 +235,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) self._abort_if_unique_id_configured() - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - if ( - user_input[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV - and self.context["source"] != SOURCE_IMPORT - ): - self._data = copy.deepcopy(user_input) - return await self.async_step_tv_apps() return await self._create_entry_if_unique(user_input) # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 elif self._must_show_form and self.context["source"] == SOURCE_IMPORT: @@ -250,7 +283,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if not import_config.get(CONF_APPS): remove_apps = True else: - updated_data[CONF_APPS] = import_config[CONF_APPS] + updated_options[CONF_APPS] = import_config[CONF_APPS] if entry.data.get(CONF_VOLUME_STEP) != import_config[CONF_VOLUME_STEP]: updated_options[CONF_VOLUME_STEP] = import_config[CONF_VOLUME_STEP] @@ -261,6 +294,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if remove_apps: new_data.pop(CONF_APPS) + new_options.pop(CONF_APPS) if updated_data: new_data.update(updated_data) @@ -319,7 +353,11 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_pair_tv( self, user_input: Dict[str, Any] = None ) -> Dict[str, Any]: - """Start pairing process and ask user for PIN to complete pairing process.""" + """ + Start pairing process for TV. + + Ask user for PIN to complete pairing process. + """ errors = {} # Start pairing process if it hasn't already started @@ -382,7 +420,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # If user is pairing via config import, show different message return await self.async_step_pairing_complete_import() - return await self.async_step_tv_apps() + return await self.async_step_pairing_complete() # If no data was retrieved, it's assumed that the pairing attempt was not # successful @@ -394,43 +432,35 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_pairing_complete_import( - self, user_input: Dict[str, Any] = None - ) -> Dict[str, Any]: - """Complete import config flow by displaying final message to show user access token and give further instructions.""" + async def _pairing_complete(self, step_id: str) -> Dict[str, Any]: + """Handle config flow completion.""" if not self._must_show_form: return await self._create_entry_if_unique(self._data) self._must_show_form = False return self.async_show_form( - step_id="pairing_complete_import", + step_id=step_id, data_schema=vol.Schema({}), description_placeholders={"access_token": self._data[CONF_ACCESS_TOKEN]}, ) - async def async_step_tv_apps( + async def async_step_pairing_complete( self, user_input: Dict[str, Any] = None ) -> Dict[str, Any]: - """Handle app configuration to complete TV configuration.""" - if user_input is not None: - if user_input.get(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): - # Update stored apps with user entry config keys - self._apps[user_input[CONF_INCLUDE_OR_EXCLUDE].lower()] = user_input[ - CONF_APPS_TO_INCLUDE_OR_EXCLUDE - ].copy() + """ + Complete non-import sourced config flow. - return await self._create_entry_if_unique(self._data) + Display final message to user confirming pairing. + """ + return await self._pairing_complete("pairing_complete") - return self.async_show_form( - step_id="tv_apps", - data_schema=vol.Schema( - { - vol.Optional( - CONF_INCLUDE_OR_EXCLUDE, default=CONF_INCLUDE.title(), - ): vol.In([CONF_INCLUDE.title(), CONF_EXCLUDE.title()]), - vol.Optional(CONF_APPS_TO_INCLUDE_OR_EXCLUDE): cv.multi_select( - VizioAsync.get_apps_list() - ), - } - ), - ) + async def async_step_pairing_complete_import( + self, user_input: Dict[str, Any] = None + ) -> Dict[str, Any]: + """ + Complete import sourced config flow. + + Display final message to user confirming pairing and displaying + access token. + """ + return await self._pairing_complete("pairing_complete_import") diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index d013f41403a..a46a4c9a2d1 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -60,7 +60,6 @@ async def async_setup_entry( token = config_entry.data.get(CONF_ACCESS_TOKEN) name = config_entry.data[CONF_NAME] device_class = config_entry.data[CONF_DEVICE_CLASS] - conf_apps = config_entry.data.get(CONF_APPS, {}) # If config entry options not set up, set them up, otherwise assign values managed in options volume_step = config_entry.options.get( @@ -70,6 +69,20 @@ async def async_setup_entry( params = {} if not config_entry.options: params["options"] = {CONF_VOLUME_STEP: volume_step} + include_or_exclude_key = next( + ( + key + for key in config_entry.data.get(CONF_APPS, {}) + if key in [CONF_INCLUDE, CONF_EXCLUDE] + ), + None, + ) + if include_or_exclude_key: + params["options"][CONF_APPS] = { + include_or_exclude_key: config_entry.data[CONF_APPS][ + include_or_exclude_key + ].copy() + } if not config_entry.data.get(CONF_VOLUME_STEP): new_data = config_entry.data.copy() @@ -93,9 +106,7 @@ async def async_setup_entry( _LOGGER.warning("Failed to connect to %s", host) raise PlatformNotReady - entity = VizioDevice( - config_entry, device, name, volume_step, device_class, conf_apps, - ) + entity = VizioDevice(config_entry, device, name, device_class,) async_add_entities([entity], update_before_add=True) @@ -108,9 +119,7 @@ class VizioDevice(MediaPlayerDevice): config_entry: ConfigEntry, device: VizioAsync, name: str, - volume_step: int, device_class: str, - conf_apps: Dict[str, List[Any]], ) -> None: """Initialize Vizio device.""" self._config_entry = config_entry @@ -119,14 +128,16 @@ class VizioDevice(MediaPlayerDevice): self._name = name self._state = None self._volume_level = None - self._volume_step = volume_step + self._volume_step = config_entry.options[CONF_VOLUME_STEP] self._is_muted = None self._current_input = None self._current_app = None self._available_inputs = [] self._available_apps = [] - self._conf_apps = conf_apps - self._additional_app_configs = self._conf_apps.get(CONF_ADDITIONAL_CONFIGS, []) + self._conf_apps = config_entry.options.get(CONF_APPS, {}) + self._additional_app_configs = config_entry.data.get(CONF_APPS, {}).get( + CONF_ADDITIONAL_CONFIGS, [] + ) self._device_class = device_class self._supported_commands = SUPPORTED_COMMANDS[device_class] self._device = device @@ -248,6 +259,7 @@ class VizioDevice(MediaPlayerDevice): async def _async_update_options(self, config_entry: ConfigEntry) -> None: """Update options if the update signal comes from this entity.""" self._volume_step = config_entry.options[CONF_VOLUME_STEP] + self._conf_apps.update(config_entry.options.get(CONF_APPS, {})) async def async_added_to_hass(self): """Register callbacks when entity is added.""" diff --git a/homeassistant/components/vizio/strings.json b/homeassistant/components/vizio/strings.json index 61db7b49665..b6f6f53cf79 100644 --- a/homeassistant/components/vizio/strings.json +++ b/homeassistant/components/vizio/strings.json @@ -19,17 +19,13 @@ "pin": "PIN" } }, + "pairing_complete": { + "title": "Pairing Complete", + "description": "Your Vizio SmartCast device is now connected to Home Assistant." + }, "pairing_complete_import": { "title": "Pairing Complete", "description": "Your Vizio SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'." - }, - "tv_apps": { - "title": "Configure Apps for Smart TV", - "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list. You can skip this step for TVs that don't support apps.", - "data": { - "include_or_exclude": "Include or Exclude Apps?", - "apps_to_include_or_exclude": "Apps to Include or Exclude" - } } }, "error": { @@ -48,8 +44,11 @@ "step": { "init": { "title": "Update Vizo SmartCast Options", + "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list.", "data": { - "volume_step": "Volume Step Size" + "volume_step": "Volume Step Size", + "include_or_exclude": "Include or Exclude Apps?", + "apps_to_include_or_exclude": "Apps to Include or Exclude" } } } diff --git a/tests/components/vizio/test_config_flow.py b/tests/components/vizio/test_config_flow.py index e773035447a..a8a760d8ca2 100644 --- a/tests/components/vizio/test_config_flow.py +++ b/tests/components/vizio/test_config_flow.py @@ -9,9 +9,7 @@ from homeassistant.components.media_player import DEVICE_CLASS_SPEAKER, DEVICE_C from homeassistant.components.vizio.config_flow import _get_config_schema from homeassistant.components.vizio.const import ( CONF_APPS, - CONF_APPS_TO_INCLUDE_OR_EXCLUDE, CONF_INCLUDE, - CONF_INCLUDE_OR_EXCLUDE, CONF_VOLUME_STEP, DEFAULT_NAME, DEFAULT_VOLUME_STEP, @@ -39,6 +37,7 @@ from .const import ( MOCK_PIN_CONFIG, MOCK_SPEAKER_CONFIG, MOCK_TV_CONFIG_NO_TOKEN, + MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, MOCK_TV_WITH_EXCLUDE_CONFIG, MOCK_USER_VALID_TV_CONFIG, MOCK_ZEROCONF_SERVICE_INFO, @@ -95,52 +94,17 @@ async def test_user_flow_all_fields( result["flow_id"], user_input=MOCK_USER_VALID_TV_CONFIG ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "tv_apps" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_INCLUDE_APPS - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME assert result["data"][CONF_NAME] == NAME assert result["data"][CONF_HOST] == HOST assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN - assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] + assert CONF_APPS not in result["data"] -async def test_user_apps_with_tv( - hass: HomeAssistantType, - vizio_connect: pytest.fixture, - vizio_bypass_setup: pytest.fixture, -) -> None: - """Test TV can have selected apps during user setup.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=MOCK_IMPORT_VALID_TV_CONFIG - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "tv_apps" - - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_INCLUDE_APPS - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == NAME - assert result["data"][CONF_NAME] == NAME - assert result["data"][CONF_HOST] == HOST - assert result["data"][CONF_DEVICE_CLASS] == DEVICE_CLASS_TV - assert result["data"][CONF_ACCESS_TOKEN] == ACCESS_TOKEN - assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] - assert CONF_APPS_TO_INCLUDE_OR_EXCLUDE not in result["data"] - assert CONF_INCLUDE_OR_EXCLUDE not in result["data"] - - -async def test_options_flow(hass: HomeAssistantType) -> None: - """Test options config flow.""" +async def test_speaker_options_flow(hass: HomeAssistantType) -> None: + """Test options config flow for speaker.""" entry = MockConfigEntry(domain=DOMAIN, data=MOCK_SPEAKER_CONFIG) entry.add_to_hass(hass) @@ -158,6 +122,58 @@ async def test_options_flow(hass: HomeAssistantType) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "" assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS not in result["data"] + + +async def test_tv_options_flow_no_apps(hass: HomeAssistantType) -> None: + """Test options config flow for TV without providing apps option.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) + entry.add_to_hass(hass) + + assert not entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + options = {CONF_VOLUME_STEP: VOLUME_STEP} + options.update(MOCK_INCLUDE_NO_APPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=options + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS not in result["data"] + + +async def test_tv_options_flow_with_apps(hass: HomeAssistantType) -> None: + """Test options config flow for TV with providing apps option.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG) + entry.add_to_hass(hass) + + assert not entry.options + + result = await hass.config_entries.options.async_init(entry.entry_id, data=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + options = {CONF_VOLUME_STEP: VOLUME_STEP} + options.update(MOCK_INCLUDE_APPS) + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input=options + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "" + assert result["data"][CONF_VOLUME_STEP] == VOLUME_STEP + assert CONF_APPS in result["data"] + assert result["data"][CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} async def test_user_host_already_configured( @@ -282,11 +298,9 @@ async def test_user_tv_pairing_no_apps( ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "tv_apps" + assert result["step_id"] == "pairing_complete" - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input=MOCK_INCLUDE_NO_APPS - ) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == NAME @@ -427,10 +441,8 @@ async def test_import_flow_update_options( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "updated_entry" - assert ( - hass.config_entries.async_get_entry(entry_id).options[CONF_VOLUME_STEP] - == VOLUME_STEP + 1 - ) + config_entry = hass.config_entries.async_get_entry(entry_id) + assert config_entry.options[CONF_VOLUME_STEP] == VOLUME_STEP + 1 async def test_import_flow_update_name_and_apps( @@ -461,10 +473,10 @@ async def test_import_flow_update_name_and_apps( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "updated_entry" - assert hass.config_entries.async_get_entry(entry_id).data[CONF_NAME] == NAME2 - assert hass.config_entries.async_get_entry(entry_id).data[CONF_APPS] == { - CONF_INCLUDE: [CURRENT_APP] - } + config_entry = hass.config_entries.async_get_entry(entry_id) + assert config_entry.data[CONF_NAME] == NAME2 + assert config_entry.data[CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} + assert config_entry.options[CONF_APPS] == {CONF_INCLUDE: [CURRENT_APP]} async def test_import_flow_update_remove_apps( @@ -482,7 +494,9 @@ async def test_import_flow_update_remove_apps( assert result["result"].data[CONF_NAME] == NAME assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - entry_id = result["result"].entry_id + config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) + assert CONF_APPS in config_entry.data + assert CONF_APPS in config_entry.options updated_config = MOCK_TV_WITH_EXCLUDE_CONFIG.copy() updated_config.pop(CONF_APPS) @@ -494,7 +508,8 @@ async def test_import_flow_update_remove_apps( assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "updated_entry" - assert hass.config_entries.async_get_entry(entry_id).data.get(CONF_APPS) is None + assert CONF_APPS not in config_entry.data + assert CONF_APPS not in config_entry.options async def test_import_needs_pairing( @@ -577,6 +592,26 @@ async def test_import_with_apps_needs_pairing( assert result["data"][CONF_APPS][CONF_INCLUDE] == [CURRENT_APP] +async def test_import_flow_additional_configs( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, +) -> None: + """Test import config flow with additional configs defined in CONF_APPS.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data=vol.Schema(VIZIO_SCHEMA)(MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG), + ) + await hass.async_block_till_done() + + assert result["result"].data[CONF_NAME] == NAME + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + config_entry = hass.config_entries.async_get_entry(result["result"].entry_id) + assert CONF_APPS in config_entry.data + assert CONF_APPS not in config_entry.options + + async def test_import_error( hass: HomeAssistantType, vizio_connect: pytest.fixture, From 992daa4a44f20664423d0a405f021b51d5057726 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 13 Mar 2020 13:19:05 +0100 Subject: [PATCH 029/431] Migrate WLED to use DataUpdateCoordinator (#32565) * Migrate WLED to use DataUpdateCoordinator * Remove stale debug statement * Process review suggestions * Improve tests * Improve tests --- homeassistant/components/wled/__init__.py | 150 +++++++-------- homeassistant/components/wled/const.py | 5 - homeassistant/components/wled/light.py | 151 ++++++--------- homeassistant/components/wled/sensor.py | 119 ++++++------ homeassistant/components/wled/switch.py | 186 +++++++++---------- tests/components/wled/test_init.py | 29 --- tests/components/wled/test_light.py | 215 ++++++++++++---------- tests/components/wled/test_switch.py | 183 +++++++++--------- 8 files changed, 478 insertions(+), 560 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 1684da28c3f..19901ce297d 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -2,35 +2,27 @@ import asyncio from datetime import timedelta import logging -from typing import Any, Dict, Optional, Union +from typing import Any, Dict -from wled import WLED, WLEDConnectionError, WLEDError +from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError from homeassistant.components.light import DOMAIN as LIGHT_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.const import ATTR_NAME, CONF_HOST -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from homeassistant.helpers.entity import Entity -from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType -from homeassistant.util import dt as dt_util +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_IDENTIFIERS, ATTR_MANUFACTURER, ATTR_MODEL, ATTR_SOFTWARE_VERSION, - DATA_WLED_CLIENT, - DATA_WLED_TIMER, - DATA_WLED_UPDATED, DOMAIN, ) @@ -49,22 +41,19 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up WLED from a config entry.""" # Create WLED instance for this entry - session = async_get_clientsession(hass) - wled = WLED(entry.data[CONF_HOST], session=session) + coordinator = WLEDDataUpdateCoordinator(hass, host=entry.data[CONF_HOST]) + await coordinator.async_refresh() - # Ensure we can connect and talk to it - try: - await wled.update() - except WLEDConnectionError as exception: - raise ConfigEntryNotReady from exception + if not coordinator.last_update_success: + raise ConfigEntryNotReady hass.data.setdefault(DOMAIN, {}) - hass.data[DOMAIN][entry.entry_id] = {DATA_WLED_CLIENT: wled} + hass.data[DOMAIN][entry.entry_id] = coordinator # For backwards compat, set unique ID if entry.unique_id is None: hass.config_entries.async_update_entry( - entry, unique_id=wled.device.info.mac_address + entry, unique_id=coordinator.data.info.mac_address ) # Set up all platforms for this device/entry. @@ -73,32 +62,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.config_entries.async_forward_entry_setup(entry, component) ) - async def interval_update(now: dt_util.dt.datetime = None) -> None: - """Poll WLED device function, dispatches event after update.""" - try: - await wled.update() - except WLEDError: - _LOGGER.debug("An error occurred while updating WLED", exc_info=True) - - # Even if the update failed, we still send out the event. - # To allow entities to make themselves unavailable. - async_dispatcher_send(hass, DATA_WLED_UPDATED, entry.entry_id) - - # Schedule update interval - hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] = async_track_time_interval( - hass, interval_update, SCAN_INTERVAL - ) - return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload WLED config entry.""" - # Cancel update timer for this entry/device. - cancel_timer = hass.data[DOMAIN][entry.entry_id][DATA_WLED_TIMER] - cancel_timer() - # Unload entities for this entry/device. await asyncio.gather( *( @@ -115,26 +84,74 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: return True +def wled_exception_handler(func): + """Decorate WLED calls to handle WLED exceptions. + + A decorator that wraps the passed in function, catches WLED errors, + and handles the availability of the device in the data coordinator. + """ + + async def handler(self, *args, **kwargs): + try: + await func(self, *args, **kwargs) + await self.coordinator.async_refresh() + + except WLEDConnectionError as error: + _LOGGER.error("Error communicating with API: %s", error) + self.coordinator.last_update_success = False + self.coordinator.update_listeners() + + except WLEDError as error: + _LOGGER.error("Invalid response from API: %s", error) + + return handler + + +class WLEDDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching WLED data from single endpoint.""" + + def __init__( + self, hass: HomeAssistant, *, host: str, + ): + """Initialize global WLED data updater.""" + self.wled = WLED(host, session=async_get_clientsession(hass)) + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + def update_listeners(self) -> None: + """Call update on all listeners.""" + for update_callback in self._listeners: + update_callback() + + async def _async_update_data(self) -> WLEDDevice: + """Fetch data from WLED.""" + try: + return await self.wled.update() + except WLEDError as error: + raise UpdateFailed(f"Invalid response from API: {error}") + + class WLEDEntity(Entity): """Defines a base WLED entity.""" def __init__( self, + *, entry_id: str, - wled: WLED, + coordinator: WLEDDataUpdateCoordinator, name: str, icon: str, enabled_default: bool = True, ) -> None: """Initialize the WLED entity.""" - self._attributes: Dict[str, Union[str, int, float]] = {} - self._available = True self._enabled_default = enabled_default self._entry_id = entry_id self._icon = icon self._name = name self._unsub_dispatcher = None - self.wled = wled + self.coordinator = coordinator @property def name(self) -> str: @@ -149,7 +166,7 @@ class WLEDEntity(Entity): @property def available(self) -> bool: """Return True if entity is available.""" - return self._available + return self.coordinator.last_update_success @property def entity_registry_enabled_default(self) -> bool: @@ -161,42 +178,17 @@ class WLEDEntity(Entity): """Return the polling requirement of the entity.""" return False - @property - def device_state_attributes(self) -> Optional[Dict[str, Any]]: - """Return the state attributes of the entity.""" - return self._attributes - async def async_added_to_hass(self) -> None: """Connect to dispatcher listening for entity data notifications.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, DATA_WLED_UPDATED, self._schedule_immediate_update - ) + self.coordinator.async_add_listener(self.async_write_ha_state) async def async_will_remove_from_hass(self) -> None: """Disconnect from update signal.""" - self._unsub_dispatcher() - - @callback - def _schedule_immediate_update(self, entry_id: str) -> None: - """Schedule an immediate update of the entity.""" - if entry_id == self._entry_id: - self.async_schedule_update_ha_state(True) + self.coordinator.async_remove_listener(self.async_write_ha_state) async def async_update(self) -> None: """Update WLED entity.""" - if not self.enabled: - return - - if self.wled.device is None: - self._available = False - return - - self._available = True - await self._wled_update() - - async def _wled_update(self) -> None: - """Update WLED entity.""" - raise NotImplementedError() + await self.coordinator.async_request_refresh() class WLEDDeviceEntity(WLEDEntity): @@ -206,9 +198,9 @@ class WLEDDeviceEntity(WLEDEntity): def device_info(self) -> Dict[str, Any]: """Return device information about this WLED device.""" return { - ATTR_IDENTIFIERS: {(DOMAIN, self.wled.device.info.mac_address)}, - ATTR_NAME: self.wled.device.info.name, - ATTR_MANUFACTURER: self.wled.device.info.brand, - ATTR_MODEL: self.wled.device.info.product, - ATTR_SOFTWARE_VERSION: self.wled.device.info.version, + ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, + ATTR_NAME: self.coordinator.data.info.name, + ATTR_MANUFACTURER: self.coordinator.data.info.brand, + ATTR_MODEL: self.coordinator.data.info.product, + ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, } diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 94ee513f134..4844f37a126 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -3,11 +3,6 @@ # Integration domain DOMAIN = "wled" -# Home Assistant data keys -DATA_WLED_CLIENT = "wled_client" -DATA_WLED_TIMER = "wled_timer" -DATA_WLED_UPDATED = "wled_updated" - # Attributes ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index f22282e5539..22c7e0649fc 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -1,8 +1,6 @@ """Support for LED lights.""" import logging -from typing import Any, Callable, List, Optional, Tuple - -from wled import WLED, Effect, WLEDError +from typing import Any, Callable, Dict, List, Optional, Tuple from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -24,7 +22,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util -from . import WLEDDeviceEntity +from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler from .const import ( ATTR_COLOR_PRIMARY, ATTR_INTENSITY, @@ -34,7 +32,6 @@ from .const import ( ATTR_PRESET, ATTR_SEGMENT_ID, ATTR_SPEED, - DATA_WLED_CLIENT, DOMAIN, ) @@ -49,19 +46,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up WLED light based on a config entry.""" - wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - # Does the WLED device support RGBW - rgbw = wled.device.info.leds.rgbw - - # List of supported effects - effects = wled.device.effects - - # WLED supports splitting a strip in multiple segments - # Each segment will be a separate light in Home Assistant - lights = [] - for light in wled.device.state.segments: - lights.append(WLEDLight(entry.entry_id, wled, light.segment_id, rgbw, effects)) + lights = [ + WLEDLight(entry.entry_id, coordinator, light.segment_id) + for light in coordinator.data.state.segments + ] async_add_entities(lights, True) @@ -70,50 +60,73 @@ class WLEDLight(Light, WLEDDeviceEntity): """Defines a WLED light.""" def __init__( - self, entry_id: str, wled: WLED, segment: int, rgbw: bool, effects: List[Effect] + self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int ): """Initialize WLED light.""" - self._effects = effects - self._rgbw = rgbw + self._rgbw = coordinator.data.info.leds.rgbw self._segment = segment - self._brightness: Optional[int] = None - self._color: Optional[Tuple[float, float]] = None - self._effect: Optional[str] = None - self._state: Optional[bool] = None - self._white_value: Optional[int] = None - # Only apply the segment ID if it is not the first segment - name = wled.device.info.name + name = coordinator.data.info.name if segment != 0: name += f" {segment}" - super().__init__(entry_id, wled, name, "mdi:led-strip-variant") + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=name, + icon="mdi:led-strip-variant", + ) @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return f"{self.wled.device.info.mac_address}_{self._segment}" + return f"{self.coordinator.data.info.mac_address}_{self._segment}" + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + playlist = self.coordinator.data.state.playlist + if playlist == -1: + playlist = None + + preset = self.coordinator.data.state.preset + if preset == -1: + preset = None + + return { + ATTR_INTENSITY: self.coordinator.data.state.segments[ + self._segment + ].intensity, + ATTR_PALETTE: self.coordinator.data.state.segments[ + self._segment + ].palette.name, + ATTR_PLAYLIST: playlist, + ATTR_PRESET: preset, + ATTR_SPEED: self.coordinator.data.state.segments[self._segment].speed, + } @property def hs_color(self) -> Optional[Tuple[float, float]]: """Return the hue and saturation color value [float, float].""" - return self._color + color = self.coordinator.data.state.segments[self._segment].color_primary + return color_util.color_RGB_to_hs(*color[:3]) @property def effect(self) -> Optional[str]: """Return the current effect of the light.""" - return self._effect + return self.coordinator.data.state.segments[self._segment].effect.name @property def brightness(self) -> Optional[int]: """Return the brightness of this light between 1..255.""" - return self._brightness + return self.coordinator.data.state.brightness @property def white_value(self) -> Optional[int]: """Return the white value of this light between 0..255.""" - return self._white_value + color = self.coordinator.data.state.segments[self._segment].color_primary + return color[-1] if self._rgbw else None @property def supported_features(self) -> int: @@ -134,13 +147,14 @@ class WLEDLight(Light, WLEDDeviceEntity): @property def effect_list(self) -> List[str]: """Return the list of supported effects.""" - return [effect.name for effect in self._effects] + return [effect.name for effect in self.coordinator.data.effects] @property def is_on(self) -> bool: """Return the state of the light.""" - return bool(self._state) + return bool(self.coordinator.data.state.on) + @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" data = {ATTR_ON: False, ATTR_SEGMENT_ID: self._segment} @@ -149,14 +163,9 @@ class WLEDLight(Light, WLEDDeviceEntity): # WLED uses 100ms per unit, so 10 = 1 second. data[ATTR_TRANSITION] = round(kwargs[ATTR_TRANSITION] * 10) - try: - await self.wled.light(**data) - self._state = False - except WLEDError: - _LOGGER.error("An error occurred while turning off WLED light.") - self._available = False - self.async_schedule_update_ha_state() + await self.coordinator.wled.light(**data) + @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" data = {ATTR_ON: True, ATTR_SEGMENT_ID: self._segment} @@ -189,8 +198,8 @@ class WLEDLight(Light, WLEDDeviceEntity): ): # WLED cannot just accept a white value, it needs the color. # We use the last know color in case just the white value changes. - if not any(x in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): - hue, sat = self._color + if all(x not in (ATTR_COLOR_TEMP, ATTR_HS_COLOR) for x in kwargs): + hue, sat = self.hs_color data[ATTR_COLOR_PRIMARY] = color_util.color_hsv_to_RGB(hue, sat, 100) # On a RGBW strip, when the color is pure white, disable the RGB LEDs in @@ -202,56 +211,6 @@ class WLEDLight(Light, WLEDDeviceEntity): if ATTR_WHITE_VALUE in kwargs: data[ATTR_COLOR_PRIMARY] += (kwargs[ATTR_WHITE_VALUE],) else: - data[ATTR_COLOR_PRIMARY] += (self._white_value,) + data[ATTR_COLOR_PRIMARY] += (self.white_value,) - try: - await self.wled.light(**data) - - self._state = True - - if ATTR_BRIGHTNESS in kwargs: - self._brightness = kwargs[ATTR_BRIGHTNESS] - - if ATTR_EFFECT in kwargs: - self._effect = kwargs[ATTR_EFFECT] - - if ATTR_HS_COLOR in kwargs: - self._color = kwargs[ATTR_HS_COLOR] - - if ATTR_COLOR_TEMP in kwargs: - self._color = color_util.color_temperature_to_hs(mireds) - - if ATTR_WHITE_VALUE in kwargs: - self._white_value = kwargs[ATTR_WHITE_VALUE] - - except WLEDError: - _LOGGER.error("An error occurred while turning on WLED light.") - self._available = False - self.async_schedule_update_ha_state() - - async def _wled_update(self) -> None: - """Update WLED entity.""" - self._brightness = self.wled.device.state.brightness - self._effect = self.wled.device.state.segments[self._segment].effect.name - self._state = self.wled.device.state.on - - color = self.wled.device.state.segments[self._segment].color_primary - self._color = color_util.color_RGB_to_hs(*color[:3]) - if self._rgbw: - self._white_value = color[-1] - - playlist = self.wled.device.state.playlist - if playlist == -1: - playlist = None - - preset = self.wled.device.state.preset - if preset == -1: - preset = None - - self._attributes = { - ATTR_INTENSITY: self.wled.device.state.segments[self._segment].intensity, - ATTR_PALETTE: self.wled.device.state.segments[self._segment].palette.name, - ATTR_PLAYLIST: playlist, - ATTR_PRESET: preset, - ATTR_SPEED: self.wled.device.state.segments[self._segment].speed, - } + await self.coordinator.wled.light(**data) diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 41e03d8c728..e0d92aecd56 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -1,7 +1,7 @@ """Support for WLED sensors.""" from datetime import timedelta import logging -from typing import Callable, List, Optional, Union +from typing import Any, Callable, Dict, List, Optional, Union from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DEVICE_CLASS_TIMESTAMP @@ -9,8 +9,8 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow -from . import WLED, WLEDDeviceEntity -from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DATA_WLED_CLIENT, DOMAIN +from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity +from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN _LOGGER = logging.getLogger(__name__) @@ -21,12 +21,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up WLED sensor based on a config entry.""" - wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ - WLEDEstimatedCurrentSensor(entry.entry_id, wled), - WLEDUptimeSensor(entry.entry_id, wled), - WLEDFreeHeapSensor(entry.entry_id, wled), + WLEDEstimatedCurrentSensor(entry.entry_id, coordinator), + WLEDUptimeSensor(entry.entry_id, coordinator), + WLEDFreeHeapSensor(entry.entry_id, coordinator), ] async_add_entities(sensors, True) @@ -37,30 +37,31 @@ class WLEDSensor(WLEDDeviceEntity): def __init__( self, - entry_id: str, - wled: WLED, - name: str, - icon: str, - unit_of_measurement: str, - key: str, + *, + coordinator: WLEDDataUpdateCoordinator, enabled_default: bool = True, + entry_id: str, + icon: str, + key: str, + name: str, + unit_of_measurement: Optional[str] = None, ) -> None: """Initialize WLED sensor.""" - self._state = None self._unit_of_measurement = unit_of_measurement self._key = key - super().__init__(entry_id, wled, name, icon, enabled_default) + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=name, + icon=icon, + enabled_default=enabled_default, + ) @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return f"{self.wled.device.info.mac_address}_{self._key}" - - @property - def state(self) -> Union[None, str, int, float]: - """Return the state of the sensor.""" - return self._state + return f"{self.coordinator.data.info.mac_address}_{self._key}" @property def unit_of_measurement(self) -> str: @@ -71,67 +72,73 @@ class WLEDSensor(WLEDDeviceEntity): class WLEDEstimatedCurrentSensor(WLEDSensor): """Defines a WLED estimated current sensor.""" - def __init__(self, entry_id: str, wled: WLED) -> None: + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED estimated current sensor.""" super().__init__( - entry_id, - wled, - f"{wled.device.info.name} Estimated Current", - "mdi:power", - CURRENT_MA, - "estimated_current", + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:power", + key="estimated_current", + name=f"{coordinator.data.info.name} Estimated Current", + unit_of_measurement=CURRENT_MA, ) - async def _wled_update(self) -> None: - """Update WLED entity.""" - self._state = self.wled.device.info.leds.power - self._attributes = { - ATTR_LED_COUNT: self.wled.device.info.leds.count, - ATTR_MAX_POWER: self.wled.device.info.leds.max_power, + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """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 state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.info.leds.power + class WLEDUptimeSensor(WLEDSensor): """Defines a WLED uptime sensor.""" - def __init__(self, entry_id: str, wled: WLED) -> None: + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED uptime sensor.""" super().__init__( - entry_id, - wled, - f"{wled.device.info.name} Uptime", - "mdi:clock-outline", - None, - "uptime", + coordinator=coordinator, enabled_default=False, + entry_id=entry_id, + icon="mdi:clock-outline", + key="uptime", + name=f"{coordinator.data.info.name} Uptime", ) + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) + return uptime.replace(microsecond=0).isoformat() + @property def device_class(self) -> Optional[str]: """Return the class of this sensor.""" return DEVICE_CLASS_TIMESTAMP - async def _wled_update(self) -> None: - """Update WLED uptime sensor.""" - uptime = utcnow() - timedelta(seconds=self.wled.device.info.uptime) - self._state = uptime.replace(microsecond=0).isoformat() - class WLEDFreeHeapSensor(WLEDSensor): """Defines a WLED free heap sensor.""" - def __init__(self, entry_id: str, wled: WLED) -> None: + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED free heap sensor.""" super().__init__( - entry_id, - wled, - f"{wled.device.info.name} Free Memory", - "mdi:memory", - DATA_BYTES, - "free_heap", + coordinator=coordinator, enabled_default=False, + entry_id=entry_id, + icon="mdi:memory", + key="free_heap", + name=f"{coordinator.data.info.name} Free Memory", + unit_of_measurement=DATA_BYTES, ) - async def _wled_update(self) -> None: - """Update WLED uptime sensor.""" - self._state = self.wled.device.info.free_heap + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.info.free_heap diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index dcb41a1e49b..85a1f261d94 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -1,21 +1,18 @@ """Support for WLED switches.""" import logging -from typing import Any, Callable, List - -from wled import WLED, WLEDError +from typing import Any, Callable, Dict, List, Optional from homeassistant.components.switch import SwitchDevice from homeassistant.config_entries import ConfigEntry from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType -from . import WLEDDeviceEntity +from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler from .const import ( ATTR_DURATION, ATTR_FADE, ATTR_TARGET_BRIGHTNESS, ATTR_UDP_PORT, - DATA_WLED_CLIENT, DOMAIN, ) @@ -30,12 +27,12 @@ async def async_setup_entry( async_add_entities: Callable[[List[Entity], bool], None], ) -> None: """Set up WLED switch based on a config entry.""" - wled: WLED = hass.data[DOMAIN][entry.entry_id][DATA_WLED_CLIENT] + coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] switches = [ - WLEDNightlightSwitch(entry.entry_id, wled), - WLEDSyncSendSwitch(entry.entry_id, wled), - WLEDSyncReceiveSwitch(entry.entry_id, wled), + WLEDNightlightSwitch(entry.entry_id, coordinator), + WLEDSyncSendSwitch(entry.entry_id, coordinator), + WLEDSyncReceiveSwitch(entry.entry_id, coordinator), ] async_add_entities(switches, True) @@ -44,132 +41,127 @@ class WLEDSwitch(WLEDDeviceEntity, SwitchDevice): """Defines a WLED switch.""" def __init__( - self, entry_id: str, wled: WLED, name: str, icon: str, key: str + self, + *, + entry_id: str, + coordinator: WLEDDataUpdateCoordinator, + name: str, + icon: str, + key: str, ) -> None: """Initialize WLED switch.""" self._key = key - self._state = False - super().__init__(entry_id, wled, name, icon) + super().__init__( + entry_id=entry_id, coordinator=coordinator, name=name, icon=icon + ) @property def unique_id(self) -> str: """Return the unique ID for this sensor.""" - return f"{self.wled.device.info.mac_address}_{self._key}" - - @property - def is_on(self) -> bool: - """Return the state of the switch.""" - return self._state - - async def async_turn_off(self, **kwargs: Any) -> None: - """Turn off the switch.""" - try: - await self._wled_turn_off() - self._state = False - except WLEDError: - _LOGGER.error("An error occurred while turning off WLED switch.") - self._available = False - self.async_schedule_update_ha_state() - - async def _wled_turn_off(self) -> None: - """Turn off the switch.""" - raise NotImplementedError() - - async def async_turn_on(self, **kwargs: Any) -> None: - """Turn on the switch.""" - try: - await self._wled_turn_on() - self._state = True - except WLEDError: - _LOGGER.error("An error occurred while turning on WLED switch") - self._available = False - self.async_schedule_update_ha_state() - - async def _wled_turn_on(self) -> None: - """Turn on the switch.""" - raise NotImplementedError() + return f"{self.coordinator.data.info.mac_address}_{self._key}" class WLEDNightlightSwitch(WLEDSwitch): """Defines a WLED nightlight switch.""" - def __init__(self, entry_id: str, wled: WLED) -> None: + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" super().__init__( - entry_id, - wled, - f"{wled.device.info.name} Nightlight", - "mdi:weather-night", - "nightlight", + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:weather-night", + key="nightlight", + name=f"{coordinator.data.info.name} Nightlight", ) - async def _wled_turn_off(self) -> None: - """Turn off the WLED nightlight switch.""" - await self.wled.nightlight(on=False) - - async def _wled_turn_on(self) -> None: - """Turn on the WLED nightlight switch.""" - await self.wled.nightlight(on=True) - - async def _wled_update(self) -> None: - """Update WLED entity.""" - self._state = self.wled.device.state.nightlight.on - self._attributes = { - ATTR_DURATION: self.wled.device.state.nightlight.duration, - ATTR_FADE: self.wled.device.state.nightlight.fade, - ATTR_TARGET_BRIGHTNESS: self.wled.device.state.nightlight.target_brightness, + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return { + ATTR_DURATION: self.coordinator.data.state.nightlight.duration, + ATTR_FADE: self.coordinator.data.state.nightlight.fade, + ATTR_TARGET_BRIGHTNESS: self.coordinator.data.state.nightlight.target_brightness, } + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.nightlight.on) + + @wled_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn off the WLED nightlight switch.""" + await self.coordinator.wled.nightlight(on=False) + + @wled_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn on the WLED nightlight switch.""" + await self.coordinator.wled.nightlight(on=True) + class WLEDSyncSendSwitch(WLEDSwitch): """Defines a WLED sync send switch.""" - def __init__(self, entry_id: str, wled: WLED) -> None: + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" super().__init__( - entry_id, - wled, - f"{wled.device.info.name} Sync Send", - "mdi:upload-network-outline", - "sync_send", + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:upload-network-outline", + key="sync_send", + name=f"{coordinator.data.info.name} Sync Send", ) - async def _wled_turn_off(self) -> None: + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return {ATTR_UDP_PORT: self.coordinator.data.info.udp_port} + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.sync.send) + + @wled_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the WLED sync send switch.""" - await self.wled.sync(send=False) + await self.coordinator.wled.sync(send=False) - async def _wled_turn_on(self) -> None: + @wled_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the WLED sync send switch.""" - await self.wled.sync(send=True) - - async def _wled_update(self) -> None: - """Update WLED entity.""" - self._state = self.wled.device.state.sync.send - self._attributes = {ATTR_UDP_PORT: self.wled.device.info.udp_port} + await self.coordinator.wled.sync(send=True) class WLEDSyncReceiveSwitch(WLEDSwitch): """Defines a WLED sync receive switch.""" - def __init__(self, entry_id: str, wled: WLED): + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator): """Initialize WLED sync receive switch.""" super().__init__( - entry_id, - wled, - f"{wled.device.info.name} Sync Receive", - "mdi:download-network-outline", - "sync_receive", + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:download-network-outline", + key="sync_receive", + name=f"{coordinator.data.info.name} Sync Receive", ) - async def _wled_turn_off(self) -> None: + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return {ATTR_UDP_PORT: self.coordinator.data.info.udp_port} + + @property + def is_on(self) -> bool: + """Return the state of the switch.""" + return bool(self.coordinator.data.state.sync.receive) + + @wled_exception_handler + async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the WLED sync receive switch.""" - await self.wled.sync(receive=False) + await self.coordinator.wled.sync(receive=False) - async def _wled_turn_on(self) -> None: + @wled_exception_handler + async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the WLED sync receive switch.""" - await self.wled.sync(receive=True) - - async def _wled_update(self) -> None: - """Update WLED entity.""" - self._state = self.wled.device.state.sync.receive - self._attributes = {ATTR_UDP_PORT: self.wled.device.info.udp_port} + await self.coordinator.wled.sync(receive=True) diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index 723f96db00d..d287ba6014a 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -1,10 +1,8 @@ """Tests for the WLED integration.""" import aiohttp -from asynctest import patch from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY -from homeassistant.const import STATE_ON from homeassistant.core import HomeAssistant from tests.components.wled import init_integration @@ -39,30 +37,3 @@ async def test_setting_unique_id(hass, aioclient_mock): assert hass.data[DOMAIN] assert entry.unique_id == "aabbccddeeff" - - -async def test_interval_update( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test the WLED configuration entry unloading.""" - entry = await init_integration(hass, aioclient_mock, skip_setup=True) - - interval_action = False - - def async_track_time_interval(hass, action, interval): - nonlocal interval_action - interval_action = action - - with patch( - "homeassistant.components.wled.async_track_time_interval", - new=async_track_time_interval, - ): - await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - assert interval_action - await interval_action() # pylint: disable=not-callable - await hass.async_block_till_done() - - state = hass.states.get("light.wled_rgb_light") - assert state.state == STATE_ON diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 3c439e71c90..3a03b93af30 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -1,5 +1,6 @@ """Tests for the WLED light platform.""" import aiohttp +from asynctest.mock import patch from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -23,7 +24,6 @@ from homeassistant.const import ( ATTR_ICON, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, STATE_ON, STATE_UNAVAILABLE, ) @@ -79,82 +79,98 @@ async def test_rgb_light_state( async def test_switch_change_state( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog ) -> None: """Test the change of state of the WLED switches.""" await init_integration(hass, aioclient_mock) - state = hass.states.get("light.wled_rgb_light") - assert state.state == STATE_ON + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + on=False, segment_id=0, transition=50, + ) - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_TRANSITION: 5}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light") - assert state.state == STATE_OFF + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_BRIGHTNESS: 42, + ATTR_EFFECT: "Chase", + ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_RGB_COLOR: [255, 0, 0], + ATTR_TRANSITION: 5, + }, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + brightness=42, + color_primary=(255, 0, 0), + effect="Chase", + on=True, + segment_id=0, + transition=50, + ) - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_BRIGHTNESS: 42, - ATTR_EFFECT: "Chase", - ATTR_ENTITY_ID: "light.wled_rgb_light", - ATTR_RGB_COLOR: [255, 0, 0], - ATTR_TRANSITION: 5, - }, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("light.wled_rgb_light") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_BRIGHTNESS) == 42 - assert state.attributes.get(ATTR_EFFECT) == "Chase" - assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgb_light", ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + color_primary=(255, 159, 70), on=True, segment_id=0, + ) async def test_light_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the WLED lights.""" + aioclient_mock.post("http://example.local:80/json/state", text="", status=400) + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_ON + assert "Invalid response from API" in caplog.text + + +async def test_light_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test error handling of the WLED switches.""" aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) await init_integration(hass, aioclient_mock) - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "light.wled_rgb_light"}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light") - assert state.state == STATE_UNAVAILABLE + with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.wled_rgb_light"}, + blocking=True, + ) + await hass.async_block_till_done() - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgb_light_1"}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("light.wled_rgb_light_1") - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("light.wled_rgb_light") + assert state.state == STATE_UNAVAILABLE async def test_rgbw_light( @@ -168,45 +184,42 @@ async def test_rgbw_light( assert state.attributes.get(ATTR_HS_COLOR) == (0.0, 100.0) assert state.attributes.get(ATTR_WHITE_VALUE) == 139 - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400}, - blocking=True, - ) - await hass.async_block_till_done() + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_COLOR_TEMP: 400}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + on=True, segment_id=0, color_primary=(255, 159, 70, 139), + ) - state = hass.states.get("light.wled_rgbw_light") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) - assert state.attributes.get(ATTR_WHITE_VALUE) == 139 + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100}, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + color_primary=(255, 0, 0, 100), on=True, segment_id=0, + ) - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "light.wled_rgbw_light", ATTR_WHITE_VALUE: 100}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("light.wled_rgbw_light") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (28.874, 72.522) - assert state.attributes.get(ATTR_WHITE_VALUE) == 100 - - await hass.services.async_call( - LIGHT_DOMAIN, - SERVICE_TURN_ON, - { - ATTR_ENTITY_ID: "light.wled_rgbw_light", - ATTR_RGB_COLOR: (255, 255, 255), - ATTR_WHITE_VALUE: 100, - }, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("light.wled_rgbw_light") - assert state.state == STATE_ON - assert state.attributes.get(ATTR_HS_COLOR) == (0, 0) - assert state.attributes.get(ATTR_WHITE_VALUE) == 100 + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.wled_rgbw_light", + ATTR_RGB_COLOR: (255, 255, 255), + ATTR_WHITE_VALUE: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + color_primary=(0, 0, 0, 100), on=True, segment_id=0, + ) diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 2dc11801712..5b315c87e9e 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -1,5 +1,6 @@ """Tests for the WLED switch platform.""" import aiohttp +from asynctest.mock import patch from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.wled.const import ( @@ -71,117 +72,105 @@ async def test_switch_change_state( await init_integration(hass, aioclient_mock) # Nightlight - state = hass.states.get("switch.wled_rgb_light_nightlight") - assert state.state == STATE_OFF + with patch("wled.WLED.nightlight") as nightlight_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + nightlight_mock.assert_called_once_with(on=True) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.wled_rgb_light_nightlight") - assert state.state == STATE_ON - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.wled_rgb_light_nightlight") - assert state.state == STATE_OFF + with patch("wled.WLED.nightlight") as nightlight_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + nightlight_mock.assert_called_once_with(on=False) # Sync send - state = hass.states.get("switch.wled_rgb_light_sync_send") - assert state.state == STATE_OFF + with patch("wled.WLED.sync") as sync_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + sync_mock.assert_called_once_with(send=True) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.wled_rgb_light_sync_send") - assert state.state == STATE_ON - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.wled_rgb_light_sync_send") - assert state.state == STATE_OFF + with patch("wled.WLED.sync") as sync_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, + blocking=True, + ) + await hass.async_block_till_done() + sync_mock.assert_called_once_with(send=False) # Sync receive - state = hass.states.get("switch.wled_rgb_light_sync_receive") - assert state.state == STATE_ON + with patch("wled.WLED.sync") as sync_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + sync_mock.assert_called_once_with(receive=False) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.wled_rgb_light_sync_receive") - assert state.state == STATE_OFF - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, - blocking=True, - ) - await hass.async_block_till_done() - - state = hass.states.get("switch.wled_rgb_light_sync_receive") - assert state.state == STATE_ON + with patch("wled.WLED.sync") as sync_mock: + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, + blocking=True, + ) + await hass.async_block_till_done() + sync_mock.assert_called_once_with(receive=True) async def test_switch_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the WLED switches.""" + aioclient_mock.post("http://example.local:80/json/state", text="", status=400) + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() + + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_OFF + assert "Invalid response from API" in caplog.text + + +async def test_switch_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test error handling of the WLED switches.""" aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) await init_integration(hass, aioclient_mock) - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("switch.wled_rgb_light_nightlight") - assert state.state == STATE_UNAVAILABLE + with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "switch.wled_rgb_light_nightlight"}, + blocking=True, + ) + await hass.async_block_till_done() - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_ON, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_send"}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("switch.wled_rgb_light_sync_send") - assert state.state == STATE_UNAVAILABLE - - await hass.services.async_call( - SWITCH_DOMAIN, - SERVICE_TURN_OFF, - {ATTR_ENTITY_ID: "switch.wled_rgb_light_sync_receive"}, - blocking=True, - ) - await hass.async_block_till_done() - state = hass.states.get("switch.wled_rgb_light_sync_receive") - assert state.state == STATE_UNAVAILABLE + state = hass.states.get("switch.wled_rgb_light_nightlight") + assert state.state == STATE_UNAVAILABLE From f3fed5647ee211d50f156fa898abb95bc2718cf2 Mon Sep 17 00:00:00 2001 From: smega Date: Fri, 13 Mar 2020 14:29:49 +0000 Subject: [PATCH 030/431] Extend rtorrent sensor functionality (#32353) * Extend rtorrent sensor functionality. * Remove blank line from end of file. * After using black formatter. * Update sensor.py using snake_case for variable names. * Update PR by using true value in condition. --- homeassistant/components/rtorrent/sensor.py | 44 +++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/homeassistant/components/rtorrent/sensor.py b/homeassistant/components/rtorrent/sensor.py index c6833fcfda0..cd27b33271f 100644 --- a/homeassistant/components/rtorrent/sensor.py +++ b/homeassistant/components/rtorrent/sensor.py @@ -21,12 +21,24 @@ _LOGGER = logging.getLogger(__name__) SENSOR_TYPE_CURRENT_STATUS = "current_status" SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed" SENSOR_TYPE_UPLOAD_SPEED = "upload_speed" +SENSOR_TYPE_ALL_TORRENTS = "all_torrents" +SENSOR_TYPE_STOPPED_TORRENTS = "stopped_torrents" +SENSOR_TYPE_COMPLETE_TORRENTS = "complete_torrents" +SENSOR_TYPE_UPLOADING_TORRENTS = "uploading_torrents" +SENSOR_TYPE_DOWNLOADING_TORRENTS = "downloading_torrents" +SENSOR_TYPE_ACTIVE_TORRENTS = "active_torrents" DEFAULT_NAME = "rtorrent" SENSOR_TYPES = { SENSOR_TYPE_CURRENT_STATUS: ["Status", None], SENSOR_TYPE_DOWNLOAD_SPEED: ["Down Speed", DATA_RATE_KILOBYTES_PER_SECOND], SENSOR_TYPE_UPLOAD_SPEED: ["Up Speed", DATA_RATE_KILOBYTES_PER_SECOND], + SENSOR_TYPE_ALL_TORRENTS: ["All Torrents", None], + SENSOR_TYPE_STOPPED_TORRENTS: ["Stopped Torrents", None], + SENSOR_TYPE_COMPLETE_TORRENTS: ["Complete Torrents", None], + SENSOR_TYPE_UPLOADING_TORRENTS: ["Uploading Torrents", None], + SENSOR_TYPE_DOWNLOADING_TORRENTS: ["Downloading Torrents", None], + SENSOR_TYPE_ACTIVE_TORRENTS: ["Active Torrents", None], } PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -102,6 +114,11 @@ class RTorrentSensor(Entity): multicall = xmlrpc.client.MultiCall(self.client) multicall.throttle.global_up.rate() multicall.throttle.global_down.rate() + multicall.d.multicall2("", "main") + multicall.d.multicall2("", "stopped") + multicall.d.multicall2("", "complete") + multicall.d.multicall2("", "seeding", "d.up.rate=") + multicall.d.multicall2("", "leeching", "d.down.rate=") try: self.data = multicall() @@ -113,6 +130,21 @@ class RTorrentSensor(Entity): upload = self.data[0] download = self.data[1] + all_torrents = self.data[2] + stopped_torrents = self.data[3] + complete_torrents = self.data[4] + + uploading_torrents = 0 + for up_torrent in self.data[5]: + if up_torrent[0]: + uploading_torrents += 1 + + downloading_torrents = 0 + for down_torrent in self.data[6]: + if down_torrent[0]: + downloading_torrents += 1 + + active_torrents = uploading_torrents + downloading_torrents if self.type == SENSOR_TYPE_CURRENT_STATUS: if self.data: @@ -132,3 +164,15 @@ class RTorrentSensor(Entity): self._state = format_speed(download) elif self.type == SENSOR_TYPE_UPLOAD_SPEED: self._state = format_speed(upload) + elif self.type == SENSOR_TYPE_ALL_TORRENTS: + self._state = len(all_torrents) + elif self.type == SENSOR_TYPE_STOPPED_TORRENTS: + self._state = len(stopped_torrents) + elif self.type == SENSOR_TYPE_COMPLETE_TORRENTS: + self._state = len(complete_torrents) + elif self.type == SENSOR_TYPE_UPLOADING_TORRENTS: + self._state = uploading_torrents + elif self.type == SENSOR_TYPE_DOWNLOADING_TORRENTS: + self._state = downloading_torrents + elif self.type == SENSOR_TYPE_ACTIVE_TORRENTS: + self._state = active_torrents From 86f61b8e55bd44b6b0e046991c20bc5cd25957ee Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Fri, 13 Mar 2020 11:50:16 -0400 Subject: [PATCH 031/431] Fix camera.options to camera.stream_options. (#32767) --- homeassistant/components/camera/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/camera/__init__.py b/homeassistant/components/camera/__init__.py index 5d9bc99f945..6bbf30b000e 100644 --- a/homeassistant/components/camera/__init__.py +++ b/homeassistant/components/camera/__init__.py @@ -141,7 +141,7 @@ async def async_request_stream(hass, entity_id, fmt): source, fmt=fmt, keepalive=camera_prefs.preload_stream, - options=camera.options, + options=camera.stream_options, ) From e2a113a2def6a86aa38a17e5d1a2c90bc760febc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Mar 2020 12:07:53 -0500 Subject: [PATCH 032/431] Bump py-august to 0.25.0 (#32769) Fixes a bug in the conversion to async where code validation failed. --- homeassistant/components/august/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/august/manifest.json b/homeassistant/components/august/manifest.json index ca757ae5ad3..f1085b81554 100644 --- a/homeassistant/components/august/manifest.json +++ b/homeassistant/components/august/manifest.json @@ -3,7 +3,7 @@ "name": "August", "documentation": "https://www.home-assistant.io/integrations/august", "requirements": [ - "py-august==0.24.0" + "py-august==0.25.0" ], "dependencies": [ "configurator" diff --git a/requirements_all.txt b/requirements_all.txt index 848b3acc38a..feba237661b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1088,7 +1088,7 @@ pushover_complete==1.1.1 pwmled==1.5.0 # homeassistant.components.august -py-august==0.24.0 +py-august==0.25.0 # homeassistant.components.canary py-canary==0.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 475f24266a5..80107b299a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -400,7 +400,7 @@ pure-python-adb==0.2.2.dev0 pushbullet.py==0.11.0 # homeassistant.components.august -py-august==0.24.0 +py-august==0.25.0 # homeassistant.components.canary py-canary==0.5.0 From 6bd55011a874ea01ad2ff031e35746c9f4360759 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 13 Mar 2020 19:55:53 +0100 Subject: [PATCH 033/431] Check if panel url used and delay dashboard reg till start (#32771) * Check if panel url used and delay dashboard reg till start * move storage_dashboard_changed * fix tests --- homeassistant/components/frontend/__init__.py | 3 +- homeassistant/components/lovelace/__init__.py | 53 ++++++++++--------- .../components/lovelace/dashboard.py | 5 +- tests/components/frontend/test_init.py | 11 +--- tests/components/lovelace/test_dashboard.py | 8 +++ 5 files changed, 42 insertions(+), 38 deletions(-) diff --git a/homeassistant/components/frontend/__init__.py b/homeassistant/components/frontend/__init__.py index 1e3dea98619..d9a39ce5726 100644 --- a/homeassistant/components/frontend/__init__.py +++ b/homeassistant/components/frontend/__init__.py @@ -276,8 +276,7 @@ async def async_setup(hass, config): hass.http.app.router.register_resource(IndexView(repo_path, hass)) - for panel in ("kiosk", "states", "profile"): - async_register_built_in_panel(hass, panel) + async_register_built_in_panel(hass, "profile") # To smooth transition to new urls, add redirects to new urls of dev tools # Added June 27, 2019. Can be removed in 2021. diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 23e8a14e511..8ed5e1abfbb 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import frontend -from homeassistant.const import CONF_FILENAME +from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_START from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv @@ -127,25 +127,12 @@ async def async_setup(hass, config): # We store a dictionary mapping url_path: config. None is the default. "dashboards": {None: default_config}, "resources": resource_collection, + "yaml_dashboards": config[DOMAIN].get(CONF_DASHBOARDS, {}), } if hass.config.safe_mode: return True - # Process YAML dashboards - for url_path, dashboard_conf in config[DOMAIN].get(CONF_DASHBOARDS, {}).items(): - # For now always mode=yaml - config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) - hass.data[DOMAIN]["dashboards"][url_path] = config - - try: - _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) - except ValueError: - _LOGGER.warning("Panel url path %s is not unique", url_path) - - # Process storage dashboards - dashboards_collection = dashboard.DashboardsCollection(hass) - async def storage_dashboard_changed(change_type, item_id, item): """Handle a storage dashboard change.""" url_path = item[CONF_URL_PATH] @@ -180,16 +167,34 @@ async def async_setup(hass, config): except ValueError: _LOGGER.warning("Failed to %s panel %s from storage", change_type, url_path) - dashboards_collection.async_add_listener(storage_dashboard_changed) - await dashboards_collection.async_load() + async def async_setup_dashboards(event): + """Register dashboards on startup.""" + # Process YAML dashboards + for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): + # For now always mode=yaml + config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) + hass.data[DOMAIN]["dashboards"][url_path] = config - collection.StorageCollectionWebsocket( - dashboards_collection, - "lovelace/dashboards", - "dashboard", - STORAGE_DASHBOARD_CREATE_FIELDS, - STORAGE_DASHBOARD_UPDATE_FIELDS, - ).async_setup(hass, create_list=False) + try: + _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) + except ValueError: + _LOGGER.warning("Panel url path %s is not unique", url_path) + + # Process storage dashboards + dashboards_collection = dashboard.DashboardsCollection(hass) + + dashboards_collection.async_add_listener(storage_dashboard_changed) + await dashboards_collection.async_load() + + collection.StorageCollectionWebsocket( + dashboards_collection, + "lovelace/dashboards", + "dashboard", + STORAGE_DASHBOARD_CREATE_FIELDS, + STORAGE_DASHBOARD_UPDATE_FIELDS, + ).async_setup(hass, create_list=False) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_setup_dashboards) return True diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 514f1eb87b6..f32ac2ed1ff 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -6,6 +6,7 @@ import time import voluptuous as vol +from homeassistant.components.frontend import DATA_PANELS from homeassistant.const import CONF_FILENAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError @@ -231,8 +232,8 @@ class DashboardsCollection(collection.StorageCollection): async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" - if data[CONF_URL_PATH] in self.hass.data[DOMAIN]["dashboards"]: - raise vol.Invalid("Dashboard url path needs to be unique") + if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]: + raise vol.Invalid("Panel url path needs to be unique") return self.CREATE_SCHEMA(data) diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index 627bf23341d..36243972fb6 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -106,15 +106,6 @@ async def test_we_cannot_POST_to_root(mock_http_client): assert resp.status == 405 -async def test_states_routes(mock_http_client): - """All served by index.""" - resp = await mock_http_client.get("/states") - assert resp.status == 200 - - resp = await mock_http_client.get("/states/group.existing") - assert resp.status == 200 - - async def test_themes_api(hass, hass_ws_client): """Test that /api/themes returns correct data.""" assert await async_setup_component(hass, "frontend", CONFIG_THEMES) @@ -217,7 +208,7 @@ async def test_missing_themes(hass, hass_ws_client): async def test_extra_urls(mock_http_client_with_urls, mock_onboarded): """Test that extra urls are loaded.""" - resp = await mock_http_client_with_urls.get("/states?latest") + resp = await mock_http_client_with_urls.get("/lovelace?latest") assert resp.status == 200 text = await resp.text() assert text.find('href="https://domain.com/my_extra_url.html"') >= 0 diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 21a44bc771d..9bfe3da38c9 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -5,6 +5,7 @@ import pytest from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard +from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component from tests.common import async_capture_events, get_system_health_info @@ -223,6 +224,8 @@ async def test_dashboard_from_yaml(hass, hass_ws_client, url_path): } }, ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() assert hass.data[frontend.DATA_PANELS]["test-panel"].config == {"mode": "yaml"} assert hass.data[frontend.DATA_PANELS]["test-panel-no-sidebar"].config == { "mode": "yaml" @@ -306,6 +309,8 @@ async def test_dashboard_from_yaml(hass, hass_ws_client, url_path): async def test_storage_dashboards(hass, hass_ws_client, hass_storage): """Test we load lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "storage"} client = await hass_ws_client(hass) @@ -450,6 +455,9 @@ async def test_websocket_list_dashboards(hass, hass_ws_client): }, ) + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + client = await hass_ws_client(hass) # Create a storage dashboard From 460857a765d0745efaaa3a144b634906e28f1cfb Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 13 Mar 2020 21:43:39 +0100 Subject: [PATCH 034/431] Updated frontend to 20200313.0 (#32777) --- 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 c3cf353dba1..2817b744d72 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==20200312.0" + "home-assistant-frontend==20200313.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index e7c374f6367..471af972755 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200312.0 +home-assistant-frontend==20200313.0 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index feba237661b..b312e7fc9c8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -696,7 +696,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200312.0 +home-assistant-frontend==20200313.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 80107b299a7..3719522358d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200312.0 +home-assistant-frontend==20200313.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.8 From 607cdfdd32b69a92f93bc9ff11605b20daec1060 Mon Sep 17 00:00:00 2001 From: Andre Lengwenus Date: Fri, 13 Mar 2020 22:27:53 +0100 Subject: [PATCH 035/431] Bump pypck to 0.6.4 (#32775) --- homeassistant/components/lcn/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/lcn/manifest.json b/homeassistant/components/lcn/manifest.json index 80a15ef6bd6..58353697d18 100644 --- a/homeassistant/components/lcn/manifest.json +++ b/homeassistant/components/lcn/manifest.json @@ -2,7 +2,7 @@ "domain": "lcn", "name": "LCN", "documentation": "https://www.home-assistant.io/integrations/lcn", - "requirements": ["pypck==0.6.3"], + "requirements": ["pypck==0.6.4"], "dependencies": [], "codeowners": ["@alengwenus"] } diff --git a/requirements_all.txt b/requirements_all.txt index b312e7fc9c8..1e9354dfb64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1467,7 +1467,7 @@ pyownet==0.10.0.post1 pypca==0.0.7 # homeassistant.components.lcn -pypck==0.6.3 +pypck==0.6.4 # homeassistant.components.pjlink pypjlink2==1.2.0 From fd5895118eba13710b64f4d31df7b409af60d3bf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Mar 2020 16:38:14 -0500 Subject: [PATCH 036/431] =?UTF-8?q?Do=20not=20fail=20when=20a=20user=20has?= =?UTF-8?q?=20a=20controller=20with=20shared=20access=20on=E2=80=A6=20(#32?= =?UTF-8?q?756)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/rachio/__init__.py | 36 +++++++++++++++++---- 1 file changed, 29 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 1b24f4e0071..faa0b9da379 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -159,10 +159,25 @@ class RachioPerson: data = rachio.person.get(self._id) assert int(data[0][KEY_STATUS]) == 200, "User ID error" self.username = data[1][KEY_USERNAME] - self._controllers = [ - RachioIro(self._hass, self.rachio, controller) - for controller in data[1][KEY_DEVICES] - ] + devices = data[1][KEY_DEVICES] + self._controllers = [] + for controller in devices: + webhooks = self.rachio.notification.getDeviceWebhook(controller[KEY_ID])[1] + # The API does not provide a way to tell if a controller is shared + # or if they are the owner. To work around this problem we fetch the webooks + # before we setup the device so we can skip it instead of failing. + # webhooks are normally a list, however if there is an error + # rachio hands us back a dict + if isinstance(webhooks, dict): + _LOGGER.error( + "Failed to add rachio controller '%s' because of an error: %s", + controller[KEY_NAME], + webhooks.get("error", "Unknown Error"), + ) + continue + self._controllers.append( + RachioIro(self._hass, self.rachio, controller, webhooks) + ) _LOGGER.info('Using Rachio API as user "%s"', self.username) @property @@ -179,7 +194,7 @@ class RachioPerson: class RachioIro: """Represent a Rachio Iro.""" - def __init__(self, hass, rachio, data): + def __init__(self, hass, rachio, data, webhooks): """Initialize a Rachio device.""" self.hass = hass self.rachio = rachio @@ -187,6 +202,7 @@ class RachioIro: self._name = data[KEY_NAME] self._zones = data[KEY_ZONES] self._init_data = data + self._webhooks = webhooks _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) # Listen for all updates @@ -199,13 +215,19 @@ class RachioIro: # First delete any old webhooks that may have stuck around def _deinit_webhooks(event) -> None: """Stop getting updates from the Rachio API.""" - webhooks = self.rachio.notification.getDeviceWebhook(self.controller_id)[1] - for webhook in webhooks: + if not self._webhooks: + # We fetched webhooks when we created the device, however if we call _init_webhooks + # again we need to fetch again + self._webhooks = self.rachio.notification.getDeviceWebhook( + self.controller_id + )[1] + for webhook in self._webhooks: if ( webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) or webhook[KEY_ID] == current_webhook_id ): self.rachio.notification.deleteWebhook(webhook[KEY_ID]) + self._webhooks = None _deinit_webhooks(None) From 9db3900cff5ff746ef564406b4c1f9e27f9f871c Mon Sep 17 00:00:00 2001 From: Slava Date: Fri, 13 Mar 2020 23:42:47 +0100 Subject: [PATCH 037/431] Add brightness state to emulated hue when devices support only color temp and brightness (#31834) --- homeassistant/components/emulated_hue/hue_api.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 9a2d624a55f..06a57960898 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -677,7 +677,11 @@ def entity_to_json(config, entity): retval["type"] = "Color temperature light" retval["modelid"] = "HASS312" retval["state"].update( - {HUE_API_STATE_COLORMODE: "ct", HUE_API_STATE_CT: state[STATE_COLOR_TEMP]} + { + HUE_API_STATE_COLORMODE: "ct", + HUE_API_STATE_CT: state[STATE_COLOR_TEMP], + HUE_API_STATE_BRI: state[STATE_BRIGHTNESS], + } ) elif entity_features & ( SUPPORT_BRIGHTNESS From 628f77f8f28e2e8dedcf6a862baa00175550cace Mon Sep 17 00:00:00 2001 From: Aidan Timson Date: Fri, 13 Mar 2020 22:58:14 +0000 Subject: [PATCH 038/431] Fix onvif error with non ptz cameras (#32783) --- homeassistant/components/onvif/camera.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index 614eb4e6556..ce241f779b1 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -375,7 +375,7 @@ class ONVIFHassCamera(Camera): def setup_ptz(self): """Set up PTZ if available.""" _LOGGER.debug("Setting up the ONVIF PTZ service") - if self._camera.get_service("ptz") is None: + if self._camera.get_service("ptz", create=False) is None: _LOGGER.debug("PTZ is not available") else: self._ptz_service = self._camera.create_ptz_service() From aa972b000531af6d186ff69b07df2b4871e81825 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Fri, 13 Mar 2020 19:17:50 -0400 Subject: [PATCH 039/431] Fix handling of attribute reports in ZHA sensors and binary sensors (#32776) * Update sensor tests. * Update light tests. * Update binary_sensor tests. * Update cover tests. * Update device tracker tests. * Update fan tests. * Update lock tests. * Update switch tests. * add sensor attr to sensors * add sensor attr to binary sensors * cleanup extra var Co-authored-by: Alexei Chetroi --- homeassistant/components/zha/binary_sensor.py | 8 ++++ homeassistant/components/zha/sensor.py | 11 ++++++ tests/components/zha/common.py | 17 +++++++++ tests/components/zha/test_binary_sensor.py | 14 ++----- tests/components/zha/test_cover.py | 14 ++----- tests/components/zha/test_device_tracker.py | 13 ++----- tests/components/zha/test_fan.py | 13 ++----- tests/components/zha/test_light.py | 25 ++++-------- tests/components/zha/test_lock.py | 16 ++------ tests/components/zha/test_sensor.py | 38 +++++++------------ tests/components/zha/test_switch.py | 12 ++---- 11 files changed, 77 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index a40bd62e83c..6c88f3e1013 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -64,6 +64,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class BinarySensor(ZhaEntity, BinarySensorDevice): """ZHA BinarySensor.""" + SENSOR_ATTR = None DEVICE_CLASS = None def __init__(self, unique_id, zha_device, channels, **kwargs): @@ -105,6 +106,8 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" + if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name: + return self._state = bool(value) self.async_write_ha_state() @@ -121,6 +124,7 @@ class BinarySensor(ZhaEntity, BinarySensorDevice): class Accelerometer(BinarySensor): """ZHA BinarySensor.""" + SENSOR_ATTR = "acceleration" DEVICE_CLASS = DEVICE_CLASS_MOVING @@ -128,6 +132,7 @@ class Accelerometer(BinarySensor): class Occupancy(BinarySensor): """ZHA BinarySensor.""" + SENSOR_ATTR = "occupancy" DEVICE_CLASS = DEVICE_CLASS_OCCUPANCY @@ -135,6 +140,7 @@ class Occupancy(BinarySensor): class Opening(BinarySensor): """ZHA BinarySensor.""" + SENSOR_ATTR = "on_off" DEVICE_CLASS = DEVICE_CLASS_OPENING @@ -142,6 +148,8 @@ class Opening(BinarySensor): class IASZone(BinarySensor): """ZHA IAS BinarySensor.""" + SENSOR_ATTR = "zone_status" + async def get_device_class(self) -> None: """Get the HA device class from the channel.""" zone_type = await self._channel.get_attribute_value("zone_type") diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 3953db27f20..8182fdcabcf 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -83,6 +83,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class Sensor(ZhaEntity): """Base ZHA sensor.""" + SENSOR_ATTR = None _decimals = 1 _device_class = None _divisor = 1 @@ -126,6 +127,8 @@ class Sensor(ZhaEntity): @callback def async_set_state(self, attr_id, attr_name, value): """Handle state update from channel.""" + if self.SENSOR_ATTR is None or self.SENSOR_ATTR != attr_name: + return if value is not None: value = self.formatter(value) self._state = value @@ -154,6 +157,7 @@ class Sensor(ZhaEntity): class AnalogInput(Sensor): """Sensor that displays analog input values.""" + SENSOR_ATTR = "present_value" pass @@ -161,6 +165,7 @@ class AnalogInput(Sensor): class Battery(Sensor): """Battery sensor of power configuration cluster.""" + SENSOR_ATTR = "battery_percentage_remaining" _device_class = DEVICE_CLASS_BATTERY _unit = UNIT_PERCENTAGE @@ -198,6 +203,7 @@ class Battery(Sensor): class ElectricalMeasurement(Sensor): """Active power measurement.""" + SENSOR_ATTR = "active_power" _device_class = DEVICE_CLASS_POWER _divisor = 10 _unit = POWER_WATT @@ -232,6 +238,7 @@ class Text(Sensor): class Humidity(Sensor): """Humidity sensor.""" + SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_HUMIDITY _divisor = 100 _unit = UNIT_PERCENTAGE @@ -241,6 +248,7 @@ class Humidity(Sensor): class Illuminance(Sensor): """Illuminance Sensor.""" + SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_ILLUMINANCE _unit = "lx" @@ -254,6 +262,7 @@ class Illuminance(Sensor): class SmartEnergyMetering(Sensor): """Metering sensor.""" + SENSOR_ATTR = "instantaneous_demand" _device_class = DEVICE_CLASS_POWER def formatter(self, value): @@ -270,6 +279,7 @@ class SmartEnergyMetering(Sensor): class Pressure(Sensor): """Pressure sensor.""" + SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_PRESSURE _decimals = 0 _unit = "hPa" @@ -279,6 +289,7 @@ class Pressure(Sensor): class Temperature(Sensor): """Temperature Sensor.""" + SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 _unit = TEMP_CELSIUS diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 3eb6f407f32..3753136d59d 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -102,6 +102,23 @@ def make_attribute(attrid, value, status=0): return attr +def send_attribute_report(hass, cluster, attrid, value): + """Send a single attribute report.""" + return send_attributes_report(hass, cluster, {attrid: value}) + + +async def send_attributes_report(hass, cluster: int, attributes: dict): + """Cause the sensor to receive an attribute report from the network. + + This is to simulate the normal device communication that happens when a + device is paired to the zigbee network. + """ + attrs = [make_attribute(attrid, value) for attrid, value in attributes.items()] + hdr = make_zcl_header(zcl_f.Command.Report_Attributes) + cluster.handle_message(hdr, [attrs]) + await hass.async_block_till_done() + + async def find_entity_id(domain, zha_device, hass): """Find the entity id under the testing. diff --git a/tests/components/zha/test_binary_sensor.py b/tests/components/zha/test_binary_sensor.py index a22bfa54dae..730c7c844f2 100644 --- a/tests/components/zha/test_binary_sensor.py +++ b/tests/components/zha/test_binary_sensor.py @@ -2,7 +2,6 @@ import pytest import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.security as security -import zigpy.zcl.foundation as zcl_f from homeassistant.components.binary_sensor import DOMAIN from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE @@ -11,8 +10,7 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attributes_report, ) DEVICE_IAS = { @@ -36,17 +34,11 @@ DEVICE_OCCUPANCY = { async def async_test_binary_sensor_on_off(hass, cluster, entity_id): """Test getting on and off messages for binary sensors.""" # binary sensor on - attr = make_attribute(0, 1) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2}) assert hass.states.get(entity_id).state == STATE_ON # binary sensor off - attr.value.value = 0 - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_cover.py b/tests/components/zha/test_cover.py index 3ece16d8116..188ddf69a23 100644 --- a/tests/components/zha/test_cover.py +++ b/tests/components/zha/test_cover.py @@ -14,8 +14,7 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attributes_report, ) from tests.common import mock_coro @@ -64,19 +63,12 @@ async def test_cover(m1, hass, zha_device_joined_restored, zigpy_cover_device): await async_enable_traffic(hass, [zha_device]) await hass.async_block_till_done() - attr = make_attribute(8, 100) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() - # test that the state has changed from unavailable to off + await send_attributes_report(hass, cluster, {0: 0, 8: 100, 1: 1}) assert hass.states.get(entity_id).state == STATE_CLOSED # test to see if it opens - attr = make_attribute(8, 0) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {0: 1, 8: 0, 1: 100}) assert hass.states.get(entity_id).state == STATE_OPEN # close from UI diff --git a/tests/components/zha/test_device_tracker.py b/tests/components/zha/test_device_tracker.py index 3782cdc09a7..330153e5f8c 100644 --- a/tests/components/zha/test_device_tracker.py +++ b/tests/components/zha/test_device_tracker.py @@ -4,7 +4,6 @@ import time import pytest import zigpy.zcl.clusters.general as general -import zigpy.zcl.foundation as zcl_f from homeassistant.components.device_tracker import DOMAIN, SOURCE_TYPE_ROUTER from homeassistant.components.zha.core.registries import ( @@ -17,8 +16,7 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attributes_report, ) from tests.common import async_fire_time_changed @@ -66,12 +64,9 @@ async def test_device_tracker(hass, zha_device_joined_restored, zigpy_device_dt) assert hass.states.get(entity_id).state == STATE_NOT_HOME # turn state flip - attr = make_attribute(0x0020, 23) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - - attr = make_attribute(0x0021, 200) - cluster.handle_message(hdr, [[attr]]) + await send_attributes_report( + hass, cluster, {0x0000: 0, 0x0020: 23, 0x0021: 200, 0x0001: 2} + ) zigpy_device_dt.last_seen = time.time() + 10 next_update = dt_util.utcnow() + timedelta(seconds=30) diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 0cf3e3e954d..5011a847a4e 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -3,7 +3,6 @@ from unittest.mock import call import pytest import zigpy.zcl.clusters.hvac as hvac -import zigpy.zcl.foundation as zcl_f from homeassistant.components import fan from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED @@ -20,8 +19,7 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attributes_report, ) @@ -52,16 +50,11 @@ async def test_fan(hass, zha_device_joined_restored, zigpy_device): assert hass.states.get(entity_id).state == STATE_OFF # turn on at fan - attr = make_attribute(0, 1) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 2, 0: 1, 2: 3}) assert hass.states.get(entity_id).state == STATE_ON # turn off at fan - attr.value.value = 0 - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 6f5bd23e297..f27bd329bdb 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -19,8 +19,7 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attributes_report, ) from tests.common import async_fire_time_changed @@ -190,26 +189,18 @@ async def test_light( async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light - attr = make_attribute(0, 1) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3}) assert hass.states.get(entity_id).state == STATE_ON # turn off at light - attr.value.value = 0 - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3}) assert hass.states.get(entity_id).state == STATE_OFF async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light - attr = make_attribute(0, 1) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2}) assert hass.states.get(entity_id).state == STATE_ON @@ -316,10 +307,10 @@ async def async_test_level_on_off_from_hass( async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected_state): """Test dimmer functionality from the light.""" - attr = make_attribute(0, level) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + + await send_attributes_report( + hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} + ) assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: diff --git a/tests/components/zha/test_lock.py b/tests/components/zha/test_lock.py index 0442ea497d7..86ec266ffa2 100644 --- a/tests/components/zha/test_lock.py +++ b/tests/components/zha/test_lock.py @@ -10,12 +10,7 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components.lock import DOMAIN from homeassistant.const import STATE_LOCKED, STATE_UNAVAILABLE, STATE_UNLOCKED -from .common import ( - async_enable_traffic, - find_entity_id, - make_attribute, - make_zcl_header, -) +from .common import async_enable_traffic, find_entity_id, send_attributes_report from tests.common import mock_coro @@ -58,16 +53,11 @@ async def test_lock(hass, lock): assert hass.states.get(entity_id).state == STATE_UNLOCKED # set state to locked - attr = make_attribute(0, 1) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2}) assert hass.states.get(entity_id).state == STATE_LOCKED # set state to unlocked - attr.value.value = 2 - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 0, 0: 2, 2: 3}) assert hass.states.get(entity_id).state == STATE_UNLOCKED # lock from HA diff --git a/tests/components/zha/test_sensor.py b/tests/components/zha/test_sensor.py index fce882c6949..50b85f5720f 100644 --- a/tests/components/zha/test_sensor.py +++ b/tests/components/zha/test_sensor.py @@ -6,7 +6,6 @@ import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.homeautomation as homeautomation import zigpy.zcl.clusters.measurement as measurement import zigpy.zcl.clusters.smartenergy as smartenergy -import zigpy.zcl.foundation as zcl_f from homeassistant.components.sensor import DOMAIN import homeassistant.config as config_util @@ -28,38 +27,41 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attribute_report, + send_attributes_report, ) async def async_test_humidity(hass, cluster, entity_id): """Test humidity sensor.""" - await send_attribute_report(hass, cluster, 0, 1000) + await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 100}) assert_state(hass, entity_id, "10.0", UNIT_PERCENTAGE) async def async_test_temperature(hass, cluster, entity_id): """Test temperature sensor.""" - await send_attribute_report(hass, cluster, 0, 2900) + await send_attributes_report(hass, cluster, {1: 1, 0: 2900, 2: 100}) assert_state(hass, entity_id, "29.0", "°C") async def async_test_pressure(hass, cluster, entity_id): """Test pressure sensor.""" - await send_attribute_report(hass, cluster, 0, 1000) + await send_attributes_report(hass, cluster, {1: 1, 0: 1000, 2: 10000}) + assert_state(hass, entity_id, "1000", "hPa") + + await send_attributes_report(hass, cluster, {0: 1000, 20: -1, 16: 10000}) assert_state(hass, entity_id, "1000", "hPa") async def async_test_illuminance(hass, cluster, entity_id): """Test illuminance sensor.""" - await send_attribute_report(hass, cluster, 0, 10) + await send_attributes_report(hass, cluster, {1: 1, 0: 10, 2: 20}) assert_state(hass, entity_id, "1.0", "lx") async def async_test_metering(hass, cluster, entity_id): """Test metering sensor.""" - await send_attribute_report(hass, cluster, 1024, 12345) + await send_attributes_report(hass, cluster, {1025: 1, 1024: 12345, 1026: 100}) assert_state(hass, entity_id, "12345.0", "unknown") @@ -73,17 +75,17 @@ async def async_test_electrical_measurement(hass, cluster, entity_id): new_callable=mock.PropertyMock, ) as divisor_mock: divisor_mock.return_value = 1 - await send_attribute_report(hass, cluster, 1291, 100) + await send_attributes_report(hass, cluster, {0: 1, 1291: 100, 10: 1000}) assert_state(hass, entity_id, "100", "W") - await send_attribute_report(hass, cluster, 1291, 99) + await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 1000}) assert_state(hass, entity_id, "99", "W") divisor_mock.return_value = 10 - await send_attribute_report(hass, cluster, 1291, 1000) + await send_attributes_report(hass, cluster, {0: 1, 1291: 1000, 10: 5000}) assert_state(hass, entity_id, "100", "W") - await send_attribute_report(hass, cluster, 1291, 99) + await send_attributes_report(hass, cluster, {0: 1, 1291: 99, 10: 5000}) assert_state(hass, entity_id, "9.9", "W") @@ -141,18 +143,6 @@ async def test_sensor( await async_test_rejoin(hass, zigpy_device, [cluster], (report_count,)) -async def send_attribute_report(hass, cluster, attrid, value): - """Cause the sensor to receive an attribute report from the network. - - This is to simulate the normal device communication that happens when a - device is paired to the zigbee network. - """ - attr = make_attribute(attrid, value) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() - - def assert_state(hass, entity_id, state, unit_of_measurement): """Check that the state is what is expected. diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 22ceb629009..98f661cc1ab 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -12,8 +12,7 @@ from .common import ( async_enable_traffic, async_test_rejoin, find_entity_id, - make_attribute, - make_zcl_header, + send_attributes_report, ) from tests.common import mock_coro @@ -53,16 +52,11 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): assert hass.states.get(entity_id).state == STATE_OFF # turn on at switch - attr = make_attribute(0, 1) - hdr = make_zcl_header(zcl_f.Command.Report_Attributes) - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 2}) assert hass.states.get(entity_id).state == STATE_ON # turn off at switch - attr.value.value = 0 - cluster.handle_message(hdr, [[attr]]) - await hass.async_block_till_done() + await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 2}) assert hass.states.get(entity_id).state == STATE_OFF # turn on from HA From 750ed2facd6b079c1a83cf865bc0440c4497a1f4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 13 Mar 2020 23:47:47 -0500 Subject: [PATCH 040/431] =?UTF-8?q?Optimize=20is=5Fentity=5Fexposed=20=20i?= =?UTF-8?q?n=20emulated=5Fhue=20by=20removing=20deprec=E2=80=A6=20(#32718)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * emulated_hue: Optimize is_entity_exposed Switch all list transversals in is_entity_exposed to hash lookups get_deprecated is now only called if explict_expose is set This funciton was iterating multiple lists per enitity every time there was an update. It was responsible for a chunk of execution time when there are large number of entities in home assistant. * Complete deprecation of ATTR_EMULATED_HUE attribute * Complete deprecation of ATTR_EMULATED_HUE attribute (remove const) * Remove ATTR_EMULATED_HUE_HIDDEN and Rewrite tests --- .../components/emulated_hue/__init__.py | 44 ++++++--------- tests/components/emulated_hue/test_hue_api.py | 53 ++++--------------- 2 files changed, 27 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index 0a358c6e894..6b234a9df7b 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -9,7 +9,6 @@ from homeassistant.components.http import real_ip from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import HomeAssistantError import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.deprecation import get_deprecated from homeassistant.util.json import load_json, save_json from .hue_api import ( @@ -91,9 +90,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -ATTR_EMULATED_HUE = "emulated_hue" ATTR_EMULATED_HUE_NAME = "emulated_hue_name" -ATTR_EMULATED_HUE_HIDDEN = "emulated_hue_hidden" async def async_setup(hass, yaml_config): @@ -220,7 +217,9 @@ class Config: # Get domains that are exposed by default when expose_by_default is # True - self.exposed_domains = conf.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) + self.exposed_domains = set( + conf.get(CONF_EXPOSED_DOMAINS, DEFAULT_EXPOSED_DOMAINS) + ) # Calculated effective advertised IP and port for network isolation self.advertise_ip = conf.get(CONF_ADVERTISE_IP) or self.host_ip_addr @@ -229,6 +228,12 @@ class Config: self.entities = conf.get(CONF_ENTITIES, {}) + self._entities_with_hidden_attr_in_config = dict() + for entity_id in self.entities: + hidden_value = self.entities[entity_id].get(CONF_ENTITY_HIDDEN, None) + if hidden_value is not None: + self._entities_with_hidden_attr_in_config[entity_id] = hidden_value + def entity_id_to_number(self, entity_id): """Get a unique number for the entity id.""" if self.type == TYPE_ALEXA: @@ -280,35 +285,18 @@ class Config: # Ignore entities that are views return False - domain = entity.domain.lower() - explicit_expose = entity.attributes.get(ATTR_EMULATED_HUE, None) - explicit_hidden = entity.attributes.get(ATTR_EMULATED_HUE_HIDDEN, None) - - if ( - entity.entity_id in self.entities - and CONF_ENTITY_HIDDEN in self.entities[entity.entity_id] - ): - explicit_hidden = self.entities[entity.entity_id][CONF_ENTITY_HIDDEN] - - if explicit_expose is True or explicit_hidden is False: - expose = True - elif explicit_expose is False or explicit_hidden is True: - expose = False - else: - expose = None - get_deprecated( - entity.attributes, ATTR_EMULATED_HUE_HIDDEN, ATTR_EMULATED_HUE, None - ) - domain_exposed_by_default = ( - self.expose_by_default and domain in self.exposed_domains - ) + if entity.entity_id in self._entities_with_hidden_attr_in_config: + return not self._entities_with_hidden_attr_in_config[entity.entity_id] + if not self.expose_by_default: + return False # Expose an entity if the entity's domain is exposed by default and # the configuration doesn't explicitly exclude it from being # exposed, or if the entity is explicitly exposed - is_default_exposed = domain_exposed_by_default and expose is not False + if entity.domain in self.exposed_domains: + return True - return is_default_exposed or expose + return False def _load_json(filename): diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 30b715c136b..76f1a224c1f 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -130,51 +130,9 @@ def hass_hue(loop, hass): ) ) - # Kitchen light is explicitly excluded from being exposed - kitchen_light_entity = hass.states.get("light.kitchen_lights") - attrs = dict(kitchen_light_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE] = False - hass.states.async_set( - kitchen_light_entity.entity_id, kitchen_light_entity.state, attributes=attrs - ) - # create a lamp without brightness support hass.states.async_set("light.no_brightness", "on", {}) - # Ceiling Fan is explicitly excluded from being exposed - ceiling_fan_entity = hass.states.get("fan.ceiling_fan") - attrs = dict(ceiling_fan_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE_HIDDEN] = True - hass.states.async_set( - ceiling_fan_entity.entity_id, ceiling_fan_entity.state, attributes=attrs - ) - - # Expose the script - script_entity = hass.states.get("script.set_kitchen_light") - attrs = dict(script_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE] = True - hass.states.async_set( - script_entity.entity_id, script_entity.state, attributes=attrs - ) - - # Expose cover - cover_entity = hass.states.get("cover.living_room_window") - attrs = dict(cover_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE_HIDDEN] = False - hass.states.async_set(cover_entity.entity_id, cover_entity.state, attributes=attrs) - - # Expose Hvac - hvac_entity = hass.states.get("climate.hvac") - attrs = dict(hvac_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE_HIDDEN] = False - hass.states.async_set(hvac_entity.entity_id, hvac_entity.state, attributes=attrs) - - # Expose HeatPump - hp_entity = hass.states.get("climate.heatpump") - attrs = dict(hp_entity.attributes) - attrs[emulated_hue.ATTR_EMULATED_HUE_HIDDEN] = False - hass.states.async_set(hp_entity.entity_id, hp_entity.state, attributes=attrs) - return hass @@ -188,7 +146,18 @@ def hue_client(loop, hass_hue, aiohttp_client): emulated_hue.CONF_TYPE: emulated_hue.TYPE_ALEXA, emulated_hue.CONF_ENTITIES: { "light.bed_light": {emulated_hue.CONF_ENTITY_HIDDEN: True}, + # Kitchen light is explicitly excluded from being exposed + "light.kitchen_lights": {emulated_hue.CONF_ENTITY_HIDDEN: True}, + # Ceiling Fan is explicitly excluded from being exposed + "fan.ceiling_fan": {emulated_hue.CONF_ENTITY_HIDDEN: True}, + # Expose the script + "script.set_kitchen_light": {emulated_hue.CONF_ENTITY_HIDDEN: False}, + # Expose cover "cover.living_room_window": {emulated_hue.CONF_ENTITY_HIDDEN: False}, + # Expose Hvac + "climate.hvac": {emulated_hue.CONF_ENTITY_HIDDEN: False}, + # Expose HeatPump + "climate.heatpump": {emulated_hue.CONF_ENTITY_HIDDEN: False}, }, }, ) From 5dd031af17b059419176212a1667088610f42224 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Fri, 13 Mar 2020 23:55:21 -0500 Subject: [PATCH 041/431] Optimize directv client initialization (#32706) * Optimize directv client initialization. * Update config_flow.py * Update media_player.py * Update media_player.py * Update __init__.py * Update __init__.py * Update __init__.py * Update __init__.py * Update test_media_player.py * Update __init__.py * Update media_player.py * Update test_media_player.py * Update media_player.py * Update test_media_player.py * Update config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update test_config_flow.py * Update __init__.py * Update test_config_flow.py * Update test_config_flow.py * Update test_media_player.py * Update test_media_player.py * Update __init__.py * Update __init__.py * Update __init__.py --- homeassistant/components/directv/__init__.py | 2 +- .../components/directv/config_flow.py | 6 +- .../components/directv/media_player.py | 27 ++----- tests/components/directv/__init__.py | 23 ++++-- tests/components/directv/test_config_flow.py | 20 ++--- tests/components/directv/test_media_player.py | 80 +++++-------------- 6 files changed, 50 insertions(+), 108 deletions(-) diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index d9f3f171992..fc7bb78989a 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -32,7 +32,7 @@ def get_dtv_data( hass: HomeAssistant, host: str, port: int = DEFAULT_PORT, client_addr: str = "0" ) -> dict: """Retrieve a DIRECTV instance, locations list, and version info for the receiver device.""" - dtv = DIRECTV(host, port, client_addr) + dtv = DIRECTV(host, port, client_addr, determine_state=False) locations = dtv.get_locations() version_info = dtv.get_version() diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index 27ddf2cda7b..d1b3a6cbe62 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -29,8 +29,7 @@ def validate_input(data: Dict) -> Dict: Data has the keys from DATA_SCHEMA with values provided by the user. """ - # directpy does IO in constructor. - dtv = DIRECTV(data["host"], DEFAULT_PORT) + dtv = DIRECTV(data["host"], DEFAULT_PORT, determine_state=False) version_info = dtv.get_version() return { @@ -76,8 +75,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return self._show_form(errors) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") - errors["base"] = ERROR_UNKNOWN - return self._show_form(errors) + return self.async_abort(reason=ERROR_UNKNOWN) await self.async_set_unique_id(info["receiver_id"]) self._abort_if_unique_id_configured() diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index c1c227d319d..b04ef9fed68 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -83,22 +83,6 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def get_dtv_instance( - host: str, port: int = DEFAULT_PORT, client_addr: str = "0" -) -> DIRECTV: - """Retrieve a DIRECTV instance for the receiver or client device.""" - try: - return DIRECTV(host, port, client_addr) - except RequestException as exception: - _LOGGER.debug( - "Request exception %s trying to retrieve DIRECTV instance for client address %s on device %s", - exception, - client_addr, - host, - ) - return None - - async def async_setup_entry( hass: HomeAssistantType, entry: ConfigEntry, @@ -114,16 +98,15 @@ async def async_setup_entry( continue if loc["clientAddr"] != "0": - # directpy does IO in constructor. - dtv = await hass.async_add_executor_job( - get_dtv_instance, entry.data[CONF_HOST], DEFAULT_PORT, loc["clientAddr"] + dtv = DIRECTV( + entry.data[CONF_HOST], + DEFAULT_PORT, + loc["clientAddr"], + determine_state=False, ) else: dtv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] - if not dtv: - continue - entities.append( DirecTvDevice( str.title(loc["locationName"]), loc["clientAddr"], dtv, version_info, diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index d7f79c76be5..876b1e311ab 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -1,4 +1,6 @@ """Tests for the DirecTV component.""" +from DirectPy import DIRECTV + from homeassistant.components.directv.const import DOMAIN from homeassistant.const import CONF_HOST from homeassistant.helpers.typing import HomeAssistantType @@ -94,18 +96,23 @@ MOCK_GET_VERSION = { } -class MockDirectvClass: +class MockDirectvClass(DIRECTV): """A fake DirecTV DVR device.""" - def __init__(self, ip, port=8080, clientAddr="0"): + def __init__(self, ip, port=8080, clientAddr="0", determine_state=False): """Initialize the fake DirecTV device.""" - self._host = ip - self._port = port - self._device = clientAddr - self._standby = True - self._play = False + super().__init__( + ip=ip, port=port, clientAddr=clientAddr, determine_state=determine_state, + ) - self.attributes = LIVE + self._play = False + self._standby = True + + if self.clientAddr == CLIENT_ADDRESS: + self.attributes = RECORDING + self._standby = False + else: + self.attributes = LIVE def get_locations(self): """Mock for get_locations method.""" diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index 5516b61cd46..bd5d8b83419 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -114,9 +114,7 @@ async def test_form_cannot_connect(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.config_flow.DIRECTV.get_version", + "tests.components.directv.test_config_flow.MockDirectvClass.get_version", side_effect=RequestException, ) as mock_validate_input: result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) @@ -135,15 +133,13 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.config_flow.DIRECTV.get_version", + "tests.components.directv.test_config_flow.MockDirectvClass.get_version", side_effect=Exception, ) as mock_validate_input: result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "unknown"} + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" await hass.async_block_till_done() assert len(mock_validate_input.mock_calls) == 1 @@ -205,9 +201,7 @@ async def test_ssdp_discovery_confirm_abort(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.config_flow.DIRECTV.get_version", + "tests.components.directv.test_config_flow.MockDirectvClass.get_version", side_effect=RequestException, ) as mock_validate_input: result = await async_configure_flow(hass, result["flow_id"], {}) @@ -227,9 +221,7 @@ async def test_ssdp_discovery_confirm_unknown_error(hass: HomeAssistantType) -> ) with patch( - "homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.config_flow.DIRECTV.get_version", + "tests.components.directv.test_config_flow.MockDirectvClass.get_version", side_effect=Exception, ) as mock_validate_input: result = await async_configure_flow(hass, result["flow_id"], {}) diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index 9c06164c309..f7cf63355a8 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -54,9 +54,7 @@ from homeassistant.util import dt as dt_util from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.directv import ( - CLIENT_ADDRESS, DOMAIN, - HOST, MOCK_GET_LOCATIONS_MULTIPLE, RECORDING, MockDirectvClass, @@ -70,15 +68,6 @@ MAIN_ENTITY_ID = f"{MP_DOMAIN}.main_dvr" # pylint: disable=redefined-outer-name -@fixture -def client_dtv() -> MockDirectvClass: - """Fixture for a client device.""" - mocked_dtv = MockDirectvClass(HOST, clientAddr=CLIENT_ADDRESS) - mocked_dtv.attributes = RECORDING - mocked_dtv._standby = False # pylint: disable=protected-access - return mocked_dtv - - @fixture def mock_now() -> datetime: """Fixture for dtutil.now.""" @@ -93,34 +82,19 @@ async def setup_directv(hass: HomeAssistantType) -> MockConfigEntry: return await setup_integration(hass) -async def setup_directv_with_instance_error(hass: HomeAssistantType) -> MockConfigEntry: +async def setup_directv_with_locations(hass: HomeAssistantType) -> MockConfigEntry: """Set up mock DirecTV integration.""" with patch( - "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.DIRECTV.get_locations", + "tests.components.directv.test_media_player.MockDirectvClass.get_locations", return_value=MOCK_GET_LOCATIONS_MULTIPLE, - ), patch( - "homeassistant.components.directv.media_player.get_dtv_instance", - return_value=None, ): - return await setup_integration(hass) - - -async def setup_directv_with_locations( - hass: HomeAssistantType, client_dtv: MockDirectvClass, -) -> MockConfigEntry: - """Set up mock DirecTV integration.""" - with patch( - "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.DIRECTV.get_locations", - return_value=MOCK_GET_LOCATIONS_MULTIPLE, - ), patch( - "homeassistant.components.directv.media_player.get_dtv_instance", - return_value=client_dtv, - ): - return await setup_integration(hass) + with patch( + "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, + ), patch( + "homeassistant.components.directv.media_player.DIRECTV", + new=MockDirectvClass, + ): + return await setup_integration(hass) async def async_turn_on( @@ -204,27 +178,17 @@ async def test_setup(hass: HomeAssistantType) -> None: assert hass.states.get(MAIN_ENTITY_ID) -async def test_setup_with_multiple_locations( - hass: HomeAssistantType, client_dtv: MockDirectvClass -) -> None: +async def test_setup_with_multiple_locations(hass: HomeAssistantType) -> None: """Test setup with basic config with client location.""" - await setup_directv_with_locations(hass, client_dtv) + await setup_directv_with_locations(hass) assert hass.states.get(MAIN_ENTITY_ID) assert hass.states.get(CLIENT_ENTITY_ID) -async def test_setup_with_instance_error(hass: HomeAssistantType) -> None: - """Test setup with basic config with client location that results in instance error.""" - await setup_directv_with_instance_error(hass) - - assert hass.states.get(MAIN_ENTITY_ID) - assert hass.states.async_entity_ids(MP_DOMAIN) == [MAIN_ENTITY_ID] - - -async def test_unique_id(hass: HomeAssistantType, client_dtv: MockDirectvClass) -> None: +async def test_unique_id(hass: HomeAssistantType) -> None: """Test unique id.""" - await setup_directv_with_locations(hass, client_dtv) + await setup_directv_with_locations(hass) entity_registry = await hass.helpers.entity_registry.async_get_registry() @@ -235,11 +199,9 @@ async def test_unique_id(hass: HomeAssistantType, client_dtv: MockDirectvClass) assert client.unique_id == "2CA17D1CD30X" -async def test_supported_features( - hass: HomeAssistantType, client_dtv: MockDirectvClass -) -> None: +async def test_supported_features(hass: HomeAssistantType) -> None: """Test supported features.""" - await setup_directv_with_locations(hass, client_dtv) + await setup_directv_with_locations(hass) # Features supported for main DVR state = hass.states.get(MAIN_ENTITY_ID) @@ -269,10 +231,10 @@ async def test_supported_features( async def test_check_attributes( - hass: HomeAssistantType, mock_now: dt_util.dt.datetime, client_dtv: MockDirectvClass + hass: HomeAssistantType, mock_now: dt_util.dt.datetime ) -> None: """Test attributes.""" - await setup_directv_with_locations(hass, client_dtv) + await setup_directv_with_locations(hass) next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): @@ -321,10 +283,10 @@ async def test_check_attributes( async def test_main_services( - hass: HomeAssistantType, mock_now: dt_util.dt.datetime, client_dtv: MockDirectvClass + hass: HomeAssistantType, mock_now: dt_util.dt.datetime ) -> None: """Test the different services.""" - await setup_directv_with_locations(hass, client_dtv) + await setup_directv(hass) next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): @@ -373,10 +335,10 @@ async def test_main_services( async def test_available( - hass: HomeAssistantType, mock_now: dt_util.dt.datetime, client_dtv: MockDirectvClass + hass: HomeAssistantType, mock_now: dt_util.dt.datetime ) -> None: """Test available status.""" - entry = await setup_directv_with_locations(hass, client_dtv) + entry = await setup_directv(hass) next_update = mock_now + timedelta(minutes=5) with patch("homeassistant.util.dt.utcnow", return_value=next_update): From 743166d284819443f3d1f305dc92456aabe45308 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 14 Mar 2020 05:58:32 +0100 Subject: [PATCH 042/431] Fix brightness_pct in light device turn_on action (#32787) --- homeassistant/components/light/device_action.py | 10 +++++----- tests/components/light/test_device_action.py | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/light/device_action.py b/homeassistant/components/light/device_action.py index 5ee2785a700..5c534cc4150 100644 --- a/homeassistant/components/light/device_action.py +++ b/homeassistant/components/light/device_action.py @@ -15,7 +15,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.helpers import config_validation as cv, entity_registry from homeassistant.helpers.typing import ConfigType, TemplateVarsType -from . import ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS +from . import ATTR_BRIGHTNESS_PCT, ATTR_BRIGHTNESS_STEP_PCT, DOMAIN, SUPPORT_BRIGHTNESS TYPE_BRIGHTNESS_INCREASE = "brightness_increase" TYPE_BRIGHTNESS_DECREASE = "brightness_decrease" @@ -28,7 +28,7 @@ ACTION_SCHEMA = cv.DEVICE_ACTION_BASE_SCHEMA.extend( toggle_entity.DEVICE_ACTION_TYPES + [TYPE_BRIGHTNESS_INCREASE, TYPE_BRIGHTNESS_DECREASE] ), - vol.Optional(ATTR_BRIGHTNESS): vol.All( + vol.Optional(ATTR_BRIGHTNESS_PCT): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ), } @@ -57,8 +57,8 @@ async def async_call_action_from_config( data[ATTR_BRIGHTNESS_STEP_PCT] = 10 elif config[CONF_TYPE] == TYPE_BRIGHTNESS_DECREASE: data[ATTR_BRIGHTNESS_STEP_PCT] = -10 - elif ATTR_BRIGHTNESS in config: - data[ATTR_BRIGHTNESS] = config[ATTR_BRIGHTNESS] + elif ATTR_BRIGHTNESS_PCT in config: + data[ATTR_BRIGHTNESS_PCT] = config[ATTR_BRIGHTNESS_PCT] await hass.services.async_call( DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=context @@ -125,7 +125,7 @@ async def async_get_action_capabilities(hass: HomeAssistant, config: dict) -> di return { "extra_fields": vol.Schema( { - vol.Optional(ATTR_BRIGHTNESS): vol.All( + vol.Optional(ATTR_BRIGHTNESS_PCT): vol.All( vol.Coerce(int), vol.Range(min=0, max=100) ) } diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 610f61dea52..6cddfc15744 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -126,7 +126,7 @@ async def test_get_action_capabilities_brightness(hass, device_reg, entity_reg): expected_capabilities = { "extra_fields": [ { - "name": "brightness", + "name": "brightness_pct", "optional": True, "type": "integer", "valueMax": 100, @@ -218,7 +218,7 @@ async def test_action(hass, calls): "device_id": "", "entity_id": ent1.entity_id, "type": "turn_on", - "brightness": 75, + "brightness_pct": 75, }, }, ] @@ -273,11 +273,11 @@ async def test_action(hass, calls): assert len(turn_on_calls) == 3 assert turn_on_calls[2].data["entity_id"] == ent1.entity_id - assert turn_on_calls[2].data["brightness"] == 75 + assert turn_on_calls[2].data["brightness_pct"] == 75 hass.bus.async_fire("test_on") await hass.async_block_till_done() assert len(turn_on_calls) == 4 assert turn_on_calls[3].data["entity_id"] == ent1.entity_id - assert "brightness" not in turn_on_calls[3].data + assert "brightness_pct" not in turn_on_calls[3].data From 7737387efe8f592a913fe8c39a6991f9266a0b78 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 14 Mar 2020 00:46:17 -0500 Subject: [PATCH 043/431] Add config flow for rachio (#32757) * Do not fail when a user has a controller with shared access on their account * Add config flow for rachio Also discoverable via homekit * Update homeassistant/components/rachio/switch.py Co-Authored-By: Paulus Schoutsen * Split setting the default run time to an options flow Ensue the run time coming from yaml gets imported into the option flow Only get the schedule once at setup instead of each zone (was hitting rate limits) Add the config entry id to the end of the webhook so there is a unique hook per config entry Breakout the slew of exceptions rachiopy can throw into RachioAPIExceptions Remove the base url override as an option for the config flow Switch identifer for device_info to serial number Add connections to device_info (mac address) * rename to make pylint happy * Fix import of custom_url * claim rachio Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 1 + .../components/rachio/.translations/en.json | 31 ++++ homeassistant/components/rachio/__init__.py | 147 ++++++++++++------ .../components/rachio/binary_sensor.py | 40 +++-- .../components/rachio/config_flow.py | 127 +++++++++++++++ homeassistant/components/rachio/const.py | 42 +++++ homeassistant/components/rachio/manifest.json | 16 +- homeassistant/components/rachio/strings.json | 31 ++++ homeassistant/components/rachio/switch.py | 105 +++++++++---- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 1 + requirements_test_all.txt | 3 + tests/components/rachio/__init__.py | 1 + tests/components/rachio/test_config_flow.py | 104 +++++++++++++ 14 files changed, 561 insertions(+), 89 deletions(-) create mode 100644 homeassistant/components/rachio/.translations/en.json create mode 100644 homeassistant/components/rachio/config_flow.py create mode 100644 homeassistant/components/rachio/const.py create mode 100644 homeassistant/components/rachio/strings.json create mode 100644 tests/components/rachio/__init__.py create mode 100644 tests/components/rachio/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 8b85278b4bb..b0498c8a60b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -286,6 +286,7 @@ homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan homeassistant/components/qvr_pro/* @oblogic7 homeassistant/components/qwikswitch/* @kellerza +homeassistant/components/rachio/* @bdraco homeassistant/components/rainbird/* @konikvranik homeassistant/components/raincloud/* @vanstinator homeassistant/components/rainforest_eagle/* @gtdiehl @jcalbert diff --git a/homeassistant/components/rachio/.translations/en.json b/homeassistant/components/rachio/.translations/en.json new file mode 100644 index 00000000000..391320289db --- /dev/null +++ b/homeassistant/components/rachio/.translations/en.json @@ -0,0 +1,31 @@ +{ + "config": { + "title": "Rachio", + "step": { + "user": { + "title": "Connect to your Rachio device", + "description" : "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.", + "data": { + "api_key": "The API key for the Rachio account." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled." + } + } + } + } +} diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index faa0b9da379..67659c6ee4d 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -9,21 +9,36 @@ from rachiopy import Rachio import voluptuous as vol from homeassistant.components.http import HomeAssistantView +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send +from .const import ( + CONF_CUSTOM_URL, + CONF_MANUAL_RUN_MINS, + DEFAULT_MANUAL_RUN_MINS, + DOMAIN, + KEY_DEVICES, + KEY_ENABLED, + KEY_EXTERNAL_ID, + KEY_ID, + KEY_MAC_ADDRESS, + KEY_NAME, + KEY_SERIAL_NUMBER, + KEY_STATUS, + KEY_TYPE, + KEY_USERNAME, + KEY_ZONES, + RACHIO_API_EXCEPTIONS, +) + _LOGGER = logging.getLogger(__name__) -DOMAIN = "rachio" - SUPPORTED_DOMAINS = ["switch", "binary_sensor"] -# Manual run length -CONF_MANUAL_RUN_MINS = "manual_run_mins" -DEFAULT_MANUAL_RUN_MINS = 10 -CONF_CUSTOM_URL = "hass_url_override" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -39,23 +54,6 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -# Keys used in the API JSON -KEY_DEVICE_ID = "deviceId" -KEY_DEVICES = "devices" -KEY_ENABLED = "enabled" -KEY_EXTERNAL_ID = "externalId" -KEY_ID = "id" -KEY_NAME = "name" -KEY_ON = "on" -KEY_STATUS = "status" -KEY_SUBTYPE = "subType" -KEY_SUMMARY = "summary" -KEY_TYPE = "type" -KEY_URL = "url" -KEY_USERNAME = "username" -KEY_ZONE_ID = "zoneId" -KEY_ZONE_NUMBER = "zoneNumber" -KEY_ZONES = "zones" STATUS_ONLINE = "ONLINE" STATUS_OFFLINE = "OFFLINE" @@ -102,28 +100,69 @@ SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + "_zone" SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule" -def setup(hass, config) -> bool: - """Set up the Rachio component.""" +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the rachio component from YAML.""" - # Listen for incoming webhook connections - hass.http.register_view(RachioWebhookView()) + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SUPPORTED_DOMAINS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up the Rachio config entry.""" + + config = entry.data + options = entry.options + + # CONF_MANUAL_RUN_MINS can only come from a yaml import + if not options.get(CONF_MANUAL_RUN_MINS) and config.get(CONF_MANUAL_RUN_MINS): + options[CONF_MANUAL_RUN_MINS] = config[CONF_MANUAL_RUN_MINS] # Configure API - api_key = config[DOMAIN].get(CONF_API_KEY) + api_key = config.get(CONF_API_KEY) rachio = Rachio(api_key) # Get the URL of this server - custom_url = config[DOMAIN].get(CONF_CUSTOM_URL) + custom_url = config.get(CONF_CUSTOM_URL) hass_url = hass.config.api.base_url if custom_url is None else custom_url rachio.webhook_auth = secrets.token_hex() - rachio.webhook_url = hass_url + WEBHOOK_PATH + webhook_url_path = f"{WEBHOOK_PATH}-{entry.entry_id}" + rachio.webhook_url = f"{hass_url}{webhook_url_path}" # Get the API user try: - person = RachioPerson(hass, rachio, config[DOMAIN]) - except AssertionError as error: + person = await hass.async_add_executor_job(RachioPerson, hass, rachio, entry) + # Yes we really do get all these exceptions (hopefully rachiopy switches to requests) + # and there is not a reasonable timeout here so it can block for a long time + except RACHIO_API_EXCEPTIONS as error: _LOGGER.error("Could not reach the Rachio API: %s", error) - return False + raise ConfigEntryNotReady # Check for Rachio controller devices if not person.controllers: @@ -132,11 +171,15 @@ def setup(hass, config) -> bool: _LOGGER.info("%d Rachio device(s) found", len(person.controllers)) # Enable component - hass.data[DOMAIN] = person + hass.data[DOMAIN][entry.entry_id] = person + + # Listen for incoming webhook connections after the data is there + hass.http.register_view(RachioWebhookView(entry.entry_id, webhook_url_path)) - # Load platforms for component in SUPPORTED_DOMAINS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) return True @@ -144,12 +187,12 @@ def setup(hass, config) -> bool: class RachioPerson: """Represent a Rachio user.""" - def __init__(self, hass, rachio, config): + def __init__(self, hass, rachio, config_entry): """Create an object from the provided API instance.""" # Use API token to get user ID self._hass = hass self.rachio = rachio - self.config = config + self.config_entry = config_entry response = rachio.person.getInfo() assert int(response[0][KEY_STATUS]) == 200, "API key error" @@ -200,6 +243,8 @@ class RachioIro: self.rachio = rachio self._id = data[KEY_ID] self._name = data[KEY_NAME] + self._serial_number = data[KEY_SERIAL_NUMBER] + self._mac_address = data[KEY_MAC_ADDRESS] self._zones = data[KEY_ZONES] self._init_data = data self._webhooks = webhooks @@ -256,6 +301,16 @@ class RachioIro: """Return the Rachio API controller ID.""" return self._id + @property + def serial_number(self) -> str: + """Return the Rachio API controller serial number.""" + return self._serial_number + + @property + def mac_address(self) -> str: + """Return the Rachio API controller mac address.""" + return self._mac_address + @property def name(self) -> str: """Return the user-defined name of the controller.""" @@ -304,10 +359,14 @@ class RachioWebhookView(HomeAssistantView): } requires_auth = False # Handled separately - url = WEBHOOK_PATH - name = url[1:].replace("/", ":") - @asyncio.coroutine + def __init__(self, entry_id, webhook_url): + """Initialize the instance of the view.""" + self._entry_id = entry_id + self.url = webhook_url + self.name = webhook_url[1:].replace("/", ":") + _LOGGER.debug("Created webhook at url: %s, with name %s", self.url, self.name) + async def post(self, request) -> web.Response: """Handle webhook calls from the server.""" hass = request.app["hass"] @@ -315,7 +374,7 @@ class RachioWebhookView(HomeAssistantView): try: auth = data.get(KEY_EXTERNAL_ID, str()).split(":")[1] - assert auth == hass.data[DOMAIN].rachio.webhook_auth + assert auth == hass.data[DOMAIN][self._entry_id].rachio.webhook_auth except (AssertionError, IndexError): return web.Response(status=web.HTTPForbidden.status_code) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index f13eba59ac9..31a5cd889e9 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -3,33 +3,41 @@ from abc import abstractmethod import logging from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import dispatcher_connect from . import ( - DOMAIN as DOMAIN_RACHIO, - KEY_DEVICE_ID, - KEY_STATUS, - KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, STATUS_OFFLINE, STATUS_ONLINE, SUBTYPE_OFFLINE, SUBTYPE_ONLINE, ) +from .const import ( + DEFAULT_NAME, + DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_STATUS, + KEY_SUBTYPE, +) _LOGGER = logging.getLogger(__name__) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Rachio binary sensors.""" - devices = [] - for controller in hass.data[DOMAIN_RACHIO].controllers: - devices.append(RachioControllerOnlineBinarySensor(hass, controller)) - - add_entities(devices) + devices = await hass.async_add_executor_job(_create_devices, hass, config_entry) + async_add_entities(devices) _LOGGER.info("%d Rachio binary sensor(s) added", len(devices)) +def _create_devices(hass, config_entry): + devices = [] + for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers: + devices.append(RachioControllerOnlineBinarySensor(hass, controller)) + return devices + + class RachioControllerBinarySensor(BinarySensorDevice): """Represent a binary sensor that reflects a Rachio state.""" @@ -70,6 +78,18 @@ class RachioControllerBinarySensor(BinarySensorDevice): """Request the state from the API.""" pass + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN_RACHIO, self._controller.serial_number,)}, + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) + }, + "name": self._controller.name, + "manufacturer": DEFAULT_NAME, + } + @abstractmethod def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py new file mode 100644 index 00000000000..3d2a4c18ab2 --- /dev/null +++ b/homeassistant/components/rachio/config_flow.py @@ -0,0 +1,127 @@ +"""Config flow for Rachio integration.""" +import logging + +from rachiopy import Rachio +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_API_KEY +from homeassistant.core import callback + +from .const import ( + CONF_MANUAL_RUN_MINS, + DEFAULT_MANUAL_RUN_MINS, + KEY_ID, + KEY_STATUS, + KEY_USERNAME, + RACHIO_API_EXCEPTIONS, +) +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str}, extra=vol.ALLOW_EXTRA) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + rachio = Rachio(data[CONF_API_KEY]) + username = None + try: + data = await hass.async_add_executor_job(rachio.person.getInfo) + _LOGGER.debug("rachio.person.getInfo: %s", data) + if int(data[0][KEY_STATUS]) != 200: + raise InvalidAuth + + rachio_id = data[1][KEY_ID] + data = await hass.async_add_executor_job(rachio.person.get, rachio_id) + _LOGGER.debug("rachio.person.get: %s", data) + if int(data[0][KEY_STATUS]) != 200: + raise CannotConnect + + username = data[1][KEY_USERNAME] + # Yes we really do get all these exceptions (hopefully rachiopy switches to requests) + except RACHIO_API_EXCEPTIONS as error: + _LOGGER.error("Could not reach the Rachio API: %s", error) + raise CannotConnect + + # Return info that you want to store in the config entry. + return {"title": username} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Rachio.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + _LOGGER.debug("async_step_user: %s", user_input) + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + await self.async_set_unique_id(user_input[CONF_API_KEY]) + return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_homekit(self, homekit_info): + """Handle HomeKit discovery.""" + return await self.async_step_user() + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @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 Rachio.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """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_MANUAL_RUN_MINS, + default=self.config_entry.options.get( + CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS + ), + ): int + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py new file mode 100644 index 00000000000..2388ed283f1 --- /dev/null +++ b/homeassistant/components/rachio/const.py @@ -0,0 +1,42 @@ +"""Constants for rachio.""" + +import http.client +import ssl + +DEFAULT_NAME = "Rachio" + +DOMAIN = "rachio" + +CONF_CUSTOM_URL = "hass_url_override" +# Manual run length +CONF_MANUAL_RUN_MINS = "manual_run_mins" +DEFAULT_MANUAL_RUN_MINS = 10 + +# Keys used in the API JSON +KEY_DEVICE_ID = "deviceId" +KEY_IMAGE_URL = "imageUrl" +KEY_DEVICES = "devices" +KEY_ENABLED = "enabled" +KEY_EXTERNAL_ID = "externalId" +KEY_ID = "id" +KEY_NAME = "name" +KEY_ON = "on" +KEY_STATUS = "status" +KEY_SUBTYPE = "subType" +KEY_SUMMARY = "summary" +KEY_SERIAL_NUMBER = "serialNumber" +KEY_MAC_ADDRESS = "macAddress" +KEY_TYPE = "type" +KEY_URL = "url" +KEY_USERNAME = "username" +KEY_ZONE_ID = "zoneId" +KEY_ZONE_NUMBER = "zoneNumber" +KEY_ZONES = "zones" + +# Yes we really do get all these exceptions (hopefully rachiopy switches to requests) +RACHIO_API_EXCEPTIONS = ( + http.client.HTTPException, + ssl.SSLError, + OSError, + AssertionError, +) diff --git a/homeassistant/components/rachio/manifest.json b/homeassistant/components/rachio/manifest.json index fae640f9262..9b293ee5df2 100644 --- a/homeassistant/components/rachio/manifest.json +++ b/homeassistant/components/rachio/manifest.json @@ -2,7 +2,17 @@ "domain": "rachio", "name": "Rachio", "documentation": "https://www.home-assistant.io/integrations/rachio", - "requirements": ["rachiopy==0.1.3"], - "dependencies": ["http"], - "codeowners": [] + "requirements": [ + "rachiopy==0.1.3" + ], + "dependencies": [ + "http" + ], + "codeowners": ["@bdraco"], + "config_flow": true, + "homekit": { + "models": [ + "Rachio" + ] + } } diff --git a/homeassistant/components/rachio/strings.json b/homeassistant/components/rachio/strings.json new file mode 100644 index 00000000000..391320289db --- /dev/null +++ b/homeassistant/components/rachio/strings.json @@ -0,0 +1,31 @@ +{ + "config": { + "title": "Rachio", + "step": { + "user": { + "title": "Connect to your Rachio device", + "description" : "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.", + "data": { + "api_key": "The API key for the Rachio account." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled." + } + } + } + } +} diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index a3a4f6bcca1..7f76cd9042d 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -4,20 +4,10 @@ from datetime import timedelta import logging from homeassistant.components.switch import SwitchDevice +from homeassistant.helpers import device_registry from homeassistant.helpers.dispatcher import dispatcher_connect from . import ( - CONF_MANUAL_RUN_MINS, - DOMAIN as DOMAIN_RACHIO, - KEY_DEVICE_ID, - KEY_ENABLED, - KEY_ID, - KEY_NAME, - KEY_ON, - KEY_SUBTYPE, - KEY_SUMMARY, - KEY_ZONE_ID, - KEY_ZONE_NUMBER, SIGNAL_RACHIO_CONTROLLER_UPDATE, SIGNAL_RACHIO_ZONE_UPDATE, SUBTYPE_SLEEP_MODE_OFF, @@ -26,6 +16,22 @@ from . import ( SUBTYPE_ZONE_STARTED, SUBTYPE_ZONE_STOPPED, ) +from .const import ( + CONF_MANUAL_RUN_MINS, + DEFAULT_MANUAL_RUN_MINS, + DEFAULT_NAME, + DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_ENABLED, + KEY_ID, + KEY_IMAGE_URL, + KEY_NAME, + KEY_ON, + KEY_SUBTYPE, + KEY_SUMMARY, + KEY_ZONE_ID, + KEY_ZONE_NUMBER, +) _LOGGER = logging.getLogger(__name__) @@ -33,25 +39,30 @@ ATTR_ZONE_SUMMARY = "Summary" ATTR_ZONE_NUMBER = "Zone number" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Rachio switches.""" - manual_run_time = timedelta( - minutes=hass.data[DOMAIN_RACHIO].config.get(CONF_MANUAL_RUN_MINS) - ) - _LOGGER.info("Rachio run time is %s", str(manual_run_time)) - # Add all zones from all controllers as switches - devices = [] - for controller in hass.data[DOMAIN_RACHIO].controllers: - devices.append(RachioStandbySwitch(hass, controller)) - - for zone in controller.list_zones(): - devices.append(RachioZone(hass, controller, zone, manual_run_time)) - - add_entities(devices) + devices = await hass.async_add_executor_job(_create_devices, hass, config_entry) + async_add_entities(devices) _LOGGER.info("%d Rachio switch(es) added", len(devices)) +def _create_devices(hass, config_entry): + devices = [] + person = hass.data[DOMAIN_RACHIO][config_entry.entry_id] + # Fetch the schedule once at startup + # in order to avoid every zone doing it + for controller in person.controllers: + devices.append(RachioStandbySwitch(hass, controller)) + zones = controller.list_zones() + current_schedule = controller.current_schedule + _LOGGER.debug("Rachio setting up zones: %s", zones) + for zone in zones: + _LOGGER.debug("Rachio setting up zone: %s", zone) + devices.append(RachioZone(hass, person, controller, zone, current_schedule)) + return devices + + class RachioSwitch(SwitchDevice): """Represent a Rachio state that can be toggled.""" @@ -93,6 +104,18 @@ class RachioSwitch(SwitchDevice): # For this device self._handle_update(args, kwargs) + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN_RACHIO, self._controller.serial_number,)}, + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) + }, + "name": self._controller.name, + "manufacturer": DEFAULT_NAME, + } + @abstractmethod def _handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook data.""" @@ -153,15 +176,18 @@ class RachioStandbySwitch(RachioSwitch): class RachioZone(RachioSwitch): """Representation of one zone of sprinklers connected to the Rachio Iro.""" - def __init__(self, hass, controller, data, manual_run_time): + def __init__(self, hass, person, controller, data, current_schedule): """Initialize a new Rachio Zone.""" self._id = data[KEY_ID] self._zone_name = data[KEY_NAME] self._zone_number = data[KEY_ZONE_NUMBER] self._zone_enabled = data[KEY_ENABLED] - self._manual_run_time = manual_run_time + self._entity_picture = data.get(KEY_IMAGE_URL) + self._person = person self._summary = str() - super().__init__(controller) + self._current_schedule = current_schedule + super().__init__(controller, poll=False) + self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) # Listen for all zone updates dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update) @@ -195,6 +221,11 @@ class RachioZone(RachioSwitch): """Return whether the zone is allowed to run.""" return self._zone_enabled + @property + def entity_picture(self): + """Return the entity picture to use in the frontend, if any.""" + return self._entity_picture + @property def state_attributes(self) -> dict: """Return the optional state attributes.""" @@ -206,8 +237,18 @@ class RachioZone(RachioSwitch): self.turn_off() # Start this zone - self._controller.rachio.zone.start(self.zone_id, self._manual_run_time.seconds) - _LOGGER.debug("Watering %s on %s", self.name, self._controller.name) + manual_run_time = timedelta( + minutes=self._person.config_entry.options.get( + CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS + ) + ) + self._controller.rachio.zone.start(self.zone_id, manual_run_time.seconds) + _LOGGER.debug( + "Watering %s on %s for %s", + self.name, + self._controller.name, + str(manual_run_time), + ) def turn_off(self, **kwargs) -> None: """Stop watering all zones.""" @@ -215,8 +256,8 @@ class RachioZone(RachioSwitch): def _poll_update(self, data=None) -> bool: """Poll the API to check whether the zone is running.""" - schedule = self._controller.current_schedule - return self.zone_id == schedule.get(KEY_ZONE_ID) + self._current_schedule = self._controller.current_schedule + return self.zone_id == self._current_schedule.get(KEY_ZONE_ID) def _handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook zone data.""" diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index a7e9b63c1a5..c19e9fafbc0 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -80,6 +80,7 @@ FLOWS = [ "plex", "point", "ps4", + "rachio", "rainmachine", "ring", "samsungtv", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 9817dd69f81..1cf88a5c7ae 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -43,6 +43,7 @@ HOMEKIT = { "LIFX": "lifx", "Netatmo Relay": "netatmo", "Presence": "netatmo", + "Rachio": "rachio", "TRADFRI": "tradfri", "Welcome": "netatmo", "Wemo": "wemo" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3719522358d..7522274f736 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -613,6 +613,9 @@ pyvizio==0.1.35 # homeassistant.components.html5 pywebpush==1.9.2 +# homeassistant.components.rachio +rachiopy==0.1.3 + # homeassistant.components.rainmachine regenmaschine==1.5.1 diff --git a/tests/components/rachio/__init__.py b/tests/components/rachio/__init__.py new file mode 100644 index 00000000000..64fdec71144 --- /dev/null +++ b/tests/components/rachio/__init__.py @@ -0,0 +1 @@ +"""Tests for the Rachio integration.""" diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py new file mode 100644 index 00000000000..f5df0817846 --- /dev/null +++ b/tests/components/rachio/test_config_flow.py @@ -0,0 +1,104 @@ +"""Test the Rachio config flow.""" +from asynctest import patch +from asynctest.mock import MagicMock + +from homeassistant import config_entries, setup +from homeassistant.components.rachio.const import ( + CONF_CUSTOM_URL, + CONF_MANUAL_RUN_MINS, + DOMAIN, +) +from homeassistant.const import CONF_API_KEY + + +def _mock_rachio_return_value(get=None, getInfo=None): + rachio_mock = MagicMock() + person_mock = MagicMock() + type(person_mock).get = MagicMock(return_value=get) + type(person_mock).getInfo = MagicMock(return_value=getInfo) + type(rachio_mock).person = person_mock + return rachio_mock + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + rachio_mock = _mock_rachio_return_value( + get=({"status": 200}, {"username": "myusername"}), + getInfo=({"status": 200}, {"id": "myid"}), + ) + + with patch( + "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock + ), patch( + "homeassistant.components.rachio.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.rachio.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_API_KEY: "api_key", + CONF_CUSTOM_URL: "http://custom.url", + CONF_MANUAL_RUN_MINS: 5, + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "myusername" + assert result2["data"] == { + CONF_API_KEY: "api_key", + CONF_CUSTOM_URL: "http://custom.url", + CONF_MANUAL_RUN_MINS: 5, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + rachio_mock = _mock_rachio_return_value( + get=({"status": 200}, {"username": "myusername"}), + getInfo=({"status": 412}, {"error": "auth fail"}), + ) + with patch( + "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_KEY: "api_key"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + rachio_mock = _mock_rachio_return_value( + get=({"status": 599}, {"username": "myusername"}), + getInfo=({"status": 200}, {"id": "myid"}), + ) + with patch( + "homeassistant.components.rachio.config_flow.Rachio", return_value=rachio_mock + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_API_KEY: "api_key"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From d04479044cf66deb0b8f547584e9e1df7448b042 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 14 Mar 2020 12:37:44 +0200 Subject: [PATCH 044/431] Upgrade huawei-lte-api to 1.4.11 (#32791) https://github.com/Salamek/huawei-lte-api/releases/tag/1.4.11 --- homeassistant/components/huawei_lte/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/huawei_lte/manifest.json b/homeassistant/components/huawei_lte/manifest.json index 795b33485b6..262ee118e0f 100644 --- a/homeassistant/components/huawei_lte/manifest.json +++ b/homeassistant/components/huawei_lte/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/huawei_lte", "requirements": [ "getmac==0.8.1", - "huawei-lte-api==1.4.10", + "huawei-lte-api==1.4.11", "stringcase==1.2.0", "url-normalize==1.4.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 1e9354dfb64..f8b29280043 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -712,7 +712,7 @@ horimote==0.4.1 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.10 +huawei-lte-api==1.4.11 # homeassistant.components.hydrawise hydrawiser==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7522274f736..0520fb5139c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -279,7 +279,7 @@ homematicip==0.10.17 httplib2==0.10.3 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.10 +huawei-lte-api==1.4.11 # homeassistant.components.iaqualink iaqualink==0.3.1 From e86919a99747349e6ea919665fce82766e6fffcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 14 Mar 2020 12:39:28 +0200 Subject: [PATCH 045/431] Type hint improvements (#32793) --- homeassistant/bootstrap.py | 3 ++- homeassistant/loader.py | 4 ++-- homeassistant/setup.py | 15 ++++++++------- 3 files changed, 12 insertions(+), 10 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 7d4155257db..000c23e1d96 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -20,6 +20,7 @@ from homeassistant.const import ( REQUIRED_NEXT_PYTHON_VER, ) from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType from homeassistant.setup import DATA_SETUP, async_setup_component from homeassistant.util.logging import AsyncHandler from homeassistant.util.package import async_get_user_site, is_virtual_env @@ -133,7 +134,7 @@ async def async_setup_hass( async def async_from_config_dict( - config: Dict[str, Any], hass: core.HomeAssistant + config: ConfigType, hass: core.HomeAssistant ) -> Optional[core.HomeAssistant]: """Try to configure Home Assistant from a configuration dictionary. diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 155dd0e059d..b2e1fa74fba 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -74,7 +74,7 @@ async def _async_get_custom_components( except ImportError: return {} - def get_sub_directories(paths: List) -> List: + def get_sub_directories(paths: List[str]) -> List[pathlib.Path]: """Return all sub directories in a set of paths.""" return [ entry @@ -506,7 +506,7 @@ async def async_component_dependencies(hass: "HomeAssistant", domain: str) -> Se async def _async_component_dependencies( - hass: "HomeAssistant", domain: str, loaded: Set[str], loading: Set + hass: "HomeAssistant", domain: str, loaded: Set[str], loading: Set[str] ) -> Set[str]: """Recursive function to get component dependencies. diff --git a/homeassistant/setup.py b/homeassistant/setup.py index f62228b28f5..40d767728d3 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -3,12 +3,13 @@ import asyncio import logging.handlers from timeit import default_timer as timer from types import ModuleType -from typing import Awaitable, Callable, Dict, List, Optional +from typing import Awaitable, Callable, List, Optional from homeassistant import config as conf_util, core, loader, requirements from homeassistant.config import async_notify_setup_error from homeassistant.const import EVENT_COMPONENT_LOADED, PLATFORM_FORMAT from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import ConfigType _LOGGER = logging.getLogger(__name__) @@ -20,7 +21,7 @@ DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 -def setup_component(hass: core.HomeAssistant, domain: str, config: Dict) -> bool: +def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: """Set up a component and all its dependencies.""" return asyncio.run_coroutine_threadsafe( async_setup_component(hass, domain, config), hass.loop @@ -28,7 +29,7 @@ def setup_component(hass: core.HomeAssistant, domain: str, config: Dict) -> bool async def async_setup_component( - hass: core.HomeAssistant, domain: str, config: Dict + hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component and all its dependencies. @@ -50,7 +51,7 @@ async def async_setup_component( async def _async_process_dependencies( - hass: core.HomeAssistant, config: Dict, name: str, dependencies: List[str] + hass: core.HomeAssistant, config: ConfigType, name: str, dependencies: List[str] ) -> bool: """Ensure all dependencies are set up.""" blacklisted = [dep for dep in dependencies if dep in loader.DEPENDENCY_BLACKLIST] @@ -85,7 +86,7 @@ async def _async_process_dependencies( async def _async_setup_component( - hass: core.HomeAssistant, domain: str, config: Dict + hass: core.HomeAssistant, domain: str, config: ConfigType ) -> bool: """Set up a component for Home Assistant. @@ -212,7 +213,7 @@ async def _async_setup_component( async def async_prepare_setup_platform( - hass: core.HomeAssistant, hass_config: Dict, domain: str, platform_name: str + hass: core.HomeAssistant, hass_config: ConfigType, domain: str, platform_name: str ) -> Optional[ModuleType]: """Load a platform and makes sure dependencies are setup. @@ -267,7 +268,7 @@ async def async_prepare_setup_platform( async def async_process_deps_reqs( - hass: core.HomeAssistant, config: Dict, integration: loader.Integration + hass: core.HomeAssistant, config: ConfigType, integration: loader.Integration ) -> None: """Process all dependencies and requirements for a module. From e5e38edcb2b886f397db287b2e0fcb725b93c9a9 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 14 Mar 2020 13:12:01 -0500 Subject: [PATCH 046/431] Fix directv location of unknown error string (#32807) * Update strings.json * Update en.json --- homeassistant/components/directv/.translations/en.json | 8 ++++---- homeassistant/components/directv/strings.json | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/directv/.translations/en.json b/homeassistant/components/directv/.translations/en.json index e2a8eff5783..667d5168f8d 100644 --- a/homeassistant/components/directv/.translations/en.json +++ b/homeassistant/components/directv/.translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "DirecTV receiver is already configured" + "already_configured": "DirecTV receiver is already configured", + "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "flow_title": "DirecTV: {name}", "step": { @@ -23,4 +23,4 @@ }, "title": "DirecTV" } -} \ No newline at end of file +} diff --git a/homeassistant/components/directv/strings.json b/homeassistant/components/directv/strings.json index 78316d663bd..e0a5a477ad2 100644 --- a/homeassistant/components/directv/strings.json +++ b/homeassistant/components/directv/strings.json @@ -16,11 +16,11 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "abort": { - "already_configured": "DirecTV receiver is already configured" + "already_configured": "DirecTV receiver is already configured", + "unknown": "Unexpected error" } } } From 04763c5bfb05469243a5905924fd36ef76c99950 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 14 Mar 2020 13:32:38 -0500 Subject: [PATCH 047/431] Remove extra logging from directv init. (#32809) --- homeassistant/components/directv/media_player.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index b04ef9fed68..f487e72f694 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -158,15 +158,6 @@ class DirecTvDevice(MediaPlayerDevice): self._model = MODEL_HOST self._software_version = version_info["stbSoftwareVersion"] - if self._is_client: - _LOGGER.debug( - "Created DirecTV media player for client %s on device %s", - self._name, - device, - ) - else: - _LOGGER.debug("Created DirecTV media player for device %s", self._name) - def update(self): """Retrieve latest state.""" _LOGGER.debug("%s: Updating status", self.entity_id) From 4f81109304cca5b08102800c62a01b0e57b59c75 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sat, 14 Mar 2020 19:35:15 +0100 Subject: [PATCH 048/431] Fix flaky tests for HMIPC (#32806) --- .../components/homematicip_cloud/conftest.py | 30 ++++++++++++++++--- .../homematicip_cloud/test_config_flow.py | 7 +++-- .../components/homematicip_cloud/test_hap.py | 7 ++--- .../components/homematicip_cloud/test_init.py | 16 +++++++--- 4 files changed, 45 insertions(+), 15 deletions(-) diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 502e9d1b73e..927690d881f 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -1,5 +1,5 @@ """Initializer helpers for HomematicIP fake server.""" -from asynctest import CoroutineMock, MagicMock, Mock +from asynctest import CoroutineMock, MagicMock, Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome @@ -106,9 +106,10 @@ async def mock_hap_with_service_fixture( @pytest.fixture(name="simple_mock_home") -def simple_mock_home_fixture() -> AsyncHome: - """Return a simple AsyncHome Mock.""" - return Mock( +def simple_mock_home_fixture(): + """Return a simple mocked connection.""" + + mock_home = Mock( spec=AsyncHome, name="Demo", devices=[], @@ -120,6 +121,27 @@ def simple_mock_home_fixture() -> AsyncHome: connected=True, ) + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome", + autospec=True, + return_value=mock_home, + ): + yield + + +@pytest.fixture(name="mock_connection_init") +def mock_connection_init_fixture(): + """Return a simple mocked connection.""" + + with patch( + "homeassistant.components.homematicip_cloud.hap.AsyncHome.init", + return_value=None, + ), patch( + "homeassistant.components.homematicip_cloud.hap.AsyncAuth.init", + return_value=None, + ): + yield + @pytest.fixture(name="simple_mock_auth") def simple_mock_auth_fixture() -> AsyncAuth: diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 01e820e7565..6436433a147 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -16,12 +16,15 @@ DEFAULT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_PIN: "123", HMIPC_NAME: "hmip"} IMPORT_CONFIG = {HMIPC_HAPID: "ABC123", HMIPC_AUTHTOKEN: "123", HMIPC_NAME: "hmip"} -async def test_flow_works(hass): +async def test_flow_works(hass, simple_mock_home): """Test config flow.""" with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", return_value=False, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.get_auth", + return_value=True, ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, context={"source": "user"}, data=DEFAULT_CONFIG @@ -137,7 +140,7 @@ async def test_init_already_configured(hass): assert result["reason"] == "already_configured" -async def test_import_config(hass): +async def test_import_config(hass, simple_mock_home): """Test importing a host with an existing config file.""" with patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_checkbutton", diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 1dd5b2fc789..e6e143973f3 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -125,14 +125,11 @@ async def test_hap_create(hass, hmip_config_entry, simple_mock_home): hass.config.components.add(HMIPC_DOMAIN) hap = HomematicipHAP(hass, hmip_config_entry) assert hap - with patch( - "homeassistant.components.homematicip_cloud.hap.AsyncHome", - return_value=simple_mock_home, - ), patch.object(hap, "async_connect"): + with patch.object(hap, "async_connect"): assert await hap.async_setup() -async def test_hap_create_exception(hass, hmip_config_entry): +async def test_hap_create_exception(hass, hmip_config_entry, mock_connection_init): """Mock AsyncHome to execute get_hap.""" hass.config.components.add(HMIPC_DOMAIN) diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index ef7f5fa24ae..f97e7114b94 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -24,7 +24,9 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_config_with_accesspoint_passed_to_config_entry(hass): +async def test_config_with_accesspoint_passed_to_config_entry( + hass, mock_connection, simple_mock_home +): """Test that config for a accesspoint are loaded via config entry.""" entry_config = { @@ -51,7 +53,9 @@ async def test_config_with_accesspoint_passed_to_config_entry(hass): assert isinstance(hass.data[HMIPC_DOMAIN]["ABC123"], HomematicipHAP) -async def test_config_already_registered_not_passed_to_config_entry(hass): +async def test_config_already_registered_not_passed_to_config_entry( + hass, simple_mock_home +): """Test that an already registered accesspoint does not get imported.""" mock_config = {HMIPC_AUTHTOKEN: "123", HMIPC_HAPID: "ABC123", HMIPC_NAME: "name"} @@ -87,7 +91,9 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): assert config_entries[0].unique_id == "ABC123" -async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry): +async def test_load_entry_fails_due_to_connection_error( + hass, hmip_config_entry, mock_connection_init +): """Test load entry fails due to connection error.""" hmip_config_entry.add_to_hass(hass) @@ -101,7 +107,9 @@ async def test_load_entry_fails_due_to_connection_error(hass, hmip_config_entry) assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY -async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry): +async def test_load_entry_fails_due_to_generic_exception( + hass, hmip_config_entry, simple_mock_home +): """Test load entry fails due to generic exception.""" hmip_config_entry.add_to_hass(hass) From 5b416805064197bbb22e20e88292172da59ab862 Mon Sep 17 00:00:00 2001 From: Greg <34967045+gtdiehl@users.noreply.github.com> Date: Sat, 14 Mar 2020 14:27:28 -0700 Subject: [PATCH 049/431] Bump eagle_reader API version to v0.2.4 (#32789) --- homeassistant/components/rainforest_eagle/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/rainforest_eagle/manifest.json b/homeassistant/components/rainforest_eagle/manifest.json index cb8e95df42f..0649dfded99 100644 --- a/homeassistant/components/rainforest_eagle/manifest.json +++ b/homeassistant/components/rainforest_eagle/manifest.json @@ -3,7 +3,7 @@ "name": "Rainforest Eagle-200", "documentation": "https://www.home-assistant.io/integrations/rainforest_eagle", "requirements": [ - "eagle200_reader==0.2.1", + "eagle200_reader==0.2.4", "uEagle==0.0.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index f8b29280043..c4601217651 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -469,7 +469,7 @@ dweepy==0.3.0 dynalite_devices==0.1.32 # homeassistant.components.rainforest_eagle -eagle200_reader==0.2.1 +eagle200_reader==0.2.4 # homeassistant.components.ebusd ebusdpy==0.0.16 From 5ec76af8755794db37632ece81f2fdb73befb94a Mon Sep 17 00:00:00 2001 From: Dave Pearce Date: Sat, 14 Mar 2020 17:56:02 -0400 Subject: [PATCH 050/431] Add Insteon Dual Band SwitchLinc model 2477S to ISY994 (#32813) --- homeassistant/components/isy994/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index ebd1b0dbbb2..f0766c4e4f9 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -143,6 +143,8 @@ NODE_FILTERS = { "Siren", "Siren_ADV", "X10", + "KeypadRelay", + "KeypadRelay_ADV", ], "insteon_type": ["2.", "9.10.", "9.11.", "113."], }, From 6affb277114831e679633ad90e3ac2e4db46d5fe Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sat, 14 Mar 2020 19:43:09 +0000 Subject: [PATCH 051/431] Fix homekit_controller beta connectivity issues (#32810) --- homeassistant/components/homekit_controller/manifest.json | 2 +- homeassistant/components/homekit_controller/media_player.py | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 5ff719dde8c..a73d68227c7 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[IP]==0.2.29"], + "requirements": ["aiohomekit[IP]==0.2.29.1"], "dependencies": [], "zeroconf": ["_hap._tcp.local."], "codeowners": ["@Jc2k"] diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 2e4b05817bb..3a1a7359e13 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -154,7 +154,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): homekit_state = self.service.value(CharacteristicsTypes.CURRENT_MEDIA_STATE) if homekit_state is not None: - return HK_TO_HA_STATE[homekit_state] + return HK_TO_HA_STATE.get(homekit_state, STATE_OK) return STATE_OK diff --git a/requirements_all.txt b/requirements_all.txt index c4601217651..b60eb022195 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioftp==0.12.0 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.29 +aiohomekit[IP]==0.2.29.1 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0520fb5139c..d5c0f26ee38 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -65,7 +65,7 @@ aioesphomeapi==2.6.1 aiofreepybox==0.0.8 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.29 +aiohomekit[IP]==0.2.29.1 # homeassistant.components.emulated_hue # homeassistant.components.http From f9634f023297a76d1c523875c5592c355d6b9bf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Sun, 15 Mar 2020 12:41:19 +0100 Subject: [PATCH 052/431] Add Netatmo Home Coach as model (#32829) --- homeassistant/components/netatmo/const.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 9216a678e68..0a0c9575600 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -19,6 +19,7 @@ MODELS = { "NAModule4": "Smart Additional Indoor module", "NAModule3": "Smart Rain Gauge", "NAModule2": "Smart Anemometer", + "NHC": "Home Coach", } AUTH = "netatmo_auth" From b6e69cd370bdfd11c5bc459f7ef4aff867cf231e Mon Sep 17 00:00:00 2001 From: Steven Looman Date: Sun, 15 Mar 2020 18:18:15 +0100 Subject: [PATCH 053/431] Add log message on timeout and update less often for upnp devices (#32740) * Catch asyncio.TimeoutError, show a proper message instead * Throttle updates to max once per 30s * Change code owner * Fix CODEOWNERS + linting * Warn on connection timeout --- CODEOWNERS | 2 +- homeassistant/components/upnp/device.py | 20 ++++++++++++++++---- homeassistant/components/upnp/manifest.json | 2 +- homeassistant/components/upnp/sensor.py | 5 +++++ 4 files changed, 23 insertions(+), 6 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index b0498c8a60b..1bcffad1d17 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -387,7 +387,7 @@ homeassistant/components/unifiled/* @florisvdk homeassistant/components/upc_connect/* @pvizeli homeassistant/components/upcloud/* @scop homeassistant/components/updater/* @home-assistant/core -homeassistant/components/upnp/* @robbiet480 +homeassistant/components/upnp/* @StevenLooman homeassistant/components/uptimerobot/* @ludeeus homeassistant/components/usgs_earthquakes_feed/* @exxamalte homeassistant/components/utility_meter/* @dgomes diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index b144d2b96ed..474170050c3 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -142,16 +142,28 @@ class Device: async def async_get_total_bytes_received(self): """Get total bytes received.""" - return await self._igd_device.async_get_total_bytes_received() + try: + return await self._igd_device.async_get_total_bytes_received() + except asyncio.TimeoutError: + _LOGGER.warning("Timeout during get_total_bytes_received") async def async_get_total_bytes_sent(self): """Get total bytes sent.""" - return await self._igd_device.async_get_total_bytes_sent() + try: + return await self._igd_device.async_get_total_bytes_sent() + except asyncio.TimeoutError: + _LOGGER.warning("Timeout during get_total_bytes_sent") async def async_get_total_packets_received(self): """Get total packets received.""" - return await self._igd_device.async_get_total_packets_received() + try: + return await self._igd_device.async_get_total_packets_received() + except asyncio.TimeoutError: + _LOGGER.warning("Timeout during get_total_packets_received") async def async_get_total_packets_sent(self): """Get total packets sent.""" - return await self._igd_device.async_get_total_packets_sent() + try: + return await self._igd_device.async_get_total_packets_sent() + except asyncio.TimeoutError: + _LOGGER.warning("Timeout during get_total_packets_sent") diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 1e55d60f95e..47ad465eb36 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -5,5 +5,5 @@ "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.14.12"], "dependencies": [], - "codeowners": ["@robbiet480"] + "codeowners": ["@StevenLooman"] } diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index c77a1b6279f..9632997ac1b 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -1,4 +1,5 @@ """Support for UPnP/IGD Sensors.""" +from datetime import timedelta import logging from homeassistant.const import DATA_BYTES, DATA_KIBIBYTES, TIME_SECONDS @@ -7,6 +8,7 @@ from homeassistant.helpers import device_registry as dr from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util import Throttle import homeassistant.util.dt as dt_util from .const import DOMAIN as DOMAIN_UPNP, SIGNAL_REMOVE_SENSOR @@ -29,6 +31,8 @@ IN = "received" OUT = "sent" KIBIBYTE = 1024 +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=30) + async def async_setup_platform( hass: HomeAssistantType, config, async_add_entities, discovery_info=None @@ -142,6 +146,7 @@ class RawUPnPIGDSensor(UpnpSensor): """Return the unit of measurement of this entity, if any.""" return self._type["unit"] + @Throttle(MIN_TIME_BETWEEN_UPDATES) async def async_update(self): """Get the latest information from the IGD.""" if self._type_name == BYTES_RECEIVED: From 2f8048942822718ef0ed49538b6a7c8398f48bdb Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 15 Mar 2020 19:01:50 +0100 Subject: [PATCH 054/431] Add SF transition to HmIP-BSL and remove obsolete code in HMIPC (#32833) --- homeassistant/components/homematicip_cloud/hap.py | 5 ----- homeassistant/components/homematicip_cloud/light.py | 3 ++- .../components/homematicip_cloud/test_alarm_control_panel.py | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/hap.py b/homeassistant/components/homematicip_cloud/hap.py index 0d6fc726050..dd85827f1ae 100644 --- a/homeassistant/components/homematicip_cloud/hap.py +++ b/homeassistant/components/homematicip_cloud/hap.py @@ -137,11 +137,6 @@ class HomematicipHAP: job = self.hass.async_create_task(self.get_state()) job.add_done_callback(self.get_state_finished) self._accesspoint_connected = True - else: - # Update home with the given json from arg[0], - # without devices and groups. - - self.home.update_home_only(args[0]) @callback def async_create_entity(self, *args, **kwargs) -> None: diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index 4e081f4d8fa..cead186db95 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -20,6 +20,7 @@ from homeassistant.components.light import ( ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_TRANSITION, Light, ) from homeassistant.config_entries import ConfigEntry @@ -197,7 +198,7 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def supported_features(self) -> int: """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_COLOR + return SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_TRANSITION @property def unique_id(self) -> str: diff --git a/tests/components/homematicip_cloud/test_alarm_control_panel.py b/tests/components/homematicip_cloud/test_alarm_control_panel.py index 23e5beb40eb..92782f2cbb2 100644 --- a/tests/components/homematicip_cloud/test_alarm_control_panel.py +++ b/tests/components/homematicip_cloud/test_alarm_control_panel.py @@ -31,9 +31,7 @@ async def _async_manipulate_security_zones( internal_zone = home.search_group_by_id(internal_zone_id) internal_zone.active = internal_active - home.from_json(json) - home._get_functionalHomes(json) - home._load_functionalChannels() + home.update_home_only(json) home.fire_update_event(json) await hass.async_block_till_done() From f4bf66aecdc5388059d5253f249535aa5857c111 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sun, 15 Mar 2020 19:50:23 +0100 Subject: [PATCH 055/431] Require a hyphen in lovelace dashboard url (#32816) * Require a hyphen in lovelace dashboard url * Keep storage dashboards working * register during startup again * Update homeassistant/components/lovelace/dashboard.py Co-Authored-By: Paulus Schoutsen * Comments Co-authored-by: Paulus Schoutsen --- homeassistant/components/lovelace/__init__.py | 47 +++--- homeassistant/components/lovelace/const.py | 2 + .../components/lovelace/dashboard.py | 23 +++ tests/components/lovelace/test_dashboard.py | 147 ++++++++++++++---- 4 files changed, 163 insertions(+), 56 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 8ed5e1abfbb..220161fb649 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import frontend -from homeassistant.const import CONF_FILENAME, EVENT_HOMEASSISTANT_START +from homeassistant.const import CONF_FILENAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv @@ -143,6 +143,7 @@ async def async_setup(hass, config): return if change_type == collection.CHANGE_ADDED: + existing = hass.data[DOMAIN]["dashboards"].get(url_path) if existing: @@ -167,34 +168,30 @@ async def async_setup(hass, config): except ValueError: _LOGGER.warning("Failed to %s panel %s from storage", change_type, url_path) - async def async_setup_dashboards(event): - """Register dashboards on startup.""" - # Process YAML dashboards - for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): - # For now always mode=yaml - config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) - hass.data[DOMAIN]["dashboards"][url_path] = config + # Process YAML dashboards + for url_path, dashboard_conf in hass.data[DOMAIN]["yaml_dashboards"].items(): + # For now always mode=yaml + config = dashboard.LovelaceYAML(hass, url_path, dashboard_conf) + hass.data[DOMAIN]["dashboards"][url_path] = config - try: - _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) - except ValueError: - _LOGGER.warning("Panel url path %s is not unique", url_path) + try: + _register_panel(hass, url_path, MODE_YAML, dashboard_conf, False) + except ValueError: + _LOGGER.warning("Panel url path %s is not unique", url_path) - # Process storage dashboards - dashboards_collection = dashboard.DashboardsCollection(hass) + # Process storage dashboards + dashboards_collection = dashboard.DashboardsCollection(hass) - dashboards_collection.async_add_listener(storage_dashboard_changed) - await dashboards_collection.async_load() + dashboards_collection.async_add_listener(storage_dashboard_changed) + await dashboards_collection.async_load() - collection.StorageCollectionWebsocket( - dashboards_collection, - "lovelace/dashboards", - "dashboard", - STORAGE_DASHBOARD_CREATE_FIELDS, - STORAGE_DASHBOARD_UPDATE_FIELDS, - ).async_setup(hass, create_list=False) - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, async_setup_dashboards) + collection.StorageCollectionWebsocket( + dashboards_collection, + "lovelace/dashboards", + "dashboard", + STORAGE_DASHBOARD_CREATE_FIELDS, + STORAGE_DASHBOARD_UPDATE_FIELDS, + ).async_setup(hass, create_list=False) return True diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 7205ae21cbe..8d7ee092cbe 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -76,6 +76,8 @@ def url_slug(value: Any) -> str: """Validate value is a valid url slug.""" if value is None: raise vol.Invalid("Slug should not be None") + if "-" not in value: + raise vol.Invalid("Url path needs to contain a hyphen (-)") str_value = str(value) slg = slugify(str_value, separator="-") if str_value == slg: diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index f32ac2ed1ff..38740672914 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -3,6 +3,7 @@ from abc import ABC, abstractmethod import logging import os import time +from typing import Optional, cast import voluptuous as vol @@ -230,8 +231,30 @@ class DashboardsCollection(collection.StorageCollection): _LOGGER, ) + async def _async_load_data(self) -> Optional[dict]: + """Load the data.""" + data = await self.store.async_load() + + if data is None: + return cast(Optional[dict], data) + + updated = False + + for item in data["items"] or []: + if "-" not in item[CONF_URL_PATH]: + updated = True + item[CONF_URL_PATH] = f"lovelace-{item[CONF_URL_PATH]}" + + if updated: + await self.store.async_save(data) + + return cast(Optional[dict], data) + async def _process_create_data(self, data: dict) -> dict: """Validate the config is valid.""" + if "-" not in data[CONF_URL_PATH]: + raise vol.Invalid("Url path needs to contain a hyphen (-)") + if data[CONF_URL_PATH] in self.hass.data[DATA_PANELS]: raise vol.Invalid("Panel url path needs to be unique") diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 9bfe3da38c9..1effb10be27 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -5,10 +5,13 @@ import pytest from homeassistant.components import frontend from homeassistant.components.lovelace import const, dashboard -from homeassistant.const import EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component -from tests.common import async_capture_events, get_system_health_info +from tests.common import ( + assert_setup_component, + async_capture_events, + get_system_health_info, +) async def test_lovelace_from_storage(hass, hass_ws_client, hass_storage): @@ -224,8 +227,6 @@ async def test_dashboard_from_yaml(hass, hass_ws_client, url_path): } }, ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() assert hass.data[frontend.DATA_PANELS]["test-panel"].config == {"mode": "yaml"} assert hass.data[frontend.DATA_PANELS]["test-panel-no-sidebar"].config == { "mode": "yaml" @@ -306,11 +307,32 @@ async def test_dashboard_from_yaml(hass, hass_ws_client, url_path): assert len(events) == 1 +async def test_wrong_key_dashboard_from_yaml(hass): + """Test we don't load lovelace dashboard without hyphen config from yaml.""" + with assert_setup_component(0): + assert not await async_setup_component( + hass, + "lovelace", + { + "lovelace": { + "dashboards": { + "testpanel": { + "mode": "yaml", + "filename": "bla.yaml", + "title": "Test Panel", + "icon": "mdi:test-icon", + "show_in_sidebar": False, + "require_admin": True, + } + } + } + }, + ) + + async def test_storage_dashboards(hass, hass_ws_client, hass_storage): """Test we load lovelace config from storage.""" assert await async_setup_component(hass, "lovelace", {}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() assert hass.data[frontend.DATA_PANELS]["lovelace"].config == {"mode": "storage"} client = await hass_ws_client(hass) @@ -321,12 +343,24 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): assert response["success"] assert response["result"] == [] - # Add a dashboard + # Add a wrong dashboard await client.send_json( { "id": 6, "type": "lovelace/dashboards/create", - "url_path": "created_url_path", + "url_path": "path", + "title": "Test path without hyphen", + } + ) + response = await client.receive_json() + assert not response["success"] + + # Add a dashboard + await client.send_json( + { + "id": 7, + "type": "lovelace/dashboards/create", + "url_path": "created-url-path", "require_admin": True, "title": "New Title", "icon": "mdi:map", @@ -339,10 +373,11 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): assert response["result"]["icon"] == "mdi:map" dashboard_id = response["result"]["id"] + dashboard_path = response["result"]["url_path"] - assert "created_url_path" in hass.data[frontend.DATA_PANELS] + assert "created-url-path" in hass.data[frontend.DATA_PANELS] - await client.send_json({"id": 7, "type": "lovelace/dashboards/list"}) + await client.send_json({"id": 8, "type": "lovelace/dashboards/list"}) response = await client.receive_json() assert response["success"] assert len(response["result"]) == 1 @@ -354,7 +389,7 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): # Fetch config await client.send_json( - {"id": 8, "type": "lovelace/config", "url_path": "created_url_path"} + {"id": 9, "type": "lovelace/config", "url_path": "created-url-path"} ) response = await client.receive_json() assert not response["success"] @@ -365,22 +400,22 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): await client.send_json( { - "id": 9, + "id": 10, "type": "lovelace/config/save", - "url_path": "created_url_path", + "url_path": "created-url-path", "config": {"yo": "hello"}, } ) response = await client.receive_json() assert response["success"] - assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format(dashboard_id)]["data"] == { - "config": {"yo": "hello"} - } + assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format(dashboard_path)][ + "data" + ] == {"config": {"yo": "hello"}} assert len(events) == 1 - assert events[0].data["url_path"] == "created_url_path" + assert events[0].data["url_path"] == "created-url-path" await client.send_json( - {"id": 10, "type": "lovelace/config", "url_path": "created_url_path"} + {"id": 11, "type": "lovelace/config", "url_path": "created-url-path"} ) response = await client.receive_json() assert response["success"] @@ -389,7 +424,7 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): # Update a dashboard await client.send_json( { - "id": 11, + "id": 12, "type": "lovelace/dashboards/update", "dashboard_id": dashboard_id, "require_admin": False, @@ -401,19 +436,19 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): response = await client.receive_json() assert response["success"] assert response["result"]["mode"] == "storage" - assert response["result"]["url_path"] == "created_url_path" + assert response["result"]["url_path"] == "created-url-path" assert response["result"]["title"] == "Updated Title" assert response["result"]["icon"] == "mdi:updated" assert response["result"]["show_in_sidebar"] is False assert response["result"]["require_admin"] is False # List dashboards again and make sure we see latest config - await client.send_json({"id": 12, "type": "lovelace/dashboards/list"}) + await client.send_json({"id": 13, "type": "lovelace/dashboards/list"}) response = await client.receive_json() assert response["success"] assert len(response["result"]) == 1 assert response["result"][0]["mode"] == "storage" - assert response["result"][0]["url_path"] == "created_url_path" + assert response["result"][0]["url_path"] == "created-url-path" assert response["result"][0]["title"] == "Updated Title" assert response["result"][0]["icon"] == "mdi:updated" assert response["result"][0]["show_in_sidebar"] is False @@ -421,22 +456,75 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): # Add dashboard with existing url path await client.send_json( - {"id": 13, "type": "lovelace/dashboards/create", "url_path": "created_url_path"} + {"id": 14, "type": "lovelace/dashboards/create", "url_path": "created-url-path"} ) response = await client.receive_json() assert not response["success"] # Delete dashboards await client.send_json( - {"id": 14, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id} + {"id": 15, "type": "lovelace/dashboards/delete", "dashboard_id": dashboard_id} ) response = await client.receive_json() assert response["success"] - assert "created_url_path" not in hass.data[frontend.DATA_PANELS] + assert "created-url-path" not in hass.data[frontend.DATA_PANELS] assert dashboard.CONFIG_STORAGE_KEY.format(dashboard_id) not in hass_storage +async def test_storage_dashboard_migrate(hass, hass_ws_client, hass_storage): + """Test changing url path from storage config.""" + hass_storage[dashboard.DASHBOARDS_STORAGE_KEY] = { + "key": "lovelace_dashboards", + "version": 1, + "data": { + "items": [ + { + "icon": "mdi:tools", + "id": "tools", + "mode": "storage", + "require_admin": True, + "show_in_sidebar": True, + "title": "Tools", + "url_path": "tools", + }, + { + "icon": "mdi:tools", + "id": "tools2", + "mode": "storage", + "require_admin": True, + "show_in_sidebar": True, + "title": "Tools", + "url_path": "dashboard-tools", + }, + ] + }, + } + + assert await async_setup_component(hass, "lovelace", {}) + + client = await hass_ws_client(hass) + + # Fetch data + await client.send_json({"id": 5, "type": "lovelace/dashboards/list"}) + response = await client.receive_json() + assert response["success"] + without_hyphen, with_hyphen = response["result"] + + assert without_hyphen["icon"] == "mdi:tools" + assert without_hyphen["id"] == "tools" + assert without_hyphen["mode"] == "storage" + assert without_hyphen["require_admin"] + assert without_hyphen["show_in_sidebar"] + assert without_hyphen["title"] == "Tools" + assert without_hyphen["url_path"] == "lovelace-tools" + + assert ( + with_hyphen + == hass_storage[dashboard.DASHBOARDS_STORAGE_KEY]["data"]["items"][1] + ) + + async def test_websocket_list_dashboards(hass, hass_ws_client): """Test listing dashboards both storage + YAML.""" assert await async_setup_component( @@ -455,9 +543,6 @@ async def test_websocket_list_dashboards(hass, hass_ws_client): }, ) - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - await hass.async_block_till_done() - client = await hass_ws_client(hass) # Create a storage dashboard @@ -465,7 +550,7 @@ async def test_websocket_list_dashboards(hass, hass_ws_client): { "id": 6, "type": "lovelace/dashboards/create", - "url_path": "created_url_path", + "url_path": "created-url-path", "title": "Test Storage", } ) @@ -473,7 +558,7 @@ async def test_websocket_list_dashboards(hass, hass_ws_client): assert response["success"] # List dashboards - await client.send_json({"id": 7, "type": "lovelace/dashboards/list"}) + await client.send_json({"id": 8, "type": "lovelace/dashboards/list"}) response = await client.receive_json() assert response["success"] assert len(response["result"]) == 2 @@ -486,4 +571,4 @@ async def test_websocket_list_dashboards(hass, hass_ws_client): assert without_sb["mode"] == "storage" assert without_sb["title"] == "Test Storage" - assert without_sb["url_path"] == "created_url_path" + assert without_sb["url_path"] == "created-url-path" From 2889067ecec7b361c10880148ae2f82021d4f204 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 15 Mar 2020 11:51:02 -0700 Subject: [PATCH 056/431] Make sure panel_custom won't crash on invalid data (#32835) * Make sure panel_custom won't crash on invalid data * Add a test --- homeassistant/components/hassio/manifest.json | 3 ++- homeassistant/components/panel_custom/__init__.py | 15 +++++++++------ tests/components/panel_custom/test_init.py | 14 ++++++++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/hassio/manifest.json b/homeassistant/components/hassio/manifest.json index d3dd7dc9c94..cd004db4c93 100644 --- a/homeassistant/components/hassio/manifest.json +++ b/homeassistant/components/hassio/manifest.json @@ -3,6 +3,7 @@ "name": "Hass.io", "documentation": "https://www.home-assistant.io/hassio", "requirements": [], - "dependencies": ["http", "panel_custom"], + "dependencies": ["http"], + "after_dependencies": ["panel_custom"], "codeowners": ["@home-assistant/hass-io"] } diff --git a/homeassistant/components/panel_custom/__init__.py b/homeassistant/components/panel_custom/__init__.py index cf861992bd6..82572d7396c 100644 --- a/homeassistant/components/panel_custom/__init__.py +++ b/homeassistant/components/panel_custom/__init__.py @@ -146,8 +146,6 @@ async def async_setup(hass, config): if DOMAIN not in config: return True - success = False - for panel in config[DOMAIN]: name = panel[CONF_COMPONENT_NAME] @@ -182,8 +180,13 @@ async def async_setup(hass, config): hass.http.register_static_path(url, panel_path) kwargs["html_url"] = url - await async_register_panel(hass, **kwargs) + try: + await async_register_panel(hass, **kwargs) + except ValueError as err: + _LOGGER.error( + "Unable to register panel %s: %s", + panel.get(CONF_SIDEBAR_TITLE, name), + err, + ) - success = True - - return success + return True diff --git a/tests/components/panel_custom/test_init.py b/tests/components/panel_custom/test_init.py index e6bc56d080e..5f7161089f6 100644 --- a/tests/components/panel_custom/test_init.py +++ b/tests/components/panel_custom/test_init.py @@ -181,3 +181,17 @@ async def test_url_option_conflict(hass): for config in to_try: result = await setup.async_setup_component(hass, "panel_custom", config) assert not result + + +async def test_url_path_conflict(hass): + """Test config with overlapping url path.""" + assert await setup.async_setup_component( + hass, + "panel_custom", + { + "panel_custom": [ + {"name": "todo-mvc", "js_url": "/local/bla.js"}, + {"name": "todo-mvc", "js_url": "/local/bla.js"}, + ] + }, + ) From 1391f90a30b30d08bf13e0eb0fdb830306af89c3 Mon Sep 17 00:00:00 2001 From: Tom Harris Date: Sun, 15 Mar 2020 16:50:23 -0400 Subject: [PATCH 057/431] Bump insteonplm to 0.16.8 (#32847) --- homeassistant/components/insteon/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/insteon/manifest.json b/homeassistant/components/insteon/manifest.json index 69c35477b8d..64c4b6a67be 100644 --- a/homeassistant/components/insteon/manifest.json +++ b/homeassistant/components/insteon/manifest.json @@ -2,7 +2,7 @@ "domain": "insteon", "name": "Insteon", "documentation": "https://www.home-assistant.io/integrations/insteon", - "requirements": ["insteonplm==0.16.7"], + "requirements": ["insteonplm==0.16.8"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index b60eb022195..2d4a477c611 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -747,7 +747,7 @@ incomfort-client==0.4.0 influxdb==5.2.3 # homeassistant.components.insteon -insteonplm==0.16.7 +insteonplm==0.16.8 # homeassistant.components.iperf3 iperf3==0.1.11 From ef54f33af7c4d6eb1d3c9f8ae397a70cb7224884 Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Sun, 15 Mar 2020 20:11:26 -0400 Subject: [PATCH 058/431] Ignore the ignored konnected config entries (#32845) * ignore the ignored konnected config entries * key off data instead of source --- homeassistant/components/konnected/__init__.py | 1 + tests/components/konnected/test_init.py | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 94508b01483..72d82fd31be 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -306,6 +306,7 @@ class KonnectedView(HomeAssistantView): [ entry.data[CONF_ACCESS_TOKEN] for entry in hass.config_entries.async_entries(DOMAIN) + if entry.data.get(CONF_ACCESS_TOKEN) ] ) if auth is None or not next( diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index 907f83cd981..e410aa9d60a 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -582,6 +582,10 @@ async def test_state_updates(hass, aiohttp_client, mock_panel): ) entry.add_to_hass(hass) + # Add empty data field to ensure we process it correctly (possible if entry is ignored) + entry = MockConfigEntry(domain="konnected", title="Konnected Alarm Panel", data={},) + entry.add_to_hass(hass) + assert ( await async_setup_component( hass, From d62bb9ed47ddaaa4af75ad4a8e67913e147749a3 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 15 Mar 2020 21:01:41 -0500 Subject: [PATCH 059/431] Add model to rachio device info (#32814) * Add model to rachio device info Address followup items * Address review items, retest zone updates back and forth, and standby mode * Remove super * Revert "Remove super" This reverts commit 02e2f156a9e0d5316f52341871071912d7207321. * Update homeassistant/components/rachio/switch.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/rachio/binary_sensor.py Co-Authored-By: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/rachio/__init__.py | 85 ++++++++++++------- .../components/rachio/binary_sensor.py | 58 +++++-------- .../components/rachio/config_flow.py | 9 +- homeassistant/components/rachio/const.py | 1 + homeassistant/components/rachio/switch.py | 59 ++++++------- 5 files changed, 108 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 67659c6ee4d..7eaa76dedd4 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -13,19 +13,22 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, device_registry from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.entity import Entity from .const import ( CONF_CUSTOM_URL, CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, + DEFAULT_NAME, DOMAIN, KEY_DEVICES, KEY_ENABLED, KEY_EXTERNAL_ID, KEY_ID, KEY_MAC_ADDRESS, + KEY_MODEL, KEY_NAME, KEY_SERIAL_NUMBER, KEY_STATUS, @@ -142,10 +145,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # CONF_MANUAL_RUN_MINS can only come from a yaml import if not options.get(CONF_MANUAL_RUN_MINS) and config.get(CONF_MANUAL_RUN_MINS): - options[CONF_MANUAL_RUN_MINS] = config[CONF_MANUAL_RUN_MINS] + options_copy = options.copy() + options_copy[CONF_MANUAL_RUN_MINS] = config[CONF_MANUAL_RUN_MINS] + hass.config_entries.async_update_entry(options=options_copy) # Configure API - api_key = config.get(CONF_API_KEY) + api_key = config[CONF_API_KEY] rachio = Rachio(api_key) # Get the URL of this server @@ -155,9 +160,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): webhook_url_path = f"{WEBHOOK_PATH}-{entry.entry_id}" rachio.webhook_url = f"{hass_url}{webhook_url_path}" + person = RachioPerson(rachio, entry) + # Get the API user try: - person = await hass.async_add_executor_job(RachioPerson, hass, rachio, entry) + await hass.async_add_executor_job(person.setup, hass) # Yes we really do get all these exceptions (hopefully rachiopy switches to requests) # and there is not a reasonable timeout here so it can block for a long time except RACHIO_API_EXCEPTIONS as error: @@ -187,23 +194,26 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): class RachioPerson: """Represent a Rachio user.""" - def __init__(self, hass, rachio, config_entry): + def __init__(self, rachio, config_entry): """Create an object from the provided API instance.""" # Use API token to get user ID - self._hass = hass self.rachio = rachio self.config_entry = config_entry + self.username = None + self._id = None + self._controllers = [] - response = rachio.person.getInfo() + def setup(self, hass): + """Rachio device setup.""" + response = self.rachio.person.getInfo() assert int(response[0][KEY_STATUS]) == 200, "API key error" self._id = response[1][KEY_ID] # Use user ID to get user data - data = rachio.person.get(self._id) + data = self.rachio.person.get(self._id) assert int(data[0][KEY_STATUS]) == 200, "User ID error" self.username = data[1][KEY_USERNAME] devices = data[1][KEY_DEVICES] - self._controllers = [] for controller in devices: webhooks = self.rachio.notification.getDeviceWebhook(controller[KEY_ID])[1] # The API does not provide a way to tell if a controller is shared @@ -218,9 +228,10 @@ class RachioPerson: webhooks.get("error", "Unknown Error"), ) continue - self._controllers.append( - RachioIro(self._hass, self.rachio, controller, webhooks) - ) + + rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) + rachio_iro.setup() + self._controllers.append(rachio_iro) _LOGGER.info('Using Rachio API as user "%s"', self.username) @property @@ -242,14 +253,17 @@ class RachioIro: self.hass = hass self.rachio = rachio self._id = data[KEY_ID] - self._name = data[KEY_NAME] - self._serial_number = data[KEY_SERIAL_NUMBER] - self._mac_address = data[KEY_MAC_ADDRESS] + self.name = data[KEY_NAME] + self.serial_number = data[KEY_SERIAL_NUMBER] + self.mac_address = data[KEY_MAC_ADDRESS] + self.model = data[KEY_MODEL] self._zones = data[KEY_ZONES] self._init_data = data self._webhooks = webhooks _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + def setup(self): + """Rachio Iro setup for webhooks.""" # Listen for all updates self._init_webhooks() @@ -301,21 +315,6 @@ class RachioIro: """Return the Rachio API controller ID.""" return self._id - @property - def serial_number(self) -> str: - """Return the Rachio API controller serial number.""" - return self._serial_number - - @property - def mac_address(self) -> str: - """Return the Rachio API controller mac address.""" - return self._mac_address - - @property - def name(self) -> str: - """Return the user-defined name of the controller.""" - return self._name - @property def current_schedule(self) -> str: """Return the schedule that the device is running right now.""" @@ -349,6 +348,28 @@ class RachioIro: _LOGGER.info("Stopped watering of all zones on %s", str(self)) +class RachioDeviceInfoProvider(Entity): + """Mixin to provide device_info.""" + + def __init__(self, controller): + """Initialize a Rachio device.""" + super().__init__() + self._controller = controller + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._controller.serial_number,)}, + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) + }, + "name": self._controller.name, + "model": self._controller.model, + "manufacturer": DEFAULT_NAME, + } + + class RachioWebhookView(HomeAssistantView): """Provide a page for the server to call.""" @@ -365,7 +386,9 @@ class RachioWebhookView(HomeAssistantView): self._entry_id = entry_id self.url = webhook_url self.name = webhook_url[1:].replace("/", ":") - _LOGGER.debug("Created webhook at url: %s, with name %s", self.url, self.name) + _LOGGER.debug( + "Initialize webhook at url: %s, with name %s", self.url, self.name + ) async def post(self, request) -> web.Response: """Handle webhook calls from the server.""" diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 31a5cd889e9..43ee9650163 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -3,8 +3,7 @@ from abc import abstractmethod import logging from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.helpers import device_registry -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( SIGNAL_RACHIO_CONTROLLER_UPDATE, @@ -12,48 +11,39 @@ from . import ( STATUS_ONLINE, SUBTYPE_OFFLINE, SUBTYPE_ONLINE, + RachioDeviceInfoProvider, ) -from .const import ( - DEFAULT_NAME, - DOMAIN as DOMAIN_RACHIO, - KEY_DEVICE_ID, - KEY_STATUS, - KEY_SUBTYPE, -) +from .const import DOMAIN as DOMAIN_RACHIO, KEY_DEVICE_ID, KEY_STATUS, KEY_SUBTYPE _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Rachio binary sensors.""" - devices = await hass.async_add_executor_job(_create_devices, hass, config_entry) - async_add_entities(devices) - _LOGGER.info("%d Rachio binary sensor(s) added", len(devices)) + entities = await hass.async_add_executor_job(_create_entities, hass, config_entry) + async_add_entities(entities) + _LOGGER.info("%d Rachio binary sensor(s) added", len(entities)) -def _create_devices(hass, config_entry): - devices = [] +def _create_entities(hass, config_entry): + entities = [] for controller in hass.data[DOMAIN_RACHIO][config_entry.entry_id].controllers: - devices.append(RachioControllerOnlineBinarySensor(hass, controller)) - return devices + entities.append(RachioControllerOnlineBinarySensor(controller)) + return entities -class RachioControllerBinarySensor(BinarySensorDevice): +class RachioControllerBinarySensor(RachioDeviceInfoProvider, BinarySensorDevice): """Represent a binary sensor that reflects a Rachio state.""" - def __init__(self, hass, controller, poll=True): + def __init__(self, controller, poll=True): """Set up a new Rachio controller binary sensor.""" - self._controller = controller + super().__init__(controller) if poll: self._state = self._poll_update() else: self._state = None - dispatcher_connect( - hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update - ) - @property def should_poll(self) -> bool: """Declare that this entity pushes its state to HA.""" @@ -78,30 +68,24 @@ class RachioControllerBinarySensor(BinarySensorDevice): """Request the state from the API.""" pass - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN_RACHIO, self._controller.serial_number,)}, - "connections": { - (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) - }, - "name": self._controller.name, - "manufacturer": DEFAULT_NAME, - } - @abstractmethod def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" pass + async def async_added_to_hass(self): + """Subscribe to updates.""" + async_dispatcher_connect( + self.hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update + ) + class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Represent a binary sensor that reflects if the controller is online.""" - def __init__(self, hass, controller): + def __init__(self, controller): """Set up a new Rachio controller online binary sensor.""" - super().__init__(hass, controller, poll=False) + super().__init__(controller, poll=False) self._state = self._poll_update(controller.init_data) @property diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 3d2a4c18ab2..3a4c2a1c171 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -61,12 +61,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} - _LOGGER.debug("async_step_user: %s", user_input) if user_input is not None: try: info = await validate_input(self.hass, user_input) - await self.async_set_unique_id(user_input[CONF_API_KEY]) - return self.async_create_entry(title=info["title"], data=user_input) + except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -75,6 +73,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" + if "base" not in errors: + await self.async_set_unique_id(user_input[CONF_API_KEY]) + 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 ) diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index 2388ed283f1..fb66d4378f1 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -20,6 +20,7 @@ KEY_ENABLED = "enabled" KEY_EXTERNAL_ID = "externalId" KEY_ID = "id" KEY_NAME = "name" +KEY_MODEL = "model" KEY_ON = "on" KEY_STATUS = "status" KEY_SUBTYPE = "subType" diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 7f76cd9042d..5320d434d00 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -4,8 +4,7 @@ from datetime import timedelta import logging from homeassistant.components.switch import SwitchDevice -from homeassistant.helpers import device_registry -from homeassistant.helpers.dispatcher import dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import ( SIGNAL_RACHIO_CONTROLLER_UPDATE, @@ -15,11 +14,11 @@ from . import ( SUBTYPE_ZONE_COMPLETED, SUBTYPE_ZONE_STARTED, SUBTYPE_ZONE_STOPPED, + RachioDeviceInfoProvider, ) from .const import ( CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, - DEFAULT_NAME, DOMAIN as DOMAIN_RACHIO, KEY_DEVICE_ID, KEY_ENABLED, @@ -42,33 +41,33 @@ ATTR_ZONE_NUMBER = "Zone number" async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Rachio switches.""" # Add all zones from all controllers as switches - devices = await hass.async_add_executor_job(_create_devices, hass, config_entry) - async_add_entities(devices) - _LOGGER.info("%d Rachio switch(es) added", len(devices)) + entities = await hass.async_add_executor_job(_create_entities, hass, config_entry) + async_add_entities(entities) + _LOGGER.info("%d Rachio switch(es) added", len(entities)) -def _create_devices(hass, config_entry): - devices = [] +def _create_entities(hass, config_entry): + entities = [] person = hass.data[DOMAIN_RACHIO][config_entry.entry_id] # Fetch the schedule once at startup # in order to avoid every zone doing it for controller in person.controllers: - devices.append(RachioStandbySwitch(hass, controller)) + entities.append(RachioStandbySwitch(controller)) zones = controller.list_zones() current_schedule = controller.current_schedule _LOGGER.debug("Rachio setting up zones: %s", zones) for zone in zones: _LOGGER.debug("Rachio setting up zone: %s", zone) - devices.append(RachioZone(hass, person, controller, zone, current_schedule)) - return devices + entities.append(RachioZone(person, controller, zone, current_schedule)) + return entities -class RachioSwitch(SwitchDevice): +class RachioSwitch(RachioDeviceInfoProvider, SwitchDevice): """Represent a Rachio state that can be toggled.""" def __init__(self, controller, poll=True): """Initialize a new Rachio switch.""" - self._controller = controller + super().__init__(controller) if poll: self._state = self._poll_update() @@ -104,18 +103,6 @@ class RachioSwitch(SwitchDevice): # For this device self._handle_update(args, kwargs) - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN_RACHIO, self._controller.serial_number,)}, - "connections": { - (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) - }, - "name": self._controller.name, - "manufacturer": DEFAULT_NAME, - } - @abstractmethod def _handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook data.""" @@ -125,11 +112,8 @@ class RachioSwitch(SwitchDevice): class RachioStandbySwitch(RachioSwitch): """Representation of a standby status/button.""" - def __init__(self, hass, controller): + def __init__(self, controller): """Instantiate a new Rachio standby mode switch.""" - dispatcher_connect( - hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update - ) super().__init__(controller, poll=True) self._poll_update(controller.init_data) @@ -172,11 +156,17 @@ class RachioStandbySwitch(RachioSwitch): """Resume controller functionality.""" self._controller.rachio.device.on(self._controller.controller_id) + async def async_added_to_hass(self): + """Subscribe to updates.""" + async_dispatcher_connect( + self.hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update + ) + class RachioZone(RachioSwitch): """Representation of one zone of sprinklers connected to the Rachio Iro.""" - def __init__(self, hass, person, controller, data, current_schedule): + def __init__(self, person, controller, data, current_schedule): """Initialize a new Rachio Zone.""" self._id = data[KEY_ID] self._zone_name = data[KEY_NAME] @@ -189,9 +179,6 @@ class RachioZone(RachioSwitch): super().__init__(controller, poll=False) self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) - # Listen for all zone updates - dispatcher_connect(hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update) - def __str__(self): """Display the zone as a string.""" return 'Rachio Zone "{}" on {}'.format(self.name, str(self._controller)) @@ -272,3 +259,9 @@ class RachioZone(RachioSwitch): self._state = False self.schedule_update_ha_state() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + async_dispatcher_connect( + self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update + ) From d36259f0671d05156a23bb623a9e2a831f3cbd55 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sun, 15 Mar 2020 19:05:10 -0700 Subject: [PATCH 060/431] Add Abode tests (#32562) * Add tests for several devices * Update coveragerc * Code review changes and minor clean up * More code review changes * Update manifest and minor test updates * Add test for locks and covers * Add tests for switch on and off * Add more complete test for alarms * Fix for camera test * Patch abodepy.mode for tests * Add test for unknown alarm state and minor cleanup * Update to make tests more robust * More specific tests * Update quality scale to silver * Remove integration quality scale --- .coveragerc | 9 - tests/components/abode/common.py | 25 + tests/components/abode/conftest.py | 22 + .../abode/test_alarm_control_panel.py | 140 +++ tests/components/abode/test_binary_sensor.py | 39 + tests/components/abode/test_camera.py | 40 + tests/components/abode/test_cover.py | 65 ++ tests/components/abode/test_init.py | 43 + tests/components/abode/test_light.py | 119 +++ tests/components/abode/test_lock.py | 62 ++ tests/components/abode/test_sensor.py | 44 + tests/components/abode/test_switch.py | 125 +++ tests/fixtures/abode_automation.json | 38 + tests/fixtures/abode_automation_changed.json | 38 + tests/fixtures/abode_devices.json | 799 ++++++++++++++++++ tests/fixtures/abode_login.json | 115 +++ tests/fixtures/abode_oauth_claims.json | 5 + tests/fixtures/abode_panel.json | 185 ++++ 18 files changed, 1904 insertions(+), 9 deletions(-) create mode 100644 tests/components/abode/common.py create mode 100644 tests/components/abode/conftest.py create mode 100644 tests/components/abode/test_alarm_control_panel.py create mode 100644 tests/components/abode/test_binary_sensor.py create mode 100644 tests/components/abode/test_camera.py create mode 100644 tests/components/abode/test_cover.py create mode 100644 tests/components/abode/test_init.py create mode 100644 tests/components/abode/test_light.py create mode 100644 tests/components/abode/test_lock.py create mode 100644 tests/components/abode/test_sensor.py create mode 100644 tests/components/abode/test_switch.py create mode 100644 tests/fixtures/abode_automation.json create mode 100644 tests/fixtures/abode_automation_changed.json create mode 100644 tests/fixtures/abode_devices.json create mode 100644 tests/fixtures/abode_login.json create mode 100644 tests/fixtures/abode_oauth_claims.json create mode 100644 tests/fixtures/abode_panel.json diff --git a/.coveragerc b/.coveragerc index c94199d6451..83285b9bd6c 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,15 +8,6 @@ omit = homeassistant/scripts/*.py # omit pieces of code that rely on external devices being present - homeassistant/components/abode/__init__.py - homeassistant/components/abode/alarm_control_panel.py - homeassistant/components/abode/binary_sensor.py - homeassistant/components/abode/camera.py - homeassistant/components/abode/cover.py - homeassistant/components/abode/light.py - homeassistant/components/abode/lock.py - homeassistant/components/abode/sensor.py - homeassistant/components/abode/switch.py homeassistant/components/acer_projector/switch.py homeassistant/components/actiontec/device_tracker.py homeassistant/components/adguard/__init__.py diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py new file mode 100644 index 00000000000..aabc732daa2 --- /dev/null +++ b/tests/components/abode/common.py @@ -0,0 +1,25 @@ +"""Common methods used across tests for Abode.""" +from unittest.mock import patch + +from homeassistant.components.abode import DOMAIN as ABODE_DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_platform(hass, platform): + """Set up the Abode platform.""" + mock_entry = MockConfigEntry( + domain=ABODE_DOMAIN, + data={CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}, + ) + mock_entry.add_to_hass(hass) + + with patch("homeassistant.components.abode.ABODE_PLATFORMS", [platform]), patch( + "abodepy.event_controller.sio" + ), patch("abodepy.utils.save_cache"): + assert await async_setup_component(hass, ABODE_DOMAIN, {}) + await hass.async_block_till_done() + + return mock_entry diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py new file mode 100644 index 00000000000..92f9bb09681 --- /dev/null +++ b/tests/components/abode/conftest.py @@ -0,0 +1,22 @@ +"""Configuration for Abode tests.""" +import abodepy.helpers.constants as CONST +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(autouse=True) +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")) + # Mocks the oauth claims response for abodepy. + requests_mock.get( + CONST.OAUTH_TOKEN_URL, text=load_fixture("abode_oauth_claims.json") + ) + # Mocks the panel response for abodepy. + requests_mock.get(CONST.PANEL_URL, text=load_fixture("abode_panel.json")) + # Mocks the automations response for abodepy. + requests_mock.get(CONST.AUTOMATION_URL, text=load_fixture("abode_automation.json")) + # Mocks the devices response for abodepy. + requests_mock.get(CONST.DEVICES_URL, text=load_fixture("abode_devices.json")) diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py new file mode 100644 index 00000000000..ca546157c93 --- /dev/null +++ b/tests/components/abode/test_alarm_control_panel.py @@ -0,0 +1,140 @@ +"""Tests for the Abode alarm control panel device.""" +from unittest.mock import PropertyMock, patch + +import abodepy.helpers.constants as CONST + +from homeassistant.components.abode import ATTR_DEVICE_ID +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + SERVICE_ALARM_ARM_AWAY, + SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_DISARM, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_DISARMED, +) + +from .common import setup_platform + +DEVICE_ID = "alarm_control_panel.abode_alarm" + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, ALARM_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(DEVICE_ID) + # Abode alarm device unique_id is the MAC address + assert entry.unique_id == "001122334455" + + +async def test_attributes(hass): + """Test the alarm control panel attributes are correct.""" + await setup_platform(hass, ALARM_DOMAIN) + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_ALARM_DISARMED + assert state.attributes.get(ATTR_DEVICE_ID) == "area_1" + assert not state.attributes.get("battery_backup") + assert not state.attributes.get("cellular_backup") + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Abode Alarm" + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 3 + + +async def test_set_alarm_away(hass): + """Test the alarm control panel can be set to away.""" + with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: + with patch("abodepy.ALARM.AbodeAlarm.set_away") as mock_set_away: + await setup_platform(hass, ALARM_DOMAIN) + + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_AWAY, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_away.assert_called_once() + + with patch( + "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock, + ) as mock_mode: + mock_mode.return_value = CONST.MODE_AWAY + + update_callback = mock_callback.call_args[0][1] + await hass.async_add_executor_job(update_callback, "area_1") + await hass.async_block_till_done() + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_ALARM_ARMED_AWAY + + +async def test_set_alarm_home(hass): + """Test the alarm control panel can be set to home.""" + with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: + with patch("abodepy.ALARM.AbodeAlarm.set_home") as mock_set_home: + await setup_platform(hass, ALARM_DOMAIN) + + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_ARM_HOME, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_home.assert_called_once() + + with patch( + "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock + ) as mock_mode: + mock_mode.return_value = CONST.MODE_HOME + + update_callback = mock_callback.call_args[0][1] + await hass.async_add_executor_job(update_callback, "area_1") + await hass.async_block_till_done() + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_ALARM_ARMED_HOME + + +async def test_set_alarm_standby(hass): + """Test the alarm control panel can be set to standby.""" + with patch("abodepy.AbodeEventController.add_device_callback") as mock_callback: + with patch("abodepy.ALARM.AbodeAlarm.set_standby") as mock_set_standby: + await setup_platform(hass, ALARM_DOMAIN) + await hass.services.async_call( + ALARM_DOMAIN, + SERVICE_ALARM_DISARM, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_standby.assert_called_once() + + with patch( + "abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock + ) as mock_mode: + mock_mode.return_value = CONST.MODE_STANDBY + + update_callback = mock_callback.call_args[0][1] + await hass.async_add_executor_job(update_callback, "area_1") + await hass.async_block_till_done() + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_ALARM_DISARMED + + +async def test_state_unknown(hass): + """Test an unknown alarm control panel state.""" + with patch("abodepy.ALARM.AbodeAlarm.mode", new_callable=PropertyMock) as mock_mode: + await setup_platform(hass, ALARM_DOMAIN) + await hass.async_block_till_done() + + mock_mode.return_value = None + + state = hass.states.get(DEVICE_ID) + assert state.state == "unknown" diff --git a/tests/components/abode/test_binary_sensor.py b/tests/components/abode/test_binary_sensor.py new file mode 100644 index 00000000000..aced7f33e73 --- /dev/null +++ b/tests/components/abode/test_binary_sensor.py @@ -0,0 +1,39 @@ +"""Tests for the Abode binary sensor device.""" +from homeassistant.components.abode import ATTR_DEVICE_ID +from homeassistant.components.abode.const import ATTRIBUTION +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_DOOR, + DOMAIN as BINARY_SENSOR_DOMAIN, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + STATE_OFF, +) + +from .common import setup_platform + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, BINARY_SENSOR_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("binary_sensor.front_door") + assert entry.unique_id == "2834013428b6035fba7d4054aa7b25a3" + + +async def test_attributes(hass): + """Test the binary sensor attributes are correct.""" + await setup_platform(hass, BINARY_SENSOR_DOMAIN) + + state = hass.states.get("binary_sensor.front_door") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_DEVICE_ID) == "RF:01430030" + assert not state.attributes.get("battery_low") + assert not state.attributes.get("no_response") + assert state.attributes.get("device_type") == "Door Contact" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Front Door" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DOOR diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py new file mode 100644 index 00000000000..8b11671a456 --- /dev/null +++ b/tests/components/abode/test_camera.py @@ -0,0 +1,40 @@ +"""Tests for the Abode camera device.""" +from unittest.mock import patch + +from homeassistant.components.abode.const import DOMAIN as ABODE_DOMAIN +from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN +from homeassistant.const import ATTR_ENTITY_ID, STATE_IDLE + +from .common import setup_platform + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, CAMERA_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("camera.test_cam") + assert entry.unique_id == "d0a3a1c316891ceb00c20118aae2a133" + + +async def test_attributes(hass): + """Test the camera attributes are correct.""" + await setup_platform(hass, CAMERA_DOMAIN) + + state = hass.states.get("camera.test_cam") + assert state.state == STATE_IDLE + + +async def test_capture_image(hass): + """Test the camera capture image service.""" + await setup_platform(hass, CAMERA_DOMAIN) + + with patch("abodepy.AbodeCamera.capture") as mock_capture: + await hass.services.async_call( + ABODE_DOMAIN, + "capture_image", + {ATTR_ENTITY_ID: "camera.test_cam"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_capture.assert_called_once() diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py new file mode 100644 index 00000000000..bb1b8fceffb --- /dev/null +++ b/tests/components/abode/test_cover.py @@ -0,0 +1,65 @@ +"""Tests for the Abode cover device.""" +from unittest.mock import patch + +from homeassistant.components.abode import ATTR_DEVICE_ID +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + STATE_CLOSED, +) + +from .common import setup_platform + +DEVICE_ID = "cover.garage_door" + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, COVER_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(DEVICE_ID) + assert entry.unique_id == "61cbz3b542d2o33ed2fz02721bda3324" + + +async def test_attributes(hass): + """Test the cover attributes are correct.""" + await setup_platform(hass, COVER_DOMAIN) + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_CLOSED + assert state.attributes.get(ATTR_DEVICE_ID) == "ZW:00000007" + assert not state.attributes.get("battery_low") + assert not state.attributes.get("no_response") + assert state.attributes.get("device_type") == "Secure Barrier" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Garage Door" + + +async def test_open(hass): + """Test the cover can be opened.""" + await setup_platform(hass, COVER_DOMAIN) + + with patch("abodepy.AbodeCover.open_cover") as mock_open: + await hass.services.async_call( + COVER_DOMAIN, SERVICE_OPEN_COVER, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_open.assert_called_once() + + +async def test_close(hass): + """Test the cover can be closed.""" + await setup_platform(hass, COVER_DOMAIN) + + with patch("abodepy.AbodeCover.close_cover") as mock_close: + await hass.services.async_call( + COVER_DOMAIN, + SERVICE_CLOSE_COVER, + {ATTR_ENTITY_ID: DEVICE_ID}, + blocking=True, + ) + await hass.async_block_till_done() + mock_close.assert_called_once() diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py new file mode 100644 index 00000000000..3f73ccd77ce --- /dev/null +++ b/tests/components/abode/test_init.py @@ -0,0 +1,43 @@ +"""Tests for the Abode module.""" +from unittest.mock import patch + +from homeassistant.components.abode import ( + DOMAIN as ABODE_DOMAIN, + SERVICE_CAPTURE_IMAGE, + SERVICE_SETTINGS, + SERVICE_TRIGGER_AUTOMATION, +) +from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN + +from .common import setup_platform + + +async def test_change_settings(hass): + """Test change_setting service.""" + await setup_platform(hass, ALARM_DOMAIN) + + with patch("abodepy.Abode.set_setting") as mock_set_setting: + await hass.services.async_call( + ABODE_DOMAIN, + SERVICE_SETTINGS, + {"setting": "confirm_snd", "value": "loud"}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_setting.assert_called_once() + + +async def test_unload_entry(hass): + """Test unloading the Abode entry.""" + mock_entry = await setup_platform(hass, ALARM_DOMAIN) + + with patch("abodepy.Abode.logout") as mock_logout, patch( + "abodepy.event_controller.AbodeEventController.stop" + ) as mock_events_stop: + assert await hass.config_entries.async_unload(mock_entry.entry_id) + mock_logout.assert_called_once() + mock_events_stop.assert_called_once() + + assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_SETTINGS) + assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE) + assert not hass.services.has_service(ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION) diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py new file mode 100644 index 00000000000..f0eee4b209b --- /dev/null +++ b/tests/components/abode/test_light.py @@ -0,0 +1,119 @@ +"""Tests for the Abode light device.""" +from unittest.mock import patch + +from homeassistant.components.abode import ATTR_DEVICE_ID +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_RGB_COLOR, + DOMAIN as LIGHT_DOMAIN, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + ATTR_SUPPORTED_FEATURES, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_ON, +) + +from .common import setup_platform + +DEVICE_ID = "light.living_room_lamp" + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, LIGHT_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(DEVICE_ID) + assert entry.unique_id == "741385f4388b2637df4c6b398fe50581" + + +async def test_attributes(hass): + """Test the light attributes are correct.""" + await setup_platform(hass, LIGHT_DOMAIN) + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_ON + assert state.attributes.get(ATTR_BRIGHTNESS) == 204 + assert state.attributes.get(ATTR_RGB_COLOR) == (0, 63, 255) + assert state.attributes.get(ATTR_COLOR_TEMP) == 280 + assert state.attributes.get(ATTR_DEVICE_ID) == "ZB:db5b1a" + assert not state.attributes.get("battery_low") + assert not state.attributes.get("no_response") + assert state.attributes.get("device_type") == "RGB Dimmer" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Living Room Lamp" + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 19 + + +async def test_switch_off(hass): + """Test the light can be turned off.""" + await setup_platform(hass, LIGHT_DOMAIN) + + with patch("abodepy.AbodeLight.switch_off") as mock_switch_off: + assert await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_switch_off.assert_called_once() + + +async def test_switch_on(hass): + """Test the light can be turned on.""" + await setup_platform(hass, LIGHT_DOMAIN) + + with patch("abodepy.AbodeLight.switch_on") as mock_switch_on: + await hass.services.async_call( + LIGHT_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_switch_on.assert_called_once() + + +async def test_set_brightness(hass): + """Test the brightness can be set.""" + await setup_platform(hass, LIGHT_DOMAIN) + + with patch("abodepy.AbodeLight.set_level") as mock_set_level: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEVICE_ID, "brightness": 100}, + blocking=True, + ) + await hass.async_block_till_done() + # Brightness is converted in abode.light.AbodeLight.turn_on + mock_set_level.assert_called_once_with(39) + + +async def test_set_color(hass): + """Test the color can be set.""" + await setup_platform(hass, LIGHT_DOMAIN) + + with patch("abodepy.AbodeLight.set_color") as mock_set_color: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEVICE_ID, "hs_color": [240, 100]}, + blocking=True, + ) + await hass.async_block_till_done() + mock_set_color.assert_called_once_with((240.0, 100.0)) + + +async def test_set_color_temp(hass): + """Test the color temp can be set.""" + await setup_platform(hass, LIGHT_DOMAIN) + + with patch("abodepy.AbodeLight.set_color_temp") as mock_set_color_temp: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: DEVICE_ID, "color_temp": 309}, + blocking=True, + ) + await hass.async_block_till_done() + # Color temp is converted in abode.light.AbodeLight.turn_on + mock_set_color_temp.assert_called_once_with(3236) diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py new file mode 100644 index 00000000000..45e17861d33 --- /dev/null +++ b/tests/components/abode/test_lock.py @@ -0,0 +1,62 @@ +"""Tests for the Abode lock device.""" +from unittest.mock import patch + +from homeassistant.components.abode import ATTR_DEVICE_ID +from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_FRIENDLY_NAME, + SERVICE_LOCK, + SERVICE_UNLOCK, + STATE_LOCKED, +) + +from .common import setup_platform + +DEVICE_ID = "lock.test_lock" + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, LOCK_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(DEVICE_ID) + assert entry.unique_id == "51cab3b545d2o34ed7fz02731bda5324" + + +async def test_attributes(hass): + """Test the lock attributes are correct.""" + await setup_platform(hass, LOCK_DOMAIN) + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_LOCKED + assert state.attributes.get(ATTR_DEVICE_ID) == "ZW:00000004" + assert not state.attributes.get("battery_low") + assert not state.attributes.get("no_response") + assert state.attributes.get("device_type") == "Door Lock" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Test Lock" + + +async def test_lock(hass): + """Test the lock can be locked.""" + await setup_platform(hass, LOCK_DOMAIN) + + with patch("abodepy.AbodeLock.lock") as mock_lock: + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_LOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_lock.assert_called_once() + + +async def test_unlock(hass): + """Test the lock can be unlocked.""" + await setup_platform(hass, LOCK_DOMAIN) + + with patch("abodepy.AbodeLock.unlock") as mock_unlock: + await hass.services.async_call( + LOCK_DOMAIN, SERVICE_UNLOCK, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + mock_unlock.assert_called_once() diff --git a/tests/components/abode/test_sensor.py b/tests/components/abode/test_sensor.py new file mode 100644 index 00000000000..bfe20be0b8c --- /dev/null +++ b/tests/components/abode/test_sensor.py @@ -0,0 +1,44 @@ +"""Tests for the Abode sensor device.""" +from homeassistant.components.abode import ATTR_DEVICE_ID +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_FRIENDLY_NAME, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_HUMIDITY, +) + +from .common import setup_platform + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, SENSOR_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get("sensor.environment_sensor_humidity") + assert entry.unique_id == "13545b21f4bdcd33d9abd461f8443e65-humidity" + + +async def test_attributes(hass): + """Test the sensor attributes are correct.""" + await setup_platform(hass, SENSOR_DOMAIN) + + state = hass.states.get("sensor.environment_sensor_humidity") + assert state.state == "32.0" + assert state.attributes.get(ATTR_DEVICE_ID) == "RF:02148e70" + assert not state.attributes.get("battery_low") + assert not state.attributes.get("no_response") + assert state.attributes.get("device_type") == "LM" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "%" + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Environment Sensor Humidity" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + + state = hass.states.get("sensor.environment_sensor_lux") + assert state.state == "1.0" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "lux" + + state = hass.states.get("sensor.environment_sensor_temperature") + # Abodepy device JSON reports 19.5, but Home Assistant shows 19.4 + assert state.state == "19.4" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "°C" diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py new file mode 100644 index 00000000000..3ec9648d87d --- /dev/null +++ b/tests/components/abode/test_switch.py @@ -0,0 +1,125 @@ +"""Tests for the Abode switch device.""" +from unittest.mock import patch + +from homeassistant.components.abode import ( + DOMAIN as ABODE_DOMAIN, + SERVICE_TRIGGER_AUTOMATION, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + ATTR_ENTITY_ID, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + STATE_OFF, + STATE_ON, +) + +from .common import setup_platform + +AUTOMATION_ID = "switch.test_automation" +AUTOMATION_UID = "47fae27488f74f55b964a81a066c3a01" +DEVICE_ID = "switch.test_switch" +DEVICE_UID = "0012a4d3614cb7e2b8c9abea31d2fb2a" + + +async def test_entity_registry(hass): + """Tests that the devices are registered in the entity registry.""" + await setup_platform(hass, SWITCH_DOMAIN) + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entry = entity_registry.async_get(AUTOMATION_ID) + assert entry.unique_id == AUTOMATION_UID + + entry = entity_registry.async_get(DEVICE_ID) + assert entry.unique_id == DEVICE_UID + + +async def test_attributes(hass): + """Test the switch attributes are correct.""" + await setup_platform(hass, SWITCH_DOMAIN) + + state = hass.states.get(DEVICE_ID) + assert state.state == STATE_OFF + + +async def test_switch_on(hass): + """Test the switch can be turned on.""" + await setup_platform(hass, SWITCH_DOMAIN) + + with patch("abodepy.AbodeSwitch.switch_on") as mock_switch_on: + assert await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + + mock_switch_on.assert_called_once() + + +async def test_switch_off(hass): + """Test the switch can be turned off.""" + await setup_platform(hass, SWITCH_DOMAIN) + + with patch("abodepy.AbodeSwitch.switch_off") as mock_switch_off: + assert await hass.services.async_call( + SWITCH_DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: DEVICE_ID}, blocking=True + ) + await hass.async_block_till_done() + + mock_switch_off.assert_called_once() + + +async def test_automation_attributes(hass): + """Test the automation attributes are correct.""" + await setup_platform(hass, SWITCH_DOMAIN) + + state = hass.states.get(AUTOMATION_ID) + # State is set based on "enabled" key in automation JSON. + assert state.state == STATE_ON + + +async def test_turn_automation_off(hass): + """Test the automation can be turned off.""" + with patch("abodepy.AbodeAutomation.enable") as mock_trigger: + await setup_platform(hass, SWITCH_DOMAIN) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: AUTOMATION_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_trigger.assert_called_once_with(False) + + +async def test_turn_automation_on(hass): + """Test the automation can be turned on.""" + with patch("abodepy.AbodeAutomation.enable") as mock_trigger: + await setup_platform(hass, SWITCH_DOMAIN) + + await hass.services.async_call( + SWITCH_DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: AUTOMATION_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock_trigger.assert_called_once_with(True) + + +async def test_trigger_automation(hass, requests_mock): + """Test the trigger automation service.""" + await setup_platform(hass, SWITCH_DOMAIN) + + with patch("abodepy.AbodeAutomation.trigger") as mock: + await hass.services.async_call( + ABODE_DOMAIN, + SERVICE_TRIGGER_AUTOMATION, + {ATTR_ENTITY_ID: AUTOMATION_ID}, + blocking=True, + ) + await hass.async_block_till_done() + + mock.assert_called_once() diff --git a/tests/fixtures/abode_automation.json b/tests/fixtures/abode_automation.json new file mode 100644 index 00000000000..fb1c00faff9 --- /dev/null +++ b/tests/fixtures/abode_automation.json @@ -0,0 +1,38 @@ +{ + "name": "Test Automation", + "enabled": "True", + "version": 2, + "id": "47fae27488f74f55b964a81a066c3a01", + "subType": "", + "actions": [ + { + "directive": { + "trait": "panel.traits.panelMode", + "name": "panel.directives.arm", + "state": { + "panelMode": "AWAY" + } + } + } + ], + "conditions": {}, + "triggers": { + "operator": "OR", + "expressions": [ + { + "mobileDevices": [ + "89381", + "658" + ], + "property": { + "trait": "mobile.traits.location", + "name": "location", + "rule": { + "location": "31675", + "equalTo": "LAST_OUT" + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/abode_automation_changed.json b/tests/fixtures/abode_automation_changed.json new file mode 100644 index 00000000000..39b874c4dfc --- /dev/null +++ b/tests/fixtures/abode_automation_changed.json @@ -0,0 +1,38 @@ +{ + "name": "Test Automation", + "enabled": "False", + "version": 2, + "id": "47fae27488f74f55b964a81a066c3a01", + "subType": "", + "actions": [ + { + "directive": { + "trait": "panel.traits.panelMode", + "name": "panel.directives.arm", + "state": { + "panelMode": "AWAY" + } + } + } + ], + "conditions": {}, + "triggers": { + "operator": "OR", + "expressions": [ + { + "mobileDevices": [ + "89381", + "658" + ], + "property": { + "trait": "mobile.traits.location", + "name": "location", + "rule": { + "location": "31675", + "equalTo": "LAST_OUT" + } + } + } + ] + } +} \ No newline at end of file diff --git a/tests/fixtures/abode_devices.json b/tests/fixtures/abode_devices.json new file mode 100644 index 00000000000..370b264427a --- /dev/null +++ b/tests/fixtures/abode_devices.json @@ -0,0 +1,799 @@ +[ + { + "id": "RF:01430030", + "type_tag": "device_type.door_contact", + "type": "Door Contact", + "name": "Front Door", + "area": "1", + "zone": "15", + "sort_order": "", + "is_window": "1", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "3", + "sresp_entry_0": "3", + "sresp_exit_0": "0", + "group_name": "Doors and Windows", + "group_id": "397972", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "1", + "sresp_entry_1": "1", + "sresp_exit_1": "0", + "sresp_mode_2": "1", + "sresp_entry_2": "1", + "sresp_exit_2": "0", + "sresp_mode_3": "1", + "uuid": "2834013428b6035fba7d4054aa7b25a3", + "sresp_entry_3": "1", + "sresp_exit_3": "0", + "sresp_mode_4": "1", + "sresp_entry_4": "1", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Closed", + "status_display": "Closed", + "statuses": { + "open": "0" + }, + "status_ex": "", + "actions": [], + "status_icons": { + "Open": "assets/icons/WindowOpened.svg", + "Closed": "assets/icons/WindowClosed.svg" + }, + "icon": "assets/icons/doorsensor-a.svg", + "sresp_trigger": "0", + "sresp_restore": "0" + }, + { + "id": "RF:01c34a30", + "type_tag": "device_type.povs", + "type": "Occupancy", + "name": "Hallway Motion", + "area": "1", + "zone": "17", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "5", + "sresp_entry_1": "4", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "4", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "ba2c7e8d4430da8d34c31425a2823fe0", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Online", + "status_display": "Online", + "statuses": { + "motion": "0" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/motioncamera-a.svg", + "sresp_trigger": "0", + "sresp_restore": "0", + "occupancy_timer": null, + "sensitivity": null, + "model": "L1", + "is_motion_sensor": true + }, + { + "id": "SR:PIR", + "type_tag": "device_type.pir", + "type": "Motion Sensor", + "name": "Living Room Motion", + "area": "1", + "zone": "2", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Motion", + "group_id": "397973", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "5", + "sresp_entry_1": "4", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "4", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "2f1bc34ceadac032af4fc9189ef821a8", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "1", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Online", + "status_display": "Online", + "statuses": [], + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/motioncamera-a.svg", + "schar_obpir_sens": "15", + "schar_obpir_pulse": "2", + "sensitivity": "15", + "model": "L1" + }, + { + "id": "ZB:db5b1a", + "type_tag": "device_type.hue", + "type": "RGB Dimmer", + "name": "Living Room Lamp", + "area": "1", + "zone": "21", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "741385f4388b2637df4c6b398fe50581", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "LCT014", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/light/ZB:db5b1a", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "On", + "status_display": "On", + "statuses": { + "saturation": 100, + "hue": 225, + "level": "79", + "switch": "1", + "color_temp": 3571, + "color_mode": "0" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/bulb-1.svg", + "statusEx": "0" + }, + { + "id": "ZB:db5b1b", + "type_tag": "device_type.hue", + "type": "Dimmer", + "name": "Test Dimmer Only Device", + "area": "1", + "zone": "21", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "641385f4388b2637df4c6b398fe50581", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "LCT014", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/light/ZB:db5b1b", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "On", + "status_display": "On", + "statuses": { + "saturation": 100, + "hue": 225, + "level": "100", + "switch": "1", + "color_temp": 3571, + "color_mode": "2" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/bulb-1.svg", + "statusEx": "0" + }, + { + "id": "ZB:db5b1c", + "type_tag": "device_type.dimmer", + "type": "Light", + "name": "Test Non-dimmer Device", + "area": "1", + "zone": "21", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "641385f4388b2637df4c6b398fe50583", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "LCT014", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/light/ZB:db5b1c", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "On", + "status_display": "On", + "statuses": { + "switch": "1" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/bulb-1.svg", + "statusEx": "0" + }, + { + "id": "RF:02148e70", + "type_tag": "device_type.lm", + "type": "LM", + "name": "Environment Sensor", + "area": "1", + "zone": "24", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Ungrouped", + "group_id": "1", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "13545b21f4bdcd33d9abd461f8443e65", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "67 \u00b0F", + "status_display": "Online", + "statuses": { + "temperature": "67 \u00b0F", + "temp": "19.5", + "lux": "1 lx", + "humidity": "32 %" + }, + "status_ex": "", + "actions": [ + { + "label": "High Humidity Alarm", + "value": "a=1&z=24&trigger=HMH;" + }, + { + "label": "Low Humidity Alarm", + "value": "a=1&z=24&trigger=HML;" + }, + { + "label": "High Temperature Alarm", + "value": "a=1&z=24&trigger=TSH;" + }, + { + "label": "Low Temperature Alarm", + "value": "a=1&z=24&trigger=TSL;" + } + ], + "status_icons": [], + "icon": "assets/icons/occupancy-sensor.svg", + "statusEx": "1" + }, + { + "id": "ZW:0000000b", + "type_tag": "device_type.power_switch_sensor", + "type": "Power Switch Sensor", + "name": "Test Switch", + "area": "1", + "zone": "23", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Lighting", + "group_id": "377075", + "default_group_id": "1", + "sort_id": "7", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "0012a4d3614cb7e2b8c9abea31d2fb2a", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "006349523032", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/power_switch/ZW:0000000b", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Off", + "status_display": "OFF", + "statuses": { + "switch": "0" + }, + "status_ex": "", + "actions": [], + "status_icons": [], + "icon": "assets/icons/plug.svg" + }, + { + "id": "XF:b0c5ba27592a", + "type_tag": "device_type.ipcam", + "type": "IP Cam", + "name": "Test Cam", + "area": "1", + "zone": "1", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "1", + "sresp_24hr": "5", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Streaming Camera", + "group_id": "397893", + "default_group_id": "1", + "sort_id": "10000", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "d0a3a1c316891ceb00c20118aae2a133", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", + "origin": "abode", + "has_subscription": null, + "onboard": "1", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/cams/XF:b0c5ba27592a/record", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "Online", + "status_display": "Online", + "statuses": [], + "status_ex": "", + "actions": [ + { + "label": "Capture Video", + "value": "a=1&z=1&req=vid;" + }, + { + "label": "Turn off Live Video", + "value": "a=1&z=1&privacy=on;" + }, + { + "label": "Turn on Live Video", + "value": "a=1&z=1&privacy=off;" + } + ], + "status_icons": [], + "icon": "assets/icons/streaming-camaera-new.svg", + "control_url_snapshot": "api/v1/cams/XF:b0c5ba27592a/capture", + "ptt_supported": true, + "is_new_camera": 1, + "stream_quality": 2, + "camera_mac": "A0:C1:B2:C3:45:6D", + "privacy": "1", + "enable_audio": "1", + "alarm_video": "25", + "pre_alarm_video": "5", + "mic_volume": "75", + "speaker_volume": "75", + "mic_default_volume": 40, + "speaker_default_volume": 46, + "bandwidth": { + "slider_labels": [ + { + "name": "High", + "value": 3 + }, + { + "name": "Medium", + "value": 2 + }, + { + "name": "Low", + "value": 1 + } + ], + "min": 1, + "max": 3, + "step": 1 + }, + "volume": { + "min": 0, + "max": 100, + "step": 1 + }, + "video_flip": "0", + "hframe": "480P" + }, + { + "id": "ZW:00000004", + "type_tag": "device_type.door_lock", + "type": "Door Lock", + "name": "Test Lock", + "area": "1", + "zone": "16", + "sort_order": "", + "is_window": "", + "bypass": "0", + "schar_24hr": "0", + "sresp_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "group_name": "Doors/Windows", + "group_id": "377028", + "default_group_id": "1", + "sort_id": "1", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "51cab3b545d2o34ed7fz02731bda5324", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "sresp_mode_4": "0", + "sresp_entry_4": "0", + "sresp_exit_4": "0", + "version": "", + "origin": "abode", + "has_subscription": null, + "onboard": "0", + "s2_grnt_keys": "", + "s2_dsk": "", + "s2_propty": "", + "s2_keys_valid": "", + "zwave_secure_protocol": "", + "control_url": "api/v1/control/lock/ZW:00000004", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0, + "jammed": 0, + "zwave_fault": 0 + }, + "status": "LockClosed", + "status_display": "LockClosed", + "statuses": { + "open": "0" + }, + "status_ex": "", + "actions": [ + { + "label": "Lock", + "value": "a=1&z=16&sw=on;" + }, + { + "label": "Unlock", + "value": "a=1&z=16&sw=off;" + } + ], + "status_icons": { + "LockOpen": "assets/icons/unlocked-red.svg", + "LockClosed": "assets/icons/locked-green.svg" + }, + "icon": "assets/icons/automation-lock.svg", + "automation_settings": null + }, + { + "id": "ZW:00000007", + "type_tag": "device_type.secure_barrier", + "type": "Secure Barrier", + "name": "Garage Door", + "area": "1", + "zone": "11", + "sort_order": "0", + "is_window": "0", + "bypass": "0", + "schar_24hr": "0", + "sresp_mode_0": "0", + "sresp_entry_0": "0", + "sresp_exit_0": "0", + "sresp_mode_1": "0", + "sresp_entry_1": "0", + "sresp_exit_1": "0", + "sresp_mode_2": "0", + "sresp_entry_2": "0", + "sresp_exit_2": "0", + "sresp_mode_3": "0", + "uuid": "61cbz3b542d2o33ed2fz02721bda3324", + "sresp_entry_3": "0", + "sresp_exit_3": "0", + "capture_mode": null, + "origin": "abode", + "control_url": "api/v1/control/power_switch/ZW:00000007", + "deep_link": null, + "status_color": "#5cb85c", + "faults": { + "low_battery": 0, + "tempered": 0, + "supervision": 0, + "out_of_order": 0, + "no_response": 0 + }, + "status": "Closed", + "statuses": { + "hvac_mode": null + }, + "status_ex": "", + "actions": [ + { + "label": "Close", + "value": "a=1&z=11&sw=off;" + }, + { + "label": "Open", + "value": "a=1&z=11&sw=on;" + } + ], + "status_icons": { + "Open": "assets/icons/garage-door-red.svg", + "Closed": "assets/icons/garage-door-green.svg" + }, + "icon": "assets/icons/garage-door.svg" + } +] \ No newline at end of file diff --git a/tests/fixtures/abode_login.json b/tests/fixtures/abode_login.json new file mode 100644 index 00000000000..fb0ed1fd4ff --- /dev/null +++ b/tests/fixtures/abode_login.json @@ -0,0 +1,115 @@ +{ + "token": "web-1eb04ba2236d85f49d4b9b4bb91665f2", + "expired_at": "2017-06-05 00:14:12", + "initiate_screen": "timeline", + "user": { + "id": "user@email.com", + "email": "user@email.com", + "first_name": "John", + "last_name": "Doe", + "phone": "5555551212", + "profile_pic": "https://website.com/default-image.svg", + "address": "555 None St.", + "city": "New York City", + "state": "NY", + "zip": "10108", + "country": "US", + "longitude": "0", + "latitude": "0", + "timezone": "America/New_York_City", + "verified": "1", + "plan": "Basic", + "plan_id": "0", + "plan_active": "1", + "cms_code": "1111", + "cms_active": "0", + "cms_started_at": "", + "cms_expiry": "", + "cms_ondemand": "", + "step": "-1", + "cms_permit_no": "", + "opted_plan_id": "", + "stripe_account": "1", + "plan_effective_from": "", + "agreement": "1", + "associate_users": "1", + "owner": "1" + }, + "panel": { + "version": "ABGW 0.0.2.17F ABGW-L1-XA36J 3.1.2.6.1 Z-Wave 3.95", + "report_account": "5555", + "online": "1", + "initialized": "1", + "net_version": "ABGW 0.0.2.17F", + "rf_version": "ABGW-L1-XA36J", + "zigbee_version": "3.1.2.6.1", + "z_wave_version": "Z-Wave 3.95", + "timezone": "America/New_York", + "ac_fail": "0", + "battery": "1", + "ip": "192.168.1.1", + "jam": "0", + "rssi": "2", + "setup_zone_1": "1", + "setup_zone_2": "1", + "setup_zone_3": "1", + "setup_zone_4": "1", + "setup_zone_5": "1", + "setup_zone_6": "1", + "setup_zone_7": "1", + "setup_zone_8": "1", + "setup_zone_9": "1", + "setup_zone_10": "1", + "setup_gateway": "1", + "setup_contacts": "1", + "setup_billing": "1", + "setup_users": "1", + "is_cellular": "False", + "plan_set_id": "1", + "dealer_id": "0", + "tz_diff": "-04:00", + "is_demo": "0", + "rf51_version": "ABGW-L1-XA36J", + "model": "L1", + "mac": "00:11:22:33:44:55", + "xml_version": "3", + "dealer_name": "abode", + "id": "0", + "dealer_address": "2625 Middlefield Road #900 Palo Alto CA 94306", + "dealer_domain": "https://my.goabode.com", + "domain_alias": "https://test.goabode.com", + "dealer_support_url": "https://support.goabode.com", + "app_launch_url": "https://goabode.app.link/abode", + "has_wifi": "0", + "mode": { + "area_1": "standby", + "area_2": "standby" + } + }, + "permissions": { + "premium_streaming": "0", + "guest_app": "0", + "family_app": "0", + "multiple_accounts": "1", + "google_voice": "1", + "nest": "1", + "alexa": "1", + "ifttt": "1", + "no_associates": "100", + "no_contacts": "2", + "no_devices": "155", + "no_ipcam": "100", + "no_quick_action": "25", + "no_automation": "75", + "media_storage": "3", + "cellular_backup": "0", + "cms_duration": "", + "cms_included": "0" + }, + "integrations": { + "nest": { + "is_connected": 0, + "is_home_selected": 0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/abode_oauth_claims.json b/tests/fixtures/abode_oauth_claims.json new file mode 100644 index 00000000000..2b313b9aa3e --- /dev/null +++ b/tests/fixtures/abode_oauth_claims.json @@ -0,0 +1,5 @@ +{ + "token_type": "Bearer", + "access_token": "ohyeahthisisanoauthtoken", + "expires_in": 3600 +} \ No newline at end of file diff --git a/tests/fixtures/abode_panel.json b/tests/fixtures/abode_panel.json new file mode 100644 index 00000000000..5a50ffe6fe7 --- /dev/null +++ b/tests/fixtures/abode_panel.json @@ -0,0 +1,185 @@ +{ + "version": "Z3 1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz 19200_UITRF1BD_BL.A30.20181117 4.1.2.6.2 Z-Wave 6.02 Bridge controller library", + "report_account": "12345", + "online": "1", + "initialized": "1", + "net_version": "1.0.2.22G_6.8E_homekit_2.0.9_s2 ABODE oz", + "rf_version": "19200_UITRF1BD_BL.A30.20181117", + "zigbee_version": "4.1.2.6.2", + "z_wave_version": "Z-Wave 6.02 Bridge controller library", + "timezone": "America/Los_Angeles", + "ac_fail": "0", + "battery": "0", + "ip": "", + "jam": "0", + "rssi": "1", + "setup_zone_1": "1", + "setup_zone_2": "1", + "setup_zone_3": "1", + "setup_zone_4": "1", + "setup_zone_5": "1", + "setup_zone_6": "1", + "setup_zone_7": "1", + "setup_zone_8": "1", + "setup_zone_9": "1", + "setup_zone_10": "1", + "setup_gateway": "1", + "setup_contacts": "1", + "setup_billing": "1", + "setup_users": "1", + "is_cellular": "0", + "plan_set_id": "7", + "dealer_id": "0", + "tz_diff": "-08:00", + "model": "Z3", + "has_wifi": "1", + "has_s2_support": "1", + "mode": { + "area_1": "standby", + "area_1_label": "Standby", + "area_2": "standby", + "area_2_label": "Standby" + }, + "areas": { + "1": { + "mode": "0", + "modes": { + "0": { + "area": "1", + "mode": "0", + "read_only": "1", + "is_set": "1", + "name": "standby", + "color": null, + "icon_id": null, + "entry1": null, + "entry2": null, + "exit": null, + "icon_path": null + }, + "1": { + "area": "1", + "mode": "1", + "read_only": "1", + "is_set": "1", + "name": "away", + "color": null, + "icon_id": null, + "entry1": "30", + "entry2": "60", + "exit": "30", + "icon_path": null + }, + "2": { + "area": "1", + "mode": "2", + "read_only": "1", + "is_set": "1", + "name": "home", + "color": null, + "icon_id": null, + "entry1": "30", + "entry2": "60", + "exit": "0", + "icon_path": null + }, + "3": { + "area": "1", + "mode": "3", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "4": { + "area": "1", + "mode": "4", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + } + } + }, + "2": { + "mode": "0", + "modes": { + "0": { + "area": "2", + "mode": "0", + "read_only": "1", + "is_set": "1", + "name": "standby", + "color": null, + "icon_id": null, + "entry1": null, + "entry2": null, + "exit": null, + "icon_path": null + }, + "1": { + "area": "2", + "mode": "1", + "read_only": "1", + "is_set": "1", + "name": "away", + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "2": { + "area": "2", + "mode": "2", + "read_only": "1", + "is_set": "1", + "name": "home", + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "3": { + "area": "2", + "mode": "3", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + }, + "4": { + "area": "2", + "mode": "4", + "read_only": "0", + "is_set": "0", + "name": null, + "color": null, + "icon_id": null, + "entry1": "60", + "entry2": "60", + "exit": "60", + "icon_path": null + } + } + } + } +} \ No newline at end of file From 6e95b90f42d8283754e17a9e7d8c2fbef32e0495 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 15 Mar 2020 20:42:07 -0700 Subject: [PATCH 061/431] Bump teslajsonpy to 0.5.1 (#32827) --- homeassistant/components/tesla/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/manifest.json b/homeassistant/components/tesla/manifest.json index 21605d16579..950a860b308 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": [ - "teslajsonpy==0.4.0" + "teslajsonpy==0.5.1" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 2d4a477c611..71500fbbb64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1996,7 +1996,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.tesla -teslajsonpy==0.4.0 +teslajsonpy==0.5.1 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5c0f26ee38..abc9d340dba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -690,7 +690,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.tesla -teslajsonpy==0.4.0 +teslajsonpy==0.5.1 # homeassistant.components.toon toonapilib==3.2.4 From cf8dfdae47570d8cb560546ff72b3fa15c4ada85 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sun, 15 Mar 2020 23:13:04 -0500 Subject: [PATCH 062/431] Add config flow to roku (#31988) * create a dedicated const.py * add DEFAULT_PORT to const.py * work on config flow conversion. * remove discovery. * work on config flow and add tests. other cleanup. * work on config flow and add tests. other cleanup. * add quality scale to manifest. * work on config flow and add tests. other cleanup. * review tweaks. * Update manifest.json * catch more specific errors * catch more errors. * impprt specific exceptions * import specific exceptions * Update __init__.py * Update config_flow.py * Update media_player.py * Update remote.py * Update media_player.py * Update remote.py * Update media_player.py * Update remote.py * Update config_flow.py * Update config_flow.py * Update media_player.py * Update __init__.py * Update __init__.py * Update config_flow.py * Update test_config_flow.py * Update config_flow.py * Update __init__.py * Update test_config_flow.py * Update remote.py * Update test_init.py * Update test_init.py * Update media_player.py * Update media_player.py * Update media_player.py --- .coveragerc | 4 +- .../components/roku/.translations/en.json | 27 ++ homeassistant/components/roku/__init__.py | 139 +++++----- homeassistant/components/roku/config_flow.py | 134 ++++++++++ homeassistant/components/roku/const.py | 6 + homeassistant/components/roku/manifest.json | 12 +- homeassistant/components/roku/media_player.py | 52 ++-- homeassistant/components/roku/remote.py | 62 +++-- homeassistant/components/roku/services.yaml | 2 - homeassistant/components/roku/strings.json | 27 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 7 + requirements_test_all.txt | 3 + tests/components/roku/__init__.py | 50 ++++ tests/components/roku/test_config_flow.py | 247 ++++++++++++++++++ tests/components/roku/test_init.py | 68 +++++ 16 files changed, 716 insertions(+), 125 deletions(-) create mode 100644 homeassistant/components/roku/.translations/en.json create mode 100644 homeassistant/components/roku/config_flow.py delete mode 100644 homeassistant/components/roku/services.yaml create mode 100644 homeassistant/components/roku/strings.json create mode 100644 tests/components/roku/__init__.py create mode 100644 tests/components/roku/test_config_flow.py create mode 100644 tests/components/roku/test_init.py diff --git a/.coveragerc b/.coveragerc index 83285b9bd6c..555dccadde7 100644 --- a/.coveragerc +++ b/.coveragerc @@ -592,7 +592,9 @@ omit = homeassistant/components/ring/camera.py homeassistant/components/ripple/sensor.py homeassistant/components/rocketchat/notify.py - homeassistant/components/roku/* + homeassistant/components/roku/__init__.py + homeassistant/components/roku/media_player.py + homeassistant/components/roku/remote.py homeassistant/components/roomba/vacuum.py homeassistant/components/route53/* homeassistant/components/rova/sensor.py diff --git a/homeassistant/components/roku/.translations/en.json b/homeassistant/components/roku/.translations/en.json new file mode 100644 index 00000000000..8dccd065121 --- /dev/null +++ b/homeassistant/components/roku/.translations/en.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Roku device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "data": {}, + "description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host or IP address" + }, + "description": "Enter your Roku information.", + "title": "Roku" + } + }, + "title": "Roku" + } +} diff --git a/homeassistant/components/roku/__init__.py b/homeassistant/components/roku/__init__.py index b84b6dd1e63..636260b510c 100644 --- a/homeassistant/components/roku/__init__.py +++ b/homeassistant/components/roku/__init__.py @@ -1,29 +1,22 @@ """Support for Roku.""" -import logging +import asyncio +from datetime import timedelta +from socket import gaierror as SocketGIAError +from typing import Dict +from requests.exceptions import RequestException from roku import Roku, RokuException import voluptuous as vol -from homeassistant.components.discovery import SERVICE_ROKU +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 SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_HOST -from homeassistant.helpers import discovery -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "roku" - -SERVICE_SCAN = "roku_scan" - -ATTR_ROKU = "roku" - -DATA_ROKU = "data_roku" - -NOTIFICATION_ID = "roku_notification" -NOTIFICATION_TITLE = "Roku Setup" -NOTIFICATION_SCAN_ID = "roku_scan_notification" -NOTIFICATION_SCAN_TITLE = "Roku Scan" +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv +from .const import DATA_CLIENT, DATA_DEVICE_INFO, DOMAIN CONFIG_SCHEMA = vol.Schema( { @@ -34,77 +27,67 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -# Currently no attributes but it might change later -ROKU_SCAN_SCHEMA = vol.Schema({}) +PLATFORMS = [MEDIA_PLAYER_DOMAIN, REMOTE_DOMAIN] +SCAN_INTERVAL = timedelta(seconds=30) -def setup(hass, config): - """Set up the Roku component.""" - hass.data[DATA_ROKU] = {} +def get_roku_data(host: str) -> dict: + """Retrieve a Roku instance and version info for the device.""" + roku = Roku(host) + roku_device_info = roku.device_info - def service_handler(service): - """Handle service calls.""" - if service.service == SERVICE_SCAN: - scan_for_rokus(hass) + return { + DATA_CLIENT: roku, + DATA_DEVICE_INFO: roku_device_info, + } - def roku_discovered(service, info): - """Set up an Roku that was auto discovered.""" - _setup_roku(hass, config, {CONF_HOST: info["host"]}) - discovery.listen(hass, SERVICE_ROKU, roku_discovered) +async def async_setup(hass: HomeAssistant, config: Dict) -> bool: + """Set up the Roku integration.""" + hass.data.setdefault(DOMAIN, {}) - for conf in config.get(DOMAIN, []): - _setup_roku(hass, config, conf) - - hass.services.register( - DOMAIN, SERVICE_SCAN, service_handler, schema=ROKU_SCAN_SCHEMA - ) + if DOMAIN in config: + for entry_config in config[DOMAIN]: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=entry_config, + ) + ) return True -def scan_for_rokus(hass): - """Scan for devices and present a notification of the ones found.""" - - rokus = Roku.discover() - - devices = [] - for roku in rokus: - try: - r_info = roku.device_info - except RokuException: # skip non-roku device - continue - devices.append( - "Name: {0}
Host: {1}
".format( - r_info.userdevicename - if r_info.userdevicename - else f"{r_info.modelname} {r_info.serial_num}", - roku.host, - ) +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Roku from a config entry.""" + try: + roku_data = await hass.async_add_executor_job( + get_roku_data, entry.data[CONF_HOST], ) - if not devices: - devices = ["No device(s) found"] + except (SocketGIAError, RequestException, RokuException) as exception: + raise ConfigEntryNotReady from exception - hass.components.persistent_notification.create( - "The following devices were found:

" + "

".join(devices), - title=NOTIFICATION_SCAN_TITLE, - notification_id=NOTIFICATION_SCAN_ID, + hass.data[DOMAIN][entry.entry_id] = roku_data + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) -def _setup_roku(hass, hass_config, roku_config): - """Set up a Roku.""" - - host = roku_config[CONF_HOST] - - if host in hass.data[DATA_ROKU]: - return - - roku = Roku(host) - r_info = roku.device_info - - hass.data[DATA_ROKU][host] = {ATTR_ROKU: r_info.serial_num} - - discovery.load_platform(hass, "media_player", DOMAIN, roku_config, hass_config) - - discovery.load_platform(hass, "remote", DOMAIN, roku_config, hass_config) + return unload_ok diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py new file mode 100644 index 00000000000..32e66901e0f --- /dev/null +++ b/homeassistant/components/roku/config_flow.py @@ -0,0 +1,134 @@ +"""Config flow for Roku.""" +import logging +from socket import gaierror as SocketGIAError +from typing import Any, Dict, Optional +from urllib.parse import urlparse + +from requests.exceptions import RequestException +from roku import Roku, RokuException +import voluptuous as vol + +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, +) +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import callback +from homeassistant.exceptions import HomeAssistantError + +from .const import DOMAIN # pylint: disable=unused-import + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) + +ERROR_CANNOT_CONNECT = "cannot_connect" +ERROR_UNKNOWN = "unknown" + +_LOGGER = logging.getLogger(__name__) + + +def validate_input(data: Dict) -> Dict: + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + roku = Roku(data["host"]) + device_info = roku.device_info + + return { + "title": data["host"], + "host": data["host"], + "serial_num": device_info.serial_num, + } + + +class RokuConfigFlow(ConfigFlow, domain=DOMAIN): + """Handle a Roku config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + @callback + def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the form to the user.""" + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors or {}, + ) + + async def async_step_import( + self, user_input: Optional[Dict] = None + ) -> Dict[str, Any]: + """Handle configuration by yaml file.""" + return await self.async_step_user(user_input) + + async def async_step_user( + self, user_input: Optional[Dict] = None + ) -> Dict[str, Any]: + """Handle a flow initialized by the user.""" + if not user_input: + return self._show_form() + + errors = {} + + try: + info = await self.hass.async_add_executor_job(validate_input, user_input) + except (SocketGIAError, RequestException, RokuException): + errors["base"] = ERROR_CANNOT_CONNECT + return self._show_form(errors) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error trying to connect.") + return self.async_abort(reason=ERROR_UNKNOWN) + + await self.async_set_unique_id(info["serial_num"]) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=info["title"], data=user_input) + + async def async_step_ssdp( + self, discovery_info: Optional[Dict] = None + ) -> Dict[str, Any]: + """Handle a flow initialized by discovery.""" + host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname + name = discovery_info[ATTR_UPNP_FRIENDLY_NAME] + serial_num = discovery_info[ATTR_UPNP_SERIAL] + + await self.async_set_unique_id(serial_num) + self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update( + {CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": host}} + ) + + return await self.async_step_ssdp_confirm() + + async def async_step_ssdp_confirm( + self, user_input: Optional[Dict] = None + ) -> Dict[str, Any]: + """Handle user-confirmation of discovered device.""" + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + name = self.context.get(CONF_NAME) + + if user_input is not None: + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + user_input[CONF_HOST] = self.context.get(CONF_HOST) + user_input[CONF_NAME] = name + + try: + await self.hass.async_add_executor_job(validate_input, user_input) + return self.async_create_entry(title=name, data=user_input) + except (SocketGIAError, RequestException, RokuException): + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unknown error trying to connect.") + return self.async_abort(reason=ERROR_UNKNOWN) + + return self.async_show_form( + step_id="ssdp_confirm", description_placeholders={"name": name}, + ) + + +class CannotConnect(HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/roku/const.py b/homeassistant/components/roku/const.py index 54c52de2622..b06eed5df9f 100644 --- a/homeassistant/components/roku/const.py +++ b/homeassistant/components/roku/const.py @@ -1,2 +1,8 @@ """Constants for the Roku integration.""" +DOMAIN = "roku" + +DATA_CLIENT = "client" +DATA_DEVICE_INFO = "device_info" + DEFAULT_PORT = 8060 +DEFAULT_MANUFACTURER = "Roku" diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index 20461c789e2..e9cdb897115 100644 --- a/homeassistant/components/roku/manifest.json +++ b/homeassistant/components/roku/manifest.json @@ -4,6 +4,14 @@ "documentation": "https://www.home-assistant.io/integrations/roku", "requirements": ["roku==4.0.0"], "dependencies": [], - "after_dependencies": ["discovery"], - "codeowners": ["@ctalkington"] + "ssdp": [ + { + "st": "roku:ecp", + "manufacturer": "Roku", + "deviceType": "urn:roku-com:device:player:1-0" + } + ], + "codeowners": ["@ctalkington"], + "quality_scale": "silver", + "config_flow": true } diff --git a/homeassistant/components/roku/media_player.py b/homeassistant/components/roku/media_player.py index 21a2f562293..ba923f0fdd2 100644 --- a/homeassistant/components/roku/media_player.py +++ b/homeassistant/components/roku/media_player.py @@ -1,8 +1,9 @@ """Support for the Roku media player.""" -import logging - -import requests.exceptions -from roku import Roku +from requests.exceptions import ( + ConnectionError as RequestsConnectionError, + ReadTimeout as RequestsReadTimeout, +) +from roku import RokuException from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -16,17 +17,9 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import ( - CONF_HOST, - STATE_HOME, - STATE_IDLE, - STATE_PLAYING, - STATE_STANDBY, -) +from homeassistant.const import STATE_HOME, STATE_IDLE, STATE_PLAYING, STATE_STANDBY -from .const import DEFAULT_PORT - -_LOGGER = logging.getLogger(__name__) +from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DEFAULT_PORT, DOMAIN SUPPORT_ROKU = ( SUPPORT_PREVIOUS_TRACK @@ -40,23 +33,19 @@ SUPPORT_ROKU = ( ) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Roku platform.""" - if not discovery_info: - return - - host = discovery_info[CONF_HOST] - async_add_entities([RokuDevice(host)], True) +async def async_setup_entry(hass, entry, async_add_entities): + """Set up the Roku config entry.""" + roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + async_add_entities([RokuDevice(roku)], True) class RokuDevice(MediaPlayerDevice): """Representation of a Roku device on the network.""" - def __init__(self, host): + def __init__(self, roku): """Initialize the Roku device.""" - - self.roku = Roku(host) - self.ip_address = host + self.roku = roku + self.ip_address = roku.host self.channels = [] self.current_app = None self._available = False @@ -77,7 +66,7 @@ class RokuDevice(MediaPlayerDevice): self.current_app = None self._available = True - except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): + except (RequestsConnectionError, RequestsReadTimeout, RokuException): self._available = False pass @@ -130,6 +119,17 @@ class RokuDevice(MediaPlayerDevice): """Return a unique, Home Assistant friendly identifier for this entity.""" return self._device_info.serial_num + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": DEFAULT_MANUFACTURER, + "model": self._device_info.model_num, + "sw_version": self._device_info.software_version, + } + @property def media_content_type(self): """Content type of current playing media.""" diff --git a/homeassistant/components/roku/remote.py b/homeassistant/components/roku/remote.py index c953d9ba734..548282d6b2f 100644 --- a/homeassistant/components/roku/remote.py +++ b/homeassistant/components/roku/remote.py @@ -1,34 +1,48 @@ """Support for the Roku remote.""" -import requests.exceptions -from roku import Roku +from typing import Callable, List -from homeassistant.components import remote -from homeassistant.const import CONF_HOST +from requests.exceptions import ( + ConnectionError as RequestsConnectionError, + ReadTimeout as RequestsReadTimeout, +) +from roku import RokuException + +from homeassistant.components.remote import RemoteDevice +from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers.typing import HomeAssistantType + +from .const import DATA_CLIENT, DEFAULT_MANUFACTURER, DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up the Roku remote platform.""" - if not discovery_info: - return - - host = discovery_info[CONF_HOST] - async_add_entities([RokuRemote(host)], True) +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List, bool], None], +) -> bool: + """Load Roku remote based on a config entry.""" + roku = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] + async_add_entities([RokuRemote(roku)], True) -class RokuRemote(remote.RemoteDevice): +class RokuRemote(RemoteDevice): """Device that sends commands to an Roku.""" - def __init__(self, host): + def __init__(self, roku): """Initialize the Roku device.""" - - self.roku = Roku(host) + self.roku = roku + self._available = False self._device_info = {} def update(self): """Retrieve latest state.""" + if not self.enabled: + return + try: self._device_info = self.roku.device_info - except (requests.exceptions.ConnectionError, requests.exceptions.ReadTimeout): + self._available = True + except (RequestsConnectionError, RequestsReadTimeout, RokuException): + self._available = False pass @property @@ -38,11 +52,27 @@ class RokuRemote(remote.RemoteDevice): return self._device_info.user_device_name return f"Roku {self._device_info.serial_num}" + @property + def available(self): + """Return if able to retrieve information from device or not.""" + return self._available + @property def unique_id(self): """Return a unique ID.""" return self._device_info.serial_num + @property + def device_info(self): + """Return device specific attributes.""" + return { + "name": self.name, + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": DEFAULT_MANUFACTURER, + "model": self._device_info.model_num, + "sw_version": self._device_info.software_version, + } + @property def is_on(self): """Return true if device is on.""" diff --git a/homeassistant/components/roku/services.yaml b/homeassistant/components/roku/services.yaml deleted file mode 100644 index 956ecb0dd2d..00000000000 --- a/homeassistant/components/roku/services.yaml +++ /dev/null @@ -1,2 +0,0 @@ -roku_scan: - description: Scans the local network for Rokus. All found devices are presented as a persistent notification. diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json new file mode 100644 index 00000000000..0069728d14a --- /dev/null +++ b/homeassistant/components/roku/strings.json @@ -0,0 +1,27 @@ +{ + "config": { + "title": "Roku", + "flow_title": "Roku: {name}", + "step": { + "user": { + "title": "Roku", + "description": "Enter your Roku information.", + "data": { + "host": "Host or IP address" + } + }, + "ssdp_confirm": { + "title": "Roku", + "description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.", + "data": {} + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Roku device is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c19e9fafbc0..0ca18cec442 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -83,6 +83,7 @@ FLOWS = [ "rachio", "rainmachine", "ring", + "roku", "samsungtv", "sense", "sentry", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 3bf54b1d9f7..1df265bffe5 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -47,6 +47,13 @@ SSDP = { "manufacturer": "konnected.io" } ], + "roku": [ + { + "deviceType": "urn:roku-com:device:player:1-0", + "manufacturer": "Roku", + "st": "roku:ecp" + } + ], "samsungtv": [ { "st": "urn:samsung.com:device:RemoteControlReceiver:1" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index abc9d340dba..651471aba6c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -628,6 +628,9 @@ rflink==0.0.52 # homeassistant.components.ring ring_doorbell==0.6.0 +# homeassistant.components.roku +roku==4.0.0 + # homeassistant.components.yamaha rxv==0.6.0 diff --git a/tests/components/roku/__init__.py b/tests/components/roku/__init__.py new file mode 100644 index 00000000000..638a37b193a --- /dev/null +++ b/tests/components/roku/__init__.py @@ -0,0 +1,50 @@ +"""Tests for the Roku component.""" +from homeassistant.components.roku.const import DOMAIN +from homeassistant.const import CONF_HOST +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +HOST = "1.2.3.4" +NAME = "Roku 3" +SSDP_LOCATION = "http://1.2.3.4/" +UPNP_FRIENDLY_NAME = "My Roku 3" +UPNP_SERIAL = "1GU48T017973" + + +class MockDeviceInfo(object): + """Mock DeviceInfo for Roku.""" + + model_name = NAME + model_num = "4200X" + software_version = "7.5.0.09021" + serial_num = UPNP_SERIAL + user_device_name = UPNP_FRIENDLY_NAME + roku_type = "Box" + + def __repr__(self): + """Return the object representation of DeviceInfo.""" + return "" % ( + self.model_name, + self.model_num, + self.software_version, + self.serial_num, + self.roku_type, + ) + + +async def setup_integration( + hass: HomeAssistantType, skip_entry_setup: bool = False +) -> MockConfigEntry: + """Set up the Roku integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id=UPNP_SERIAL, data={CONF_HOST: HOST} + ) + + entry.add_to_hass(hass) + + if not skip_entry_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py new file mode 100644 index 00000000000..93d3fbb938d --- /dev/null +++ b/tests/components/roku/test_config_flow.py @@ -0,0 +1,247 @@ +"""Test the Roku config flow.""" +from socket import gaierror as SocketGIAError +from typing import Any, Dict, Optional + +from asynctest import patch +from requests.exceptions import RequestException +from roku import RokuException + +from homeassistant.components.roku.const import DOMAIN +from homeassistant.components.ssdp import ( + ATTR_SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL, +) +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component + +from tests.components.roku import ( + HOST, + SSDP_LOCATION, + UPNP_FRIENDLY_NAME, + UPNP_SERIAL, + MockDeviceInfo, + setup_integration, +) + + +async def async_configure_flow( + hass: HomeAssistantType, flow_id: str, user_input: Optional[Dict] = None +) -> Any: + """Set up mock Roku integration flow.""" + with patch( + "homeassistant.components.roku.config_flow.Roku.device_info", + new=MockDeviceInfo, + ): + return await hass.config_entries.flow.async_configure( + flow_id=flow_id, user_input=user_input + ) + + +async def async_init_flow( + hass: HomeAssistantType, + handler: str = DOMAIN, + context: Optional[Dict] = None, + data: Any = None, +) -> Any: + """Set up mock Roku integration flow.""" + with patch( + "homeassistant.components.roku.config_flow.Roku.device_info", + new=MockDeviceInfo, + ): + return await hass.config_entries.flow.async_init( + handler=handler, context=context, data=data + ) + + +async def test_duplicate_error(hass: HomeAssistantType) -> None: + """Test that errors are shown when duplicates are added.""" + await setup_integration(hass, skip_entry_setup=True) + + result = await async_init_flow( + hass, context={CONF_SOURCE: SOURCE_IMPORT}, data={CONF_HOST: HOST} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + result = await async_init_flow( + hass, context={CONF_SOURCE: SOURCE_USER}, data={CONF_HOST: HOST} + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + result = await async_init_flow( + hass, + context={CONF_SOURCE: SOURCE_SSDP}, + data={ + ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_SERIAL: UPNP_SERIAL, + }, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_form(hass: HomeAssistantType) -> None: + """Test the user step.""" + await async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.roku.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roku.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST}) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == {CONF_HOST: HOST} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass: HomeAssistantType) -> None: + """Test we handle cannot connect roku error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + + with patch( + "homeassistant.components.roku.config_flow.validate_input", + side_effect=RokuException, + ) as mock_validate_input: + result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + await hass.async_block_till_done() + assert len(mock_validate_input.mock_calls) == 1 + + +async def test_form_cannot_connect_request(hass: HomeAssistantType) -> None: + """Test we handle cannot connect request error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + + with patch( + "homeassistant.components.roku.config_flow.validate_input", + side_effect=RequestException, + ) as mock_validate_input: + result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + await hass.async_block_till_done() + assert len(mock_validate_input.mock_calls) == 1 + + +async def test_form_cannot_connect_socket(hass: HomeAssistantType) -> None: + """Test we handle cannot connect socket error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + + with patch( + "homeassistant.components.roku.config_flow.validate_input", + side_effect=SocketGIAError, + ) as mock_validate_input: + result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + await hass.async_block_till_done() + assert len(mock_validate_input.mock_calls) == 1 + + +async def test_form_unknown_error(hass: HomeAssistantType) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER} + ) + + with patch( + "homeassistant.components.roku.config_flow.validate_input", + side_effect=Exception, + ) as mock_validate_input: + result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + await hass.async_block_till_done() + assert len(mock_validate_input.mock_calls) == 1 + + +async def test_import(hass: HomeAssistantType) -> None: + """Test the import step.""" + with patch( + "homeassistant.components.roku.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roku.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await async_init_flow( + hass, context={CONF_SOURCE: SOURCE_IMPORT}, data={CONF_HOST: HOST} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + assert result["data"] == {CONF_HOST: HOST} + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_ssdp_discovery(hass: HomeAssistantType) -> None: + """Test the ssdp discovery step.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP}, + data={ + ATTR_SSDP_LOCATION: SSDP_LOCATION, + ATTR_UPNP_FRIENDLY_NAME: UPNP_FRIENDLY_NAME, + ATTR_UPNP_SERIAL: UPNP_SERIAL, + }, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "ssdp_confirm" + assert result["description_placeholders"] == {CONF_NAME: UPNP_FRIENDLY_NAME} + + with patch( + "homeassistant.components.roku.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.roku.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await async_configure_flow(hass, result["flow_id"], {}) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == UPNP_FRIENDLY_NAME + assert result["data"] == { + CONF_HOST: HOST, + CONF_NAME: UPNP_FRIENDLY_NAME, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py new file mode 100644 index 00000000000..c9eff43c858 --- /dev/null +++ b/tests/components/roku/test_init.py @@ -0,0 +1,68 @@ +"""Tests for the Roku integration.""" +from socket import gaierror as SocketGIAError + +from asynctest import patch +from requests.exceptions import RequestException +from roku import RokuException + +from homeassistant.components.roku.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.helpers.typing import HomeAssistantType + +from tests.components.roku import MockDeviceInfo, setup_integration + + +async def test_config_entry_not_ready(hass: HomeAssistantType) -> None: + """Test the Roku configuration entry not ready.""" + with patch( + "homeassistant.components.roku.Roku._call", side_effect=RokuException, + ): + entry = await setup_integration(hass) + + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_config_entry_not_ready_request(hass: HomeAssistantType) -> None: + """Test the Roku configuration entry not ready.""" + with patch( + "homeassistant.components.roku.Roku._call", side_effect=RequestException, + ): + entry = await setup_integration(hass) + + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_config_entry_not_ready_socket(hass: HomeAssistantType) -> None: + """Test the Roku configuration entry not ready.""" + with patch( + "homeassistant.components.roku.Roku._call", side_effect=SocketGIAError, + ): + entry = await setup_integration(hass) + + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry(hass: HomeAssistantType) -> None: + """Test the Roku configuration entry unloading.""" + with patch( + "homeassistant.components.roku.Roku.device_info", return_value=MockDeviceInfo, + ), patch( + "homeassistant.components.roku.media_player.async_setup_entry", + return_value=True, + ), patch( + "homeassistant.components.roku.remote.async_setup_entry", return_value=True, + ): + entry = await setup_integration(hass) + + assert hass.data[DOMAIN][entry.entry_id] + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.entry_id not in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_NOT_LOADED From 9a099bdf0a44c3e1ab09aba21f366083f8999777 Mon Sep 17 00:00:00 2001 From: David Bonnes Date: Mon, 16 Mar 2020 10:04:12 +0000 Subject: [PATCH 063/431] Ensure unique_ids for all evohome thermostats (#32604) * initial commit * small tweak --- homeassistant/components/evohome/climate.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/evohome/climate.py b/homeassistant/components/evohome/climate.py index 8b65d837171..b7899afdd7b 100644 --- a/homeassistant/components/evohome/climate.py +++ b/homeassistant/components/evohome/climate.py @@ -149,7 +149,12 @@ class EvoZone(EvoChild, EvoClimateDevice): """Initialize a Honeywell TCC Zone.""" super().__init__(evo_broker, evo_device) - self._unique_id = evo_device.zoneId + if evo_device.modelType.startswith("VisionProWifi"): + # this system does not have a distinct ID for the zone + self._unique_id = f"{evo_device.zoneId}z" + else: + self._unique_id = evo_device.zoneId + self._name = evo_device.name self._icon = "mdi:radiator" From 4e3b079a29c95beb2f5f9abf9db426d2c25f4fbc Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Mon, 16 Mar 2020 06:04:47 -0400 Subject: [PATCH 064/431] Add imperial units to met weather (#32824) * add feet to meter conversion * convert pressure and wind. fix wind for metric. * dont convert wind speed in metric. pymetno already converts it. * add units to setup_platform constuctor --- homeassistant/components/met/weather.py | 32 +++++++++++++++++++++---- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/met/weather.py b/homeassistant/components/met/weather.py index 13150098452..6523efa0eb7 100644 --- a/homeassistant/components/met/weather.py +++ b/homeassistant/components/met/weather.py @@ -12,13 +12,20 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_NAME, EVENT_CORE_CONFIG_UPDATE, + LENGTH_FEET, + LENGTH_METERS, + LENGTH_MILES, + PRESSURE_HPA, + PRESSURE_INHG, TEMP_CELSIUS, ) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.event import async_call_later +from homeassistant.util.distance import convert as convert_distance import homeassistant.util.dt as dt_util +from homeassistant.util.pressure import convert as convert_pressure from .const import CONF_TRACK_HOME @@ -56,20 +63,21 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= if config.get(CONF_LATITUDE) is None: config[CONF_TRACK_HOME] = True - async_add_entities([MetWeather(config)]) + async_add_entities([MetWeather(config, hass.config.units.is_metric)]) async def async_setup_entry(hass, config_entry, async_add_entities): """Add a weather entity from a config_entry.""" - async_add_entities([MetWeather(config_entry.data)]) + async_add_entities([MetWeather(config_entry.data, hass.config.units.is_metric)]) class MetWeather(WeatherEntity): """Implementation of a Met.no weather condition.""" - def __init__(self, config): + def __init__(self, config, is_metric): """Initialise the platform with a data instance and site.""" self._config = config + self._is_metric = is_metric self._unsub_track_home = None self._unsub_fetch_data = None self._weather_data = None @@ -99,6 +107,10 @@ class MetWeather(WeatherEntity): longitude = conf[CONF_LONGITUDE] elevation = conf[CONF_ELEVATION] + if not self._is_metric: + elevation = int( + round(convert_distance(elevation, LENGTH_FEET, LENGTH_METERS)) + ) coordinates = { "lat": str(latitude), "lon": str(longitude), @@ -201,7 +213,11 @@ class MetWeather(WeatherEntity): @property def pressure(self): """Return the pressure.""" - return self._current_weather_data.get("pressure") + pressure_hpa = self._current_weather_data.get("pressure") + if self._is_metric or pressure_hpa is None: + return pressure_hpa + + return round(convert_pressure(pressure_hpa, PRESSURE_HPA, PRESSURE_INHG), 2) @property def humidity(self): @@ -211,7 +227,13 @@ class MetWeather(WeatherEntity): @property def wind_speed(self): """Return the wind speed.""" - return self._current_weather_data.get("wind_speed") + speed_m_s = self._current_weather_data.get("wind_speed") + 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 + return int(round(speed_mi_h)) @property def wind_bearing(self): From af021b1c8182b44112405f758cac1343ded6944a Mon Sep 17 00:00:00 2001 From: Kris Bennett <1435262+i00@users.noreply.github.com> Date: Mon, 16 Mar 2020 20:09:48 +1000 Subject: [PATCH 065/431] Add additional information to SynologySRM device tracker (#32669) * Updating SynologySRM device trakcers to show additional information * Fixes * Aliasing attributes to snake case * Sugguested changes as per MartinHjelmare --- .../components/synology_srm/device_tracker.py | 61 +++++++++++++++---- 1 file changed, 49 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/synology_srm/device_tracker.py b/homeassistant/components/synology_srm/device_tracker.py index 577a01c5148..af7a3ea5f63 100644 --- a/homeassistant/components/synology_srm/device_tracker.py +++ b/homeassistant/components/synology_srm/device_tracker.py @@ -37,6 +37,34 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( } ) +ATTRIBUTE_ALIAS = { + "band": None, + "connection": None, + "current_rate": None, + "dev_type": None, + "hostname": None, + "ip6_addr": None, + "ip_addr": None, + "is_baned": "is_banned", + "is_beamforming_on": None, + "is_guest": None, + "is_high_qos": None, + "is_low_qos": None, + "is_manual_dev_type": None, + "is_manual_hostname": None, + "is_online": None, + "is_parental_controled": "is_parental_controlled", + "is_qos": None, + "is_wireless": None, + "mac": None, + "max_rate": None, + "mesh_node_id": None, + "rate_quality": None, + "signalstrength": "signal_strength", + "transferRXRate": "transfer_rx_rate", + "transferTXRate": "transfer_tx_rate", +} + def get_scanner(hass, config): """Validate the configuration and return Synology SRM scanner.""" @@ -62,7 +90,7 @@ class SynologySrmDeviceScanner(DeviceScanner): if not config[CONF_VERIFY_SSL]: self.client.http.disable_https_verify() - self.last_results = [] + self.devices = [] self.success_init = self._update_info() _LOGGER.info("Synology SRM scanner initialized") @@ -71,14 +99,28 @@ class SynologySrmDeviceScanner(DeviceScanner): """Scan for new devices and return a list with found device IDs.""" self._update_info() - return [device["mac"] for device in self.last_results] + return [device["mac"] for device in self.devices] + + def get_extra_attributes(self, device) -> dict: + """Get the extra attributes of a device.""" + device = next( + (result for result in self.devices if result["mac"] == device), None + ) + filtered_attributes = {} + if not device: + return filtered_attributes + for attribute, alias in ATTRIBUTE_ALIAS.items(): + value = device.get(attribute) + if value is None: + continue + attr = alias or attribute + filtered_attributes[attr] = value + return filtered_attributes def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" filter_named = [ - result["hostname"] - for result in self.last_results - if result["mac"] == device + result["hostname"] for result in self.devices if result["mac"] == device ] if filter_named: @@ -90,13 +132,8 @@ class SynologySrmDeviceScanner(DeviceScanner): """Check the router for connected devices.""" _LOGGER.debug("Scanning for connected devices") - devices = self.client.core.network_nsm_device({"is_online": True}) - last_results = [] + self.devices = self.client.core.network_nsm_device({"is_online": True}) - for device in devices: - last_results.append({"mac": device["mac"], "hostname": device["hostname"]}) + _LOGGER.debug("Found %d device(s) connected to the router", len(self.devices)) - _LOGGER.debug("Found %d device(s) connected to the router", len(devices)) - - self.last_results = last_results return True From 451c6c25cd44e144ac1afb25c34d92619bbb51e3 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 16 Mar 2020 11:40:21 +0100 Subject: [PATCH 066/431] Update pyozw 0.1.9 (#32864) --- homeassistant/components/zwave/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/manifest.json b/homeassistant/components/zwave/manifest.json index 1fc6401f25b..81978aa96cd 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.8", "pydispatcher==2.0.5"], + "requirements": ["homeassistant-pyozw==0.1.9", "pydispatcher==2.0.5"], "dependencies": [], "codeowners": ["@home-assistant/z-wave"] } diff --git a/requirements_all.txt b/requirements_all.txt index 71500fbbb64..c93033594db 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -699,7 +699,7 @@ holidays==0.10.1 home-assistant-frontend==20200313.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.8 +homeassistant-pyozw==0.1.9 # homeassistant.components.homematicip_cloud homematicip==0.10.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 651471aba6c..6713beeb656 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -269,7 +269,7 @@ holidays==0.10.1 home-assistant-frontend==20200313.0 # homeassistant.components.zwave -homeassistant-pyozw==0.1.8 +homeassistant-pyozw==0.1.9 # homeassistant.components.homematicip_cloud homematicip==0.10.17 From 77b3f31e9bf4406b3fb68e0bd6725948aafc8feb Mon Sep 17 00:00:00 2001 From: Jason Lachowsky Date: Mon, 16 Mar 2020 05:58:12 -0500 Subject: [PATCH 067/431] Corrected minor misspellings (#32857) --- .../components/homekit_controller/.translations/en.json | 2 +- homeassistant/components/homekit_controller/strings.json | 2 +- homeassistant/components/system_log/__init__.py | 4 ++-- homeassistant/components/toon/.translations/en.json | 2 +- homeassistant/components/toon/strings.json | 2 +- tests/components/system_log/test_init.py | 2 +- 6 files changed, 7 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/homekit_controller/.translations/en.json b/homeassistant/components/homekit_controller/.translations/en.json index 72aa720b449..eb994289a62 100644 --- a/homeassistant/components/homekit_controller/.translations/en.json +++ b/homeassistant/components/homekit_controller/.translations/en.json @@ -14,7 +14,7 @@ "busy_error": "Device refused to add pairing as it is already pairing with another controller.", "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", - "pairing_failed": "An unhandled error occured while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", + "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently.", "unable_to_pair": "Unable to pair, please try again.", "unknown_error": "Device reported an unknown error. Pairing failed." }, diff --git a/homeassistant/components/homekit_controller/strings.json b/homeassistant/components/homekit_controller/strings.json index 55718e35b59..80370717183 100644 --- a/homeassistant/components/homekit_controller/strings.json +++ b/homeassistant/components/homekit_controller/strings.json @@ -25,7 +25,7 @@ "max_peers_error": "Device refused to add pairing as it has no free pairing storage.", "busy_error": "Device refused to add pairing as it is already pairing with another controller.", "max_tries_error": "Device refused to add pairing as it has received more than 100 unsuccessful authentication attempts.", - "pairing_failed": "An unhandled error occured while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently." + "pairing_failed": "An unhandled error occurred while attempting to pair with this device. This may be a temporary failure or your device may not be supported currently." }, "abort": { "no_devices": "No unpaired devices could be found", diff --git a/homeassistant/components/system_log/__init__.py b/homeassistant/components/system_log/__init__.py index 2ddf02f76ed..bf49de5a731 100644 --- a/homeassistant/components/system_log/__init__.py +++ b/homeassistant/components/system_log/__init__.py @@ -91,7 +91,7 @@ class LogEntry: def __init__(self, record, stack, source): """Initialize a log entry.""" - self.first_occured = self.timestamp = record.created + self.first_occurred = self.timestamp = record.created self.name = record.name self.level = record.levelname self.message = deque([record.getMessage()], maxlen=5) @@ -117,7 +117,7 @@ class LogEntry: "timestamp": self.timestamp, "exception": self.exception, "count": self.count, - "first_occured": self.first_occured, + "first_occurred": self.first_occurred, } diff --git a/homeassistant/components/toon/.translations/en.json b/homeassistant/components/toon/.translations/en.json index cea3146a3a5..7d7d6c73e16 100644 --- a/homeassistant/components/toon/.translations/en.json +++ b/homeassistant/components/toon/.translations/en.json @@ -5,7 +5,7 @@ "client_secret": "The client secret from the configuration is invalid.", "no_agreements": "This account has no Toon displays.", "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Unexpected error occured, while authenticating." + "unknown_auth_fail": "Unexpected error occurred, while authenticating." }, "error": { "credentials": "The provided credentials are invalid.", diff --git a/homeassistant/components/toon/strings.json b/homeassistant/components/toon/strings.json index 80d71d4e421..20d6ba3d72c 100644 --- a/homeassistant/components/toon/strings.json +++ b/homeassistant/components/toon/strings.json @@ -26,7 +26,7 @@ "abort": { "client_id": "The client ID from the configuration is invalid.", "client_secret": "The client secret from the configuration is invalid.", - "unknown_auth_fail": "Unexpected error occured, while authenticating.", + "unknown_auth_fail": "Unexpected error occurred, while authenticating.", "no_agreements": "This account has no Toon displays.", "no_app": "You need to configure Toon before being able to authenticate with it. [Please read the instructions](https://www.home-assistant.io/components/toon/)." } diff --git a/tests/components/system_log/test_init.py b/tests/components/system_log/test_init.py index 9862260c5f8..92f0ed9fd16 100644 --- a/tests/components/system_log/test_init.py +++ b/tests/components/system_log/test_init.py @@ -157,7 +157,7 @@ async def test_dedup_logs(hass, hass_client): log_msg() log = await get_error_log(hass, hass_client, 3) assert_log(log[0], "", ["error message 2", "error message 2-2"], "ERROR") - assert log[0]["timestamp"] > log[0]["first_occured"] + assert log[0]["timestamp"] > log[0]["first_occurred"] log_msg("2-3") log_msg("2-4") From b9ad40ed3821ab557547222b3c8d02cedfe246f3 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 16 Mar 2020 12:00:39 +0100 Subject: [PATCH 068/431] Bump brother to 0.1.9 (#32861) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index ec87adacb5f..4528e3e6d1f 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.8"], + "requirements": ["brother==0.1.9"], "zeroconf": ["_printer._tcp.local."], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index c93033594db..dbc62906ac5 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -350,7 +350,7 @@ bravia-tv==1.0.1 broadlink==0.12.0 # homeassistant.components.brother -brother==0.1.8 +brother==0.1.9 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 6713beeb656..7443243c842 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -130,7 +130,7 @@ bomradarloop==0.1.4 broadlink==0.12.0 # homeassistant.components.brother -brother==0.1.8 +brother==0.1.9 # homeassistant.components.buienradar buienradar==1.0.4 From fa63dc1e25f73f47341d7a748f1e497714a70f49 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Mon, 16 Mar 2020 12:10:45 +0100 Subject: [PATCH 069/431] UniFi - Improve expected SSID filter behavior (#32785) * Improve expected ssid filter behavior * Fix tests --- .../components/unifi/device_tracker.py | 30 ++++++++++++++++--- tests/components/unifi/test_device_tracker.py | 12 ++++---- 2 files changed, 32 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index e5d3bcfa82b..07e96a45fce 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -45,6 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): option_track_clients = controller.option_track_clients option_track_devices = controller.option_track_devices option_track_wired_clients = controller.option_track_wired_clients + option_ssid_filter = controller.option_ssid_filter registry = await hass.helpers.entity_registry.async_get_registry() @@ -86,6 +87,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): nonlocal option_track_clients nonlocal option_track_devices nonlocal option_track_wired_clients + nonlocal option_ssid_filter update = False remove = set() @@ -116,6 +118,18 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if isinstance(entity, UniFiClientTracker) and entity.is_wired: remove.add(mac) + if option_ssid_filter != controller.option_ssid_filter: + option_ssid_filter = controller.option_ssid_filter + update = True + + for mac, entity in tracked.items(): + if ( + isinstance(entity, UniFiClientTracker) + and not entity.is_wired + and entity.client.essid not in option_ssid_filter + ): + remove.add(mac) + option_track_clients = controller.option_track_clients option_track_devices = controller.option_track_devices option_track_wired_clients = controller.option_track_wired_clients @@ -157,10 +171,18 @@ def add_entities(controller, async_add_entities, tracked): if item_id in tracked: continue - if tracker_class is UniFiClientTracker and ( - not controller.option_track_wired_clients and items[item_id].is_wired - ): - continue + if tracker_class is UniFiClientTracker: + client = items[item_id] + + if not controller.option_track_wired_clients and client.is_wired: + continue + + if ( + controller.option_ssid_filter + and not client.is_wired + and client.essid not in controller.option_ssid_filter + ): + continue tracked[item_id] = tracker_class(items[item_id], controller) new_tracked.append(tracked[item_id]) diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 1d314c1fe86..bb15ff65fb9 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -124,7 +124,7 @@ async def test_tracked_devices(hass): devices_response=[DEVICE_1, DEVICE_2], known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_all()) == 7 + assert len(hass.states.async_all()) == 6 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -134,9 +134,9 @@ async def test_tracked_devices(hass): assert client_2 is not None assert client_2.state == "not_home" + # Client on SSID not in SSID filter client_3 = hass.states.get("device_tracker.client_3") - assert client_3 is not None - assert client_3.state == "not_home" + assert not client_3 # Wireless client with wired bug, if bug active on restart mark device away client_4 = hass.states.get("device_tracker.client_4") @@ -350,11 +350,11 @@ async def test_option_ssid_filter(hass): controller = await setup_unifi_integration( hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 # SSID filter active client_3 = hass.states.get("device_tracker.client_3") - assert client_3.state == "not_home" + assert not client_3 client_3_copy = copy(CLIENT_3) client_3_copy["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) @@ -364,7 +364,7 @@ async def test_option_ssid_filter(hass): # SSID filter active even though time stamp should mark as home client_3 = hass.states.get("device_tracker.client_3") - assert client_3.state == "not_home" + assert not client_3 # Remove SSID filter hass.config_entries.async_update_entry( From f4b3760a1a6e233e366e1700f081587ab21a28ba Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 16 Mar 2020 13:30:59 +0100 Subject: [PATCH 070/431] Updated frontend to 20200316.0 (#32866) --- 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 2817b744d72..211e5bc7e84 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==20200313.0" + "home-assistant-frontend==20200316.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 471af972755..72182b1f6be 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200313.0 +home-assistant-frontend==20200316.0 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index dbc62906ac5..230b78d08e2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -696,7 +696,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200313.0 +home-assistant-frontend==20200316.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7443243c842..7046f54f42a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200313.0 +home-assistant-frontend==20200316.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 From 40356b4fc57e8573ada15603ff15a059ca7c352a Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Mon, 16 Mar 2020 09:11:08 -0400 Subject: [PATCH 071/431] Enable AlarmDecoder arming without security code (#32390) * set alarmdecoder code_arm_required to False * rm unnecessary f strings * add code_arm_required config option * add self as codeowner :-) * add self as codeowner the right way --- CODEOWNERS | 1 + .../components/alarmdecoder/__init__.py | 12 ++++++++- .../alarmdecoder/alarm_control_panel.py | 26 ++++++++++++++++--- .../components/alarmdecoder/manifest.json | 14 +++++----- 4 files changed, 41 insertions(+), 12 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 1bcffad1d17..9730c96a573 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -17,6 +17,7 @@ homeassistant/components/abode/* @shred86 homeassistant/components/adguard/* @frenck homeassistant/components/airly/* @bieniu homeassistant/components/airvisual/* @bachya +homeassistant/components/alarmdecoder/* @ajschmidt8 homeassistant/components/alexa/* @home-assistant/cloud @ochlocracy homeassistant/components/almond/* @gcampax @balloob homeassistant/components/alpha_vantage/* @fabaff diff --git a/homeassistant/components/alarmdecoder/__init__.py b/homeassistant/components/alarmdecoder/__init__.py index a990de9bf98..5e143fcca81 100644 --- a/homeassistant/components/alarmdecoder/__init__.py +++ b/homeassistant/components/alarmdecoder/__init__.py @@ -33,6 +33,7 @@ CONF_ZONE_RFID = "rfid" CONF_ZONES = "zones" CONF_RELAY_ADDR = "relayaddr" CONF_RELAY_CHAN = "relaychan" +CONF_CODE_ARM_REQUIRED = "code_arm_required" DEFAULT_DEVICE_TYPE = "socket" DEFAULT_DEVICE_HOST = "localhost" @@ -42,6 +43,7 @@ DEFAULT_DEVICE_BAUD = 115200 DEFAULT_AUTO_BYPASS = False DEFAULT_PANEL_DISPLAY = False +DEFAULT_CODE_ARM_REQUIRED = True DEFAULT_ZONE_TYPE = "opening" @@ -105,6 +107,9 @@ CONFIG_SCHEMA = vol.Schema( CONF_PANEL_DISPLAY, default=DEFAULT_PANEL_DISPLAY ): cv.boolean, vol.Optional(CONF_AUTO_BYPASS, default=DEFAULT_AUTO_BYPASS): cv.boolean, + vol.Optional( + CONF_CODE_ARM_REQUIRED, default=DEFAULT_CODE_ARM_REQUIRED + ): cv.boolean, vol.Optional(CONF_ZONES): {vol.Coerce(int): ZONE_SCHEMA}, } ) @@ -121,6 +126,7 @@ def setup(hass, config): device = conf[CONF_DEVICE] display = conf[CONF_PANEL_DISPLAY] auto_bypass = conf[CONF_AUTO_BYPASS] + code_arm_required = conf[CONF_CODE_ARM_REQUIRED] zones = conf.get(CONF_ZONES) device_type = device[CONF_DEVICE_TYPE] @@ -206,7 +212,11 @@ def setup(hass, config): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_alarmdecoder) load_platform( - hass, "alarm_control_panel", DOMAIN, {CONF_AUTO_BYPASS: auto_bypass}, config + hass, + "alarm_control_panel", + DOMAIN, + {CONF_AUTO_BYPASS: auto_bypass, CONF_CODE_ARM_REQUIRED: code_arm_required}, + config, ) if zones: diff --git a/homeassistant/components/alarmdecoder/alarm_control_panel.py b/homeassistant/components/alarmdecoder/alarm_control_panel.py index 06783df674d..57004191064 100644 --- a/homeassistant/components/alarmdecoder/alarm_control_panel.py +++ b/homeassistant/components/alarmdecoder/alarm_control_panel.py @@ -21,7 +21,13 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import CONF_AUTO_BYPASS, DATA_AD, DOMAIN, SIGNAL_PANEL_MESSAGE +from . import ( + CONF_AUTO_BYPASS, + CONF_CODE_ARM_REQUIRED, + DATA_AD, + DOMAIN, + SIGNAL_PANEL_MESSAGE, +) _LOGGER = logging.getLogger(__name__) @@ -39,7 +45,8 @@ def setup_platform(hass, config, add_entities, discovery_info=None): return auto_bypass = discovery_info[CONF_AUTO_BYPASS] - entity = AlarmDecoderAlarmPanel(auto_bypass) + code_arm_required = discovery_info[CONF_CODE_ARM_REQUIRED] + entity = AlarmDecoderAlarmPanel(auto_bypass, code_arm_required) add_entities([entity]) def alarm_toggle_chime_handler(service): @@ -70,7 +77,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): class AlarmDecoderAlarmPanel(AlarmControlPanel): """Representation of an AlarmDecoder-based alarm panel.""" - def __init__(self, auto_bypass): + def __init__(self, auto_bypass, code_arm_required): """Initialize the alarm panel.""" self._display = "" self._name = "Alarm Panel" @@ -85,6 +92,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): self._ready = None self._zone_bypassed = None self._auto_bypass = auto_bypass + self._code_arm_required = code_arm_required async def async_added_to_hass(self): """Register callbacks.""" @@ -140,6 +148,11 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): """Return the list of supported features.""" return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY | SUPPORT_ALARM_ARM_NIGHT + @property + def code_arm_required(self): + """Whether the code is required for arm actions.""" + return self._code_arm_required + @property def device_state_attributes(self): """Return the state attributes.""" @@ -153,6 +166,7 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): "programming_mode": self._programming_mode, "ready": self._ready, "zone_bypassed": self._zone_bypassed, + "code_arm_required": self._code_arm_required, } def alarm_disarm(self, code=None): @@ -166,6 +180,8 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): if self._auto_bypass: self.hass.data[DATA_AD].send(f"{code!s}6#") self.hass.data[DATA_AD].send(f"{code!s}2") + elif not self._code_arm_required: + self.hass.data[DATA_AD].send("#2") def alarm_arm_home(self, code=None): """Send arm home command.""" @@ -173,11 +189,15 @@ class AlarmDecoderAlarmPanel(AlarmControlPanel): if self._auto_bypass: self.hass.data[DATA_AD].send(f"{code!s}6#") self.hass.data[DATA_AD].send(f"{code!s}3") + elif not self._code_arm_required: + self.hass.data[DATA_AD].send("#3") def alarm_arm_night(self, code=None): """Send arm night command.""" if code: self.hass.data[DATA_AD].send(f"{code!s}7") + elif not self._code_arm_required: + self.hass.data[DATA_AD].send("#7") def alarm_toggle_chime(self, code=None): """Send toggle chime command.""" diff --git a/homeassistant/components/alarmdecoder/manifest.json b/homeassistant/components/alarmdecoder/manifest.json index f146f6f4a7e..9824b20db2a 100644 --- a/homeassistant/components/alarmdecoder/manifest.json +++ b/homeassistant/components/alarmdecoder/manifest.json @@ -1,10 +1,8 @@ { - "domain": "alarmdecoder", - "name": "AlarmDecoder", - "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", - "requirements": [ - "alarmdecoder==1.13.2" - ], - "dependencies": [], - "codeowners": [] + "domain": "alarmdecoder", + "name": "AlarmDecoder", + "documentation": "https://www.home-assistant.io/integrations/alarmdecoder", + "requirements": ["alarmdecoder==1.13.2"], + "dependencies": [], + "codeowners": ["@ajschmidt8"] } From c1908d16b5d30d538e5a4433f5aa3913ab95ea41 Mon Sep 17 00:00:00 2001 From: sbilly Date: Mon, 16 Mar 2020 21:21:27 +0800 Subject: [PATCH 072/431] Add 'Yeelight LED Ceiling Light' model (#31615) * Add 'Yeelight LED Ceiling Light' model Add new model https://www.yeelight.com/en_US/product/luna * Update requirements_all.txt bump to yeelight 0.5.1 Update requirements_all.txt bump to yeelight 0.5.1 * Update manifest.json, bump to 0.5.1 Update manifest.json, bump to 0.5.1 --- homeassistant/components/yeelight/light.py | 1 + homeassistant/components/yeelight/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 2605823a99d..59863464d21 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -141,6 +141,7 @@ MODEL_TO_DEVICE_TYPE = { "ceiling2": BulbType.WhiteTemp, "ceiling3": BulbType.WhiteTemp, "ceiling4": BulbType.WhiteTempMood, + "ceiling13": BulbType.WhiteTemp, } EFFECTS_MAP = { diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 1a181536d0b..c5396030813 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.5.0"], + "requirements": ["yeelight==0.5.1"], "dependencies": [], "after_dependencies": ["discovery"], "codeowners": ["@rytilahti", "@zewelor"] diff --git a/requirements_all.txt b/requirements_all.txt index 230b78d08e2..39b9857ca00 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2143,7 +2143,7 @@ yahooweather==0.10 yalesmartalarmclient==0.1.6 # homeassistant.components.yeelight -yeelight==0.5.0 +yeelight==0.5.1 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 From 8d68f34650eb68470113127b9e1a67d2ae753a5b Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Mon, 16 Mar 2020 07:03:42 -0700 Subject: [PATCH 073/431] Add window class for Abode binary sensors (#32854) * Add window class for binary sensor --- homeassistant/components/abode/binary_sensor.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/abode/binary_sensor.py b/homeassistant/components/abode/binary_sensor.py index c4cdadf9bd9..916ed2e2613 100644 --- a/homeassistant/components/abode/binary_sensor.py +++ b/homeassistant/components/abode/binary_sensor.py @@ -1,7 +1,10 @@ """Support for Abode Security System binary sensors.""" import abodepy.helpers.constants as CONST -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_WINDOW, + BinarySensorDevice, +) from . import AbodeDevice from .const import DOMAIN @@ -38,4 +41,6 @@ class AbodeBinarySensor(AbodeDevice, BinarySensorDevice): @property def device_class(self): """Return the class of the binary sensor.""" + if self._device.get_value("is_window") == "1": + return DEVICE_CLASS_WINDOW return self._device.generic_type From 7ec7306ea8a6335eed32e02a2ef2b5473b00febc Mon Sep 17 00:00:00 2001 From: Knapoc Date: Mon, 16 Mar 2020 15:44:59 +0100 Subject: [PATCH 074/431] Update deconz/cover.py to adapt rest-api changes (#32351) * Update cover.py to adapt rest-api changes * adjust test to reflect max brightness changes * implement test for rest-api cover position of 255 --- homeassistant/components/deconz/cover.py | 4 +-- tests/components/deconz/test_cover.py | 34 +++++++++++++++++++++--- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/deconz/cover.py b/homeassistant/components/deconz/cover.py index 6e5e616fbb8..7db3477c3bb 100644 --- a/homeassistant/components/deconz/cover.py +++ b/homeassistant/components/deconz/cover.py @@ -61,7 +61,7 @@ class DeconzCover(DeconzDevice, CoverDevice): @property def current_cover_position(self): """Return the current position of the cover.""" - return 100 - int(self._device.brightness / 255 * 100) + return 100 - int(self._device.brightness / 254 * 100) @property def is_closed(self): @@ -88,7 +88,7 @@ class DeconzCover(DeconzDevice, CoverDevice): if position < 100: data["on"] = True - data["bri"] = 255 - int(position / 100 * 255) + data["bri"] = 254 - int(position / 100 * 254) await self._device.async_set_state(data) diff --git a/tests/components/deconz/test_cover.py b/tests/components/deconz/test_cover.py index 4bf0ec86f4a..43bb165b1a6 100644 --- a/tests/components/deconz/test_cover.py +++ b/tests/components/deconz/test_cover.py @@ -14,7 +14,7 @@ COVERS = { "id": "Level controllable cover id", "name": "Level controllable cover", "type": "Level controllable output", - "state": {"bri": 255, "on": False, "reachable": True}, + "state": {"bri": 254, "on": False, "reachable": True}, "modelid": "Not zigbee spec", "uniqueid": "00:00:00:00:00:00:00:00-00", }, @@ -22,7 +22,7 @@ COVERS = { "id": "Window covering device id", "name": "Window covering device", "type": "Window covering device", - "state": {"bri": 255, "on": True, "reachable": True}, + "state": {"bri": 254, "on": True, "reachable": True}, "modelid": "lumi.curtain", "uniqueid": "00:00:00:00:00:00:00:01-00", }, @@ -33,6 +33,14 @@ COVERS = { "state": {"reachable": True}, "uniqueid": "00:00:00:00:00:00:00:02-00", }, + "4": { + "id": "deconz old brightness cover id", + "name": "deconz old brightness cover", + "type": "Level controllable output", + "state": {"bri": 255, "on": False, "reachable": True}, + "modelid": "Not zigbee spec", + "uniqueid": "00:00:00:00:00:00:00:03-00", + }, } @@ -62,7 +70,8 @@ async def test_cover(hass): assert "cover.level_controllable_cover" in gateway.deconz_ids assert "cover.window_covering_device" in gateway.deconz_ids assert "cover.unsupported_cover" not in gateway.deconz_ids - assert len(hass.states.async_all()) == 3 + assert "cover.deconz_old_brightness_cover" in gateway.deconz_ids + assert len(hass.states.async_all()) == 4 level_controllable_cover = hass.states.get("cover.level_controllable_cover") assert level_controllable_cover.state == "open" @@ -105,7 +114,7 @@ async def test_cover(hass): ) await hass.async_block_till_done() set_callback.assert_called_with( - "put", "/lights/1/state", json={"on": True, "bri": 255} + "put", "/lights/1/state", json={"on": True, "bri": 254} ) with patch.object( @@ -120,6 +129,23 @@ async def test_cover(hass): await hass.async_block_till_done() set_callback.assert_called_with("put", "/lights/1/state", json={"bri_inc": 0}) + """Test that a reported cover position of 255 (deconz-rest-api < 2.05.73) is interpreted correctly.""" + deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover") + assert deconz_old_brightness_cover.state == "open" + + state_changed_event = { + "t": "event", + "e": "changed", + "r": "lights", + "id": "4", + "state": {"on": True}, + } + gateway.api.event_handler(state_changed_event) + await hass.async_block_till_done() + + deconz_old_brightness_cover = hass.states.get("cover.deconz_old_brightness_cover") + assert deconz_old_brightness_cover.attributes["current_position"] == 0 + await gateway.async_reset() assert len(hass.states.async_all()) == 0 From 999c5443c1979689a38f43cc5cfdfd8a0b584d0c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Mar 2020 10:03:44 -0700 Subject: [PATCH 075/431] Fix abode test (#32871) --- tests/components/abode/test_binary_sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/components/abode/test_binary_sensor.py b/tests/components/abode/test_binary_sensor.py index aced7f33e73..a826191ccf3 100644 --- a/tests/components/abode/test_binary_sensor.py +++ b/tests/components/abode/test_binary_sensor.py @@ -2,7 +2,7 @@ from homeassistant.components.abode import ATTR_DEVICE_ID from homeassistant.components.abode.const import ATTRIBUTION from homeassistant.components.binary_sensor import ( - DEVICE_CLASS_DOOR, + DEVICE_CLASS_WINDOW, DOMAIN as BINARY_SENSOR_DOMAIN, ) from homeassistant.const import ( @@ -36,4 +36,4 @@ async def test_attributes(hass): assert not state.attributes.get("no_response") assert state.attributes.get("device_type") == "Door Contact" assert state.attributes.get(ATTR_FRIENDLY_NAME) == "Front Door" - assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_DOOR + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_WINDOW From 2f1824774f63302a3b902a34847d3831f22bc698 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 16 Mar 2020 20:08:00 +0100 Subject: [PATCH 076/431] Lovelace: storage key based on id instead of url_path (#32873) * Fix storage key based on url_path * Fix test --- homeassistant/components/lovelace/dashboard.py | 2 +- tests/components/lovelace/test_dashboard.py | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index 38740672914..cdb104a150b 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -88,7 +88,7 @@ class LovelaceStorage(LovelaceConfig): storage_key = CONFIG_STORAGE_KEY_DEFAULT else: url_path = config[CONF_URL_PATH] - storage_key = CONFIG_STORAGE_KEY.format(url_path) + storage_key = CONFIG_STORAGE_KEY.format(config["id"]) super().__init__(hass, url_path, config) diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 1effb10be27..775b2760c96 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -373,7 +373,6 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): assert response["result"]["icon"] == "mdi:map" dashboard_id = response["result"]["id"] - dashboard_path = response["result"]["url_path"] assert "created-url-path" in hass.data[frontend.DATA_PANELS] @@ -408,9 +407,9 @@ async def test_storage_dashboards(hass, hass_ws_client, hass_storage): ) response = await client.receive_json() assert response["success"] - assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format(dashboard_path)][ - "data" - ] == {"config": {"yo": "hello"}} + assert hass_storage[dashboard.CONFIG_STORAGE_KEY.format(dashboard_id)]["data"] == { + "config": {"yo": "hello"} + } assert len(events) == 1 assert events[0].data["url_path"] == "created-url-path" From 426f546c2f21afa359a6df34becda1cf52398a19 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 16 Mar 2020 20:25:23 +0100 Subject: [PATCH 077/431] Add lovelace reload service for yaml resources (#32865) * Lovelace add reload service for yaml resources * Clean up imports * Comments --- homeassistant/components/lovelace/__init__.py | 64 +++++++++++++++---- homeassistant/components/lovelace/const.py | 3 + .../components/lovelace/services.yaml | 4 ++ 3 files changed, 57 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/lovelace/services.yaml diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 220161fb649..95508c2f8f3 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -4,10 +4,14 @@ import logging import voluptuous as vol from homeassistant.components import frontend +from homeassistant.config import async_hass_config_yaml, async_process_component_config from homeassistant.const import CONF_FILENAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import collection, config_validation as cv +from homeassistant.helpers.service import async_register_admin_service +from homeassistant.helpers.typing import ConfigType, HomeAssistantType, ServiceCallType +from homeassistant.loader import async_get_integration from homeassistant.util import sanitize_filename from . import dashboard, resources, websocket @@ -25,8 +29,10 @@ from .const import ( MODE_STORAGE, MODE_YAML, RESOURCE_CREATE_FIELDS, + RESOURCE_RELOAD_SERVICE_SCHEMA, RESOURCE_SCHEMA, RESOURCE_UPDATE_FIELDS, + SERVICE_RELOAD_RESOURCES, STORAGE_DASHBOARD_CREATE_FIELDS, STORAGE_DASHBOARD_UPDATE_FIELDS, url_slug, @@ -62,29 +68,41 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up the Lovelace commands.""" mode = config[DOMAIN][CONF_MODE] yaml_resources = config[DOMAIN].get(CONF_RESOURCES) frontend.async_register_built_in_panel(hass, DOMAIN, config={"mode": mode}) + async def reload_resources_service_handler(service_call: ServiceCallType) -> None: + """Reload yaml resources.""" + try: + conf = await async_hass_config_yaml(hass) + except HomeAssistantError as err: + _LOGGER.error(err) + return + + integration = await async_get_integration(hass, DOMAIN) + + config = await async_process_component_config(hass, conf, integration) + + resource_collection = await create_yaml_resource_col( + hass, config[DOMAIN].get(CONF_RESOURCES) + ) + hass.data[DOMAIN]["resources"] = resource_collection + if mode == MODE_YAML: default_config = dashboard.LovelaceYAML(hass, None, None) + resource_collection = await create_yaml_resource_col(hass, yaml_resources) - if yaml_resources is None: - try: - ll_conf = await default_config.async_load(False) - except HomeAssistantError: - pass - else: - if CONF_RESOURCES in ll_conf: - _LOGGER.warning( - "Resources need to be specified in your configuration.yaml. Please see the docs." - ) - yaml_resources = ll_conf[CONF_RESOURCES] - - resource_collection = resources.ResourceYAMLCollection(yaml_resources or []) + async_register_admin_service( + hass, + DOMAIN, + SERVICE_RELOAD_RESOURCES, + reload_resources_service_handler, + schema=RESOURCE_RELOAD_SERVICE_SCHEMA, + ) else: default_config = dashboard.LovelaceStorage(hass, None) @@ -196,6 +214,24 @@ async def async_setup(hass, config): return True +async def create_yaml_resource_col(hass, yaml_resources): + """Create yaml resources collection.""" + if yaml_resources is None: + default_config = dashboard.LovelaceYAML(hass, None, None) + try: + ll_conf = await default_config.async_load(False) + except HomeAssistantError: + pass + else: + if CONF_RESOURCES in ll_conf: + _LOGGER.warning( + "Resources need to be specified in your configuration.yaml. Please see the docs." + ) + yaml_resources = ll_conf[CONF_RESOURCES] + + return resources.ResourceYAMLCollection(yaml_resources or []) + + async def system_health_info(hass): """Get info for the info page.""" return await hass.data[DOMAIN]["dashboards"][None].async_get_info() diff --git a/homeassistant/components/lovelace/const.py b/homeassistant/components/lovelace/const.py index 8d7ee092cbe..a093c672dd6 100644 --- a/homeassistant/components/lovelace/const.py +++ b/homeassistant/components/lovelace/const.py @@ -41,6 +41,9 @@ RESOURCE_UPDATE_FIELDS = { vol.Optional(CONF_URL): cv.string, } +SERVICE_RELOAD_RESOURCES = "reload_resources" +RESOURCE_RELOAD_SERVICE_SCHEMA = vol.Schema({}) + CONF_TITLE = "title" CONF_REQUIRE_ADMIN = "require_admin" CONF_SHOW_IN_SIDEBAR = "show_in_sidebar" diff --git a/homeassistant/components/lovelace/services.yaml b/homeassistant/components/lovelace/services.yaml new file mode 100644 index 00000000000..1147f287e59 --- /dev/null +++ b/homeassistant/components/lovelace/services.yaml @@ -0,0 +1,4 @@ +# Describes the format for available lovelace services + +reload_resources: + description: Reload Lovelace resources from yaml configuration. From 41cd3ba532ee329d67c6e042793dab5628f4d809 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 16 Mar 2020 15:29:14 -0400 Subject: [PATCH 078/431] Bump ZHA quirks to 0.0.37 (#32867) --- 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 fec85625ee4..19940eaea00 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -5,7 +5,7 @@ "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows-homeassistant==0.14.0", - "zha-quirks==0.0.36", + "zha-quirks==0.0.37", "zigpy-cc==0.1.0", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.16.0", diff --git a/requirements_all.txt b/requirements_all.txt index 39b9857ca00..75fd30aac18 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2158,7 +2158,7 @@ zengge==0.2 zeroconf==0.24.5 # homeassistant.components.zha -zha-quirks==0.0.36 +zha-quirks==0.0.37 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7046f54f42a..974dbce4826 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -756,7 +756,7 @@ yahooweather==0.10 zeroconf==0.24.5 # homeassistant.components.zha -zha-quirks==0.0.36 +zha-quirks==0.0.37 # homeassistant.components.zha zigpy-cc==0.1.0 From 682fcec99e45a8aac9e5a182765ace1910033133 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 16 Mar 2020 22:27:20 +0100 Subject: [PATCH 079/431] Updated frontend to 20200316.1 (#32878) --- 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 211e5bc7e84..174bab5a189 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==20200316.0" + "home-assistant-frontend==20200316.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 72182b1f6be..f4b15cdec87 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200316.0 +home-assistant-frontend==20200316.1 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 75fd30aac18..3e427ad270d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -696,7 +696,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200316.0 +home-assistant-frontend==20200316.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 974dbce4826..598758949df 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200316.0 +home-assistant-frontend==20200316.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 From 397238372efe7e74613c49c4c542a39d0e49f8df Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 16 Mar 2020 14:47:44 -0700 Subject: [PATCH 080/431] Remove group as a dependency from entity integrations (#32870) * remove group dependency * Update device sun light trigger * Add zone dep back to device tracker --- homeassistant/components/automation/manifest.json | 3 ++- homeassistant/components/cover/manifest.json | 2 +- .../components/device_sun_light_trigger/manifest.json | 3 ++- homeassistant/components/device_tracker/manifest.json | 3 ++- homeassistant/components/fan/manifest.json | 2 +- homeassistant/components/light/manifest.json | 2 +- homeassistant/components/lock/manifest.json | 2 +- homeassistant/components/plant/manifest.json | 2 +- homeassistant/components/remote/manifest.json | 2 +- homeassistant/components/script/manifest.json | 2 +- homeassistant/components/switch/manifest.json | 2 +- homeassistant/components/vacuum/manifest.json | 2 +- script/hassfest/dependencies.py | 4 ++-- script/hassfest/manifest.py | 4 ++-- 14 files changed, 19 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/automation/manifest.json b/homeassistant/components/automation/manifest.json index 34261cba5a9..48d8c58dfe1 100644 --- a/homeassistant/components/automation/manifest.json +++ b/homeassistant/components/automation/manifest.json @@ -3,7 +3,8 @@ "name": "Automation", "documentation": "https://www.home-assistant.io/integrations/automation", "requirements": [], - "dependencies": ["device_automation", "group", "webhook"], + "dependencies": [], + "after_dependencies": ["device_automation", "webhook"], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/cover/manifest.json b/homeassistant/components/cover/manifest.json index aa43e934dc9..788d72b707f 100644 --- a/homeassistant/components/cover/manifest.json +++ b/homeassistant/components/cover/manifest.json @@ -3,7 +3,7 @@ "name": "Cover", "documentation": "https://www.home-assistant.io/integrations/cover", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/device_sun_light_trigger/manifest.json b/homeassistant/components/device_sun_light_trigger/manifest.json index 702f8704564..edeb10dcec2 100644 --- a/homeassistant/components/device_sun_light_trigger/manifest.json +++ b/homeassistant/components/device_sun_light_trigger/manifest.json @@ -3,7 +3,8 @@ "name": "Presence-based Lights", "documentation": "https://www.home-assistant.io/integrations/device_sun_light_trigger", "requirements": [], - "dependencies": ["device_tracker", "group", "light", "person"], + "dependencies": [], + "after_dependencies": ["device_tracker", "group", "light", "person"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 35b9a4a3fdb..4bd9846f76d 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -3,7 +3,8 @@ "name": "Device Tracker", "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], - "dependencies": ["group", "zone"], + "dependencies": ["zone"], + "after_dependencies": [], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/fan/manifest.json b/homeassistant/components/fan/manifest.json index 02ed368feac..53b7873612c 100644 --- a/homeassistant/components/fan/manifest.json +++ b/homeassistant/components/fan/manifest.json @@ -3,7 +3,7 @@ "name": "Fan", "documentation": "https://www.home-assistant.io/integrations/fan", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/light/manifest.json b/homeassistant/components/light/manifest.json index e0a9652a10c..64e21654afd 100644 --- a/homeassistant/components/light/manifest.json +++ b/homeassistant/components/light/manifest.json @@ -3,7 +3,7 @@ "name": "Light", "documentation": "https://www.home-assistant.io/integrations/light", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/lock/manifest.json b/homeassistant/components/lock/manifest.json index ab05166d15f..cd2fdf27f2d 100644 --- a/homeassistant/components/lock/manifest.json +++ b/homeassistant/components/lock/manifest.json @@ -3,7 +3,7 @@ "name": "Lock", "documentation": "https://www.home-assistant.io/integrations/lock", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/plant/manifest.json b/homeassistant/components/plant/manifest.json index de5f0c1f880..f0ff20f3759 100644 --- a/homeassistant/components/plant/manifest.json +++ b/homeassistant/components/plant/manifest.json @@ -3,7 +3,7 @@ "name": "Plant Monitor", "documentation": "https://www.home-assistant.io/integrations/plant", "requirements": [], - "dependencies": ["group", "zone"], + "dependencies": [], "after_dependencies": ["recorder"], "codeowners": ["@ChristianKuehnel"], "quality_scale": "internal" diff --git a/homeassistant/components/remote/manifest.json b/homeassistant/components/remote/manifest.json index 24616bc5947..8f559b758d6 100644 --- a/homeassistant/components/remote/manifest.json +++ b/homeassistant/components/remote/manifest.json @@ -3,6 +3,6 @@ "name": "Remote", "documentation": "https://www.home-assistant.io/integrations/remote", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/script/manifest.json b/homeassistant/components/script/manifest.json index dac37110172..ce9899f021c 100644 --- a/homeassistant/components/script/manifest.json +++ b/homeassistant/components/script/manifest.json @@ -3,7 +3,7 @@ "name": "Scripts", "documentation": "https://www.home-assistant.io/integrations/script", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": ["@home-assistant/core"], "quality_scale": "internal" } diff --git a/homeassistant/components/switch/manifest.json b/homeassistant/components/switch/manifest.json index b14c8ca48d5..37cdf77172c 100644 --- a/homeassistant/components/switch/manifest.json +++ b/homeassistant/components/switch/manifest.json @@ -3,7 +3,7 @@ "name": "Switch", "documentation": "https://www.home-assistant.io/integrations/switch", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/vacuum/manifest.json b/homeassistant/components/vacuum/manifest.json index 895311ae5b6..a6f7ddb2bda 100644 --- a/homeassistant/components/vacuum/manifest.json +++ b/homeassistant/components/vacuum/manifest.json @@ -3,6 +3,6 @@ "name": "Vacuum", "documentation": "https://www.home-assistant.io/integrations/vacuum", "requirements": [], - "dependencies": ["group"], + "dependencies": [], "codeowners": [] } diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index 934400533e1..660e8065966 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -156,7 +156,7 @@ def calc_allowed_references(integration: Integration) -> Set[str]: """Return a set of allowed references.""" allowed_references = ( ALLOWED_USED_COMPONENTS - | set(integration.manifest["dependencies"]) + | set(integration.manifest.get("dependencies", [])) | set(integration.manifest.get("after_dependencies", [])) ) @@ -250,7 +250,7 @@ def validate(integrations: Dict[str, Integration], config): validate_dependencies(integrations, integration) # check that all referenced dependencies exist - for dep in integration.manifest["dependencies"]: + for dep in integration.manifest.get("dependencies", []): if dep not in integrations: integration.add_error( "dependencies", f"Dependency {dep} does not exist" diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 7852953dc92..758279cabf8 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -52,8 +52,8 @@ MANIFEST_SCHEMA = vol.Schema( vol.Url(), documentation_url # pylint: disable=no-value-for-parameter ), vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), - vol.Required("requirements"): [str], - vol.Required("dependencies"): [str], + vol.Optional("requirements"): [str], + vol.Optional("dependencies"): [str], vol.Optional("after_dependencies"): [str], vol.Required("codeowners"): [str], vol.Optional("logo"): vol.Url(), # pylint: disable=no-value-for-parameter From 51b9afe3c194818057f1901864bee06435bdbd0f Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 17 Mar 2020 00:08:06 +0100 Subject: [PATCH 081/431] Add device condition for alarm_control_panel (#29063) * Initial commit * Restricting conditions by supported_features, drop pending condition * Added tests for no and minimum amount of conditions * Address review comments * Sort impors with isort * Sort impors with isort v2 --- .../components/alarm_control_panel/const.py | 7 + .../alarm_control_panel/device_condition.py | 162 +++++++++ .../alarm_control_panel/strings.json | 37 +- .../test_device_condition.py | 334 ++++++++++++++++++ 4 files changed, 525 insertions(+), 15 deletions(-) create mode 100644 homeassistant/components/alarm_control_panel/device_condition.py create mode 100644 tests/components/alarm_control_panel/test_device_condition.py diff --git a/homeassistant/components/alarm_control_panel/const.py b/homeassistant/components/alarm_control_panel/const.py index 77f7846fc34..2844cb286ab 100644 --- a/homeassistant/components/alarm_control_panel/const.py +++ b/homeassistant/components/alarm_control_panel/const.py @@ -5,3 +5,10 @@ SUPPORT_ALARM_ARM_AWAY = 2 SUPPORT_ALARM_ARM_NIGHT = 4 SUPPORT_ALARM_TRIGGER = 8 SUPPORT_ALARM_ARM_CUSTOM_BYPASS = 16 + +CONDITION_TRIGGERED = "is_triggered" +CONDITION_DISARMED = "is_disarmed" +CONDITION_ARMED_HOME = "is_armed_home" +CONDITION_ARMED_AWAY = "is_armed_away" +CONDITION_ARMED_NIGHT = "is_armed_night" +CONDITION_ARMED_CUSTOM_BYPASS = "is_armed_custom_bypass" diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py new file mode 100644 index 00000000000..068c665ca5e --- /dev/null +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -0,0 +1,162 @@ +"""Provide the device automations for Alarm control panel.""" +from typing import Dict, List + +import voluptuous as vol + +from homeassistant.components.alarm_control_panel.const import ( + SUPPORT_ALARM_ARM_AWAY, + SUPPORT_ALARM_ARM_CUSTOM_BYPASS, + SUPPORT_ALARM_ARM_HOME, + SUPPORT_ALARM_ARM_NIGHT, +) +from homeassistant.const import ( + ATTR_ENTITY_ID, + CONF_CONDITION, + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_ENTITY_ID, + CONF_TYPE, + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.core import HomeAssistant +from homeassistant.helpers import condition, config_validation as cv, entity_registry +from homeassistant.helpers.config_validation import DEVICE_CONDITION_BASE_SCHEMA +from homeassistant.helpers.typing import ConfigType, TemplateVarsType + +from . import DOMAIN +from .const import ( + CONDITION_ARMED_AWAY, + CONDITION_ARMED_CUSTOM_BYPASS, + CONDITION_ARMED_HOME, + CONDITION_ARMED_NIGHT, + CONDITION_DISARMED, + CONDITION_TRIGGERED, +) + +CONDITION_TYPES = { + CONDITION_TRIGGERED, + CONDITION_DISARMED, + CONDITION_ARMED_HOME, + CONDITION_ARMED_AWAY, + CONDITION_ARMED_NIGHT, + CONDITION_ARMED_CUSTOM_BYPASS, +} + +CONDITION_SCHEMA = DEVICE_CONDITION_BASE_SCHEMA.extend( + { + vol.Required(CONF_ENTITY_ID): cv.entity_id, + vol.Required(CONF_TYPE): vol.In(CONDITION_TYPES), + } +) + + +async def async_get_conditions( + hass: HomeAssistant, device_id: str +) -> List[Dict[str, str]]: + """List device conditions for Alarm control panel devices.""" + registry = await entity_registry.async_get_registry(hass) + conditions = [] + + # Get all the integrations entities for this device + for entry in entity_registry.async_entries_for_device(registry, device_id): + if entry.domain != DOMAIN: + continue + + state = hass.states.get(entry.entity_id) + + # We need a state or else we can't populate the different armed conditions + if state is None: + continue + + supported_features = state.attributes["supported_features"] + + # Add conditions for each entity that belongs to this integration + conditions += [ + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_DISARMED, + }, + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_TRIGGERED, + }, + ] + if supported_features & SUPPORT_ALARM_ARM_HOME: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_HOME, + } + ) + if supported_features & SUPPORT_ALARM_ARM_AWAY: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_AWAY, + } + ) + if supported_features & SUPPORT_ALARM_ARM_NIGHT: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_NIGHT, + } + ) + if supported_features & SUPPORT_ALARM_ARM_CUSTOM_BYPASS: + conditions.append( + { + CONF_CONDITION: "device", + CONF_DEVICE_ID: device_id, + CONF_DOMAIN: DOMAIN, + CONF_ENTITY_ID: entry.entity_id, + CONF_TYPE: CONDITION_ARMED_CUSTOM_BYPASS, + } + ) + + return conditions + + +def async_condition_from_config( + config: ConfigType, config_validation: bool +) -> condition.ConditionCheckerType: + """Create a function to test a device condition.""" + if config_validation: + config = CONDITION_SCHEMA(config) + elif config[CONF_TYPE] == CONDITION_TRIGGERED: + state = STATE_ALARM_TRIGGERED + elif config[CONF_TYPE] == CONDITION_DISARMED: + state = STATE_ALARM_DISARMED + elif config[CONF_TYPE] == CONDITION_ARMED_HOME: + state = STATE_ALARM_ARMED_HOME + elif config[CONF_TYPE] == CONDITION_ARMED_AWAY: + state = STATE_ALARM_ARMED_AWAY + elif config[CONF_TYPE] == CONDITION_ARMED_NIGHT: + state = STATE_ALARM_ARMED_NIGHT + elif config[CONF_TYPE] == CONDITION_ARMED_CUSTOM_BYPASS: + state = STATE_ALARM_ARMED_CUSTOM_BYPASS + + def test_is_state(hass: HomeAssistant, variables: TemplateVarsType) -> bool: + """Test if an entity is a certain state.""" + return condition.state(hass, config[ATTR_ENTITY_ID], state) + + return test_is_state diff --git a/homeassistant/components/alarm_control_panel/strings.json b/homeassistant/components/alarm_control_panel/strings.json index cbca15c8cf6..4e14a8c2a3d 100644 --- a/homeassistant/components/alarm_control_panel/strings.json +++ b/homeassistant/components/alarm_control_panel/strings.json @@ -1,18 +1,25 @@ { - "device_automation": { - "action_type": { - "arm_away": "Arm {entity_name} away", - "arm_home": "Arm {entity_name} home", - "arm_night": "Arm {entity_name} night", - "disarm": "Disarm {entity_name}", - "trigger": "Trigger {entity_name}" - }, - "trigger_type": { - "triggered": "{entity_name} triggered", - "disarmed": "{entity_name} disarmed", - "armed_home": "{entity_name} armed home", - "armed_away": "{entity_name} armed away", - "armed_night": "{entity_name} armed night" + "device_automation": { + "action_type": { + "arm_away": "Arm {entity_name} away", + "arm_home": "Arm {entity_name} home", + "arm_night": "Arm {entity_name} night", + "disarm": "Disarm {entity_name}", + "trigger": "Trigger {entity_name}" + }, + "condition_type": { + "is_triggered": "{entity_name} is triggered", + "is_disarmed": "{entity_name} is disarmed", + "is_armed_home": "{entity_name} is armed home", + "is_armed_away": "{entity_name} is armed away", + "is_armed_night": "{entity_name} is armed night" + }, + "trigger_type": { + "triggered": "{entity_name} triggered", + "disarmed": "{entity_name} disarmed", + "armed_home": "{entity_name} armed home", + "armed_away": "{entity_name} armed away", + "armed_night": "{entity_name} armed night" + } } - } } \ No newline at end of file diff --git a/tests/components/alarm_control_panel/test_device_condition.py b/tests/components/alarm_control_panel/test_device_condition.py new file mode 100644 index 00000000000..fcb2ba5a09b --- /dev/null +++ b/tests/components/alarm_control_panel/test_device_condition.py @@ -0,0 +1,334 @@ +"""The tests for Alarm control panel device conditions.""" +import pytest + +from homeassistant.components.alarm_control_panel import DOMAIN +import homeassistant.components.automation as automation +from homeassistant.const import ( + STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, + STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_TRIGGERED, +) +from homeassistant.helpers import device_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): + """Return an empty, loaded, registry.""" + return mock_device_registry(hass) + + +@pytest.fixture +def entity_reg(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@pytest.fixture +def calls(hass): + """Track calls to a mock service.""" + return async_mock_service(hass, "test", "automation") + + +async def test_get_no_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a alarm_control_panel.""" + 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) + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, []) + + +async def test_get_minimum_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a alarm_control_panel.""" + 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) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 0} + ) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_disarmed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_triggered", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_get_maximum_conditions(hass, device_reg, entity_reg): + """Test we get the expected conditions from a alarm_control_panel.""" + 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) + hass.states.async_set( + "alarm_control_panel.test_5678", "attributes", {"supported_features": 31} + ) + expected_conditions = [ + { + "condition": "device", + "domain": DOMAIN, + "type": "is_disarmed", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_triggered", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_armed_home", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_armed_away", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_armed_night", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + { + "condition": "device", + "domain": DOMAIN, + "type": "is_armed_custom_bypass", + "device_id": device_entry.id, + "entity_id": f"{DOMAIN}.test_5678", + }, + ] + + conditions = await async_get_device_automations(hass, "condition", device_entry.id) + assert_lists_same(conditions, expected_conditions) + + +async def test_if_state(hass, calls): + """Test for all conditions.""" + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": {"platform": "event", "event_type": "test_event1"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_triggered", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_triggered - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event2"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_disarmed", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_disarmed - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event3"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_armed_home", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_armed_home - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event4"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_armed_away", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_armed_away - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event5"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_armed_night", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_armed_night - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + { + "trigger": {"platform": "event", "event_type": "test_event6"}, + "condition": [ + { + "condition": "device", + "domain": DOMAIN, + "device_id": "", + "entity_id": "alarm_control_panel.entity", + "type": "is_armed_custom_bypass", + } + ], + "action": { + "service": "test.automation", + "data_template": { + "some": "is_armed_custom_bypass - {{ trigger.platform }} - {{ trigger.event.event_type }}" + }, + }, + }, + ] + }, + ) + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_TRIGGERED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + await hass.async_block_till_done() + assert len(calls) == 1 + assert calls[0].data["some"] == "is_triggered - event - test_event1" + + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_DISARMED) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + await hass.async_block_till_done() + assert len(calls) == 2 + assert calls[1].data["some"] == "is_disarmed - event - test_event2" + + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_HOME) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + await hass.async_block_till_done() + assert len(calls) == 3 + assert calls[2].data["some"] == "is_armed_home - event - test_event3" + + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_AWAY) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + await hass.async_block_till_done() + assert len(calls) == 4 + assert calls[3].data["some"] == "is_armed_away - event - test_event4" + + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_NIGHT) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + await hass.async_block_till_done() + assert len(calls) == 5 + assert calls[4].data["some"] == "is_armed_night - event - test_event5" + + hass.states.async_set("alarm_control_panel.entity", STATE_ALARM_ARMED_CUSTOM_BYPASS) + hass.bus.async_fire("test_event1") + hass.bus.async_fire("test_event2") + hass.bus.async_fire("test_event3") + hass.bus.async_fire("test_event4") + hass.bus.async_fire("test_event5") + hass.bus.async_fire("test_event6") + await hass.async_block_till_done() + assert len(calls) == 6 + assert calls[5].data["some"] == "is_armed_custom_bypass - event - test_event6" From 7ac014744c6afff7c60a3aa21707edcb7930dcb9 Mon Sep 17 00:00:00 2001 From: Paolo Tuninetto Date: Tue, 17 Mar 2020 00:37:10 +0100 Subject: [PATCH 082/431] Add default port to samsung tv (#32820) * Default port for websocket tv * Update config entry * move bridge creation * fix indent * remove loop --- homeassistant/components/samsungtv/bridge.py | 2 ++ .../components/samsungtv/media_player.py | 26 ++++++++++++------- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index 31f102a62a4..b582f6269e4 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -46,6 +46,7 @@ class SamsungTVBridge(ABC): self.method = method self.host = host self.token = None + self.default_port = None self._remote = None self._callback = None @@ -191,6 +192,7 @@ class SamsungTVWSBridge(SamsungTVBridge): """Initialize Bridge.""" super().__init__(method, host, port) self.token = token + self.default_port = 8001 def try_connect(self): """Try to connect to the Websocket TV.""" diff --git a/homeassistant/components/samsungtv/media_player.py b/homeassistant/components/samsungtv/media_player.py index 8fa6a93088a..8f12341ee4a 100644 --- a/homeassistant/components/samsungtv/media_player.py +++ b/homeassistant/components/samsungtv/media_player.py @@ -71,13 +71,27 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ): turn_on_action = hass.data[DOMAIN][ip_address][CONF_ON_ACTION] on_script = Script(hass, turn_on_action) - async_add_entities([SamsungTVDevice(config_entry, on_script)]) + + # Initialize bridge + data = config_entry.data.copy() + bridge = SamsungTVBridge.get_bridge( + data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN), + ) + if bridge.port is None and bridge.default_port is not None: + # For backward compat, set default port for websocket tv + data[CONF_PORT] = bridge.default_port + hass.config_entries.async_update_entry(config_entry, data=data) + bridge = SamsungTVBridge.get_bridge( + data[CONF_METHOD], data[CONF_HOST], data[CONF_PORT], data.get(CONF_TOKEN), + ) + + async_add_entities([SamsungTVDevice(bridge, config_entry, on_script)]) class SamsungTVDevice(MediaPlayerDevice): """Representation of a Samsung TV.""" - def __init__(self, config_entry, on_script): + def __init__(self, bridge, config_entry, on_script): """Initialize the Samsung device.""" self._config_entry = config_entry self._manufacturer = config_entry.data.get(CONF_MANUFACTURER) @@ -93,13 +107,7 @@ class SamsungTVDevice(MediaPlayerDevice): # 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 = None - # Initialize bridge - self._bridge = SamsungTVBridge.get_bridge( - config_entry.data[CONF_METHOD], - config_entry.data[CONF_HOST], - config_entry.data[CONF_PORT], - config_entry.data.get(CONF_TOKEN), - ) + self._bridge = bridge self._bridge.register_reauth_callback(self.access_denied) def access_denied(self): From 2cda7bf1e7e160d996359188f1fa25265914650b Mon Sep 17 00:00:00 2001 From: Kevin Eifinger Date: Tue, 17 Mar 2020 05:16:49 +0100 Subject: [PATCH 083/431] Add Here travel time arrival departure (#29909) * here_travel_time: Add modes arrival and departure * convert arrival/departure from datetime to time * Default departure is set by external lib on None * Use cv.key_value_schemas --- .../components/here_travel_time/sensor.py | 97 ++++++++--- .../here_travel_time/test_sensor.py | 150 ++++++++++++++++-- 2 files changed, 207 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index 4c7652484d6..d93cfdf7053 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -1,5 +1,5 @@ """Support for HERE travel time sensors.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging from typing import Callable, Dict, Optional, Union @@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt _LOGGER = logging.getLogger(__name__) @@ -36,6 +37,8 @@ CONF_ORIGIN_ENTITY_ID = "origin_entity_id" CONF_API_KEY = "api_key" CONF_TRAFFIC_MODE = "traffic_mode" CONF_ROUTE_MODE = "route_mode" +CONF_ARRIVAL = "arrival" +CONF_DEPARTURE = "departure" DEFAULT_NAME = "HERE Travel Time" @@ -90,32 +93,49 @@ SCAN_INTERVAL = timedelta(minutes=5) NO_ROUTE_ERROR_MESSAGE = "HERE could not find a route based on the input" +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_API_KEY): cv.string, + vol.Inclusive( + CONF_DESTINATION_LATITUDE, "destination_coordinates" + ): cv.latitude, + vol.Inclusive( + CONF_DESTINATION_LONGITUDE, "destination_coordinates" + ): cv.longitude, + vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, + vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, + vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, + vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, + vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, + vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, + vol.Optional(CONF_DEPARTURE): cv.time, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE), + vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In(ROUTE_MODE), + vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, + vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), + } +) + PLATFORM_SCHEMA = vol.All( cv.has_at_least_one_key(CONF_DESTINATION_LATITUDE, CONF_DESTINATION_ENTITY_ID), cv.has_at_least_one_key(CONF_ORIGIN_LATITUDE, CONF_ORIGIN_ENTITY_ID), - PLATFORM_SCHEMA.extend( + cv.key_value_schemas( + CONF_MODE, { - vol.Required(CONF_API_KEY): cv.string, - vol.Inclusive( - CONF_DESTINATION_LATITUDE, "destination_coordinates" - ): cv.latitude, - vol.Inclusive( - CONF_DESTINATION_LONGITUDE, "destination_coordinates" - ): cv.longitude, - vol.Exclusive(CONF_DESTINATION_LATITUDE, "destination"): cv.latitude, - vol.Exclusive(CONF_DESTINATION_ENTITY_ID, "destination"): cv.entity_id, - vol.Inclusive(CONF_ORIGIN_LATITUDE, "origin_coordinates"): cv.latitude, - vol.Inclusive(CONF_ORIGIN_LONGITUDE, "origin_coordinates"): cv.longitude, - vol.Exclusive(CONF_ORIGIN_LATITUDE, "origin"): cv.latitude, - vol.Exclusive(CONF_ORIGIN_ENTITY_ID, "origin"): cv.entity_id, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_MODE, default=TRAVEL_MODE_CAR): vol.In(TRAVEL_MODE), - vol.Optional(CONF_ROUTE_MODE, default=ROUTE_MODE_FASTEST): vol.In( - ROUTE_MODE + None: PLATFORM_SCHEMA, + TRAVEL_MODE_BICYCLE: PLATFORM_SCHEMA, + TRAVEL_MODE_CAR: PLATFORM_SCHEMA, + TRAVEL_MODE_PEDESTRIAN: PLATFORM_SCHEMA, + TRAVEL_MODE_PUBLIC: PLATFORM_SCHEMA, + TRAVEL_MODE_TRUCK: PLATFORM_SCHEMA, + TRAVEL_MODE_PUBLIC_TIME_TABLE: PLATFORM_SCHEMA.extend( + { + vol.Exclusive(CONF_ARRIVAL, "arrival_departure"): cv.time, + vol.Exclusive(CONF_DEPARTURE, "arrival_departure"): cv.time, + } ), - vol.Optional(CONF_TRAFFIC_MODE, default=False): cv.boolean, - vol.Optional(CONF_UNIT_SYSTEM): vol.In(UNITS), - } + }, ), ) @@ -160,9 +180,11 @@ async def async_setup_platform( route_mode = config[CONF_ROUTE_MODE] name = config[CONF_NAME] units = config.get(CONF_UNIT_SYSTEM, hass.config.units.name) + arrival = config.get(CONF_ARRIVAL) + departure = config.get(CONF_DEPARTURE) here_data = HERETravelTimeData( - here_client, travel_mode, traffic_mode, route_mode, units + here_client, travel_mode, traffic_mode, route_mode, units, arrival, departure ) sensor = HERETravelTimeSensor( @@ -361,6 +383,8 @@ class HERETravelTimeData: traffic_mode: bool, route_mode: str, units: str, + arrival: datetime, + departure: datetime, ) -> None: """Initialize herepy.""" self.origin = None @@ -368,6 +392,8 @@ class HERETravelTimeData: self.travel_mode = travel_mode self.traffic_mode = traffic_mode self.route_mode = route_mode + self.arrival = arrival + self.departure = departure self.attribution = None self.traffic_time = None self.distance = None @@ -377,6 +403,7 @@ class HERETravelTimeData: self.destination_name = None self.units = units self._client = here_client + self.combine_change = True def update(self) -> None: """Get the latest data from HERE.""" @@ -389,24 +416,36 @@ class HERETravelTimeData: # Convert location to HERE friendly location destination = self.destination.split(",") origin = self.origin.split(",") + arrival = self.arrival + if arrival is not None: + arrival = convert_time_to_isodate(arrival) + departure = self.departure + if departure is not None: + departure = convert_time_to_isodate(departure) _LOGGER.debug( - "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s", + "Requesting route for origin: %s, destination: %s, route_mode: %s, mode: %s, traffic_mode: %s, arrival: %s, departure: %s", origin, destination, herepy.RouteMode[self.route_mode], herepy.RouteMode[self.travel_mode], herepy.RouteMode[traffic_mode], + arrival, + departure, ) + try: - response = self._client.car_route( + response = self._client.public_transport_timetable( origin, destination, + self.combine_change, [ herepy.RouteMode[self.route_mode], herepy.RouteMode[self.travel_mode], herepy.RouteMode[traffic_mode], ], + arrival=arrival, + departure=departure, ) except herepy.NoRouteFoundError: # Better error message for cryptic no route error codes @@ -453,3 +492,11 @@ class HERETravelTimeData: joined_supplier_titles = ",".join(supplier_titles) attribution = f"With the support of {joined_supplier_titles}. All information is provided without warranty of any kind." return attribution + + +def convert_time_to_isodate(timestr: str) -> str: + """Take a string like 08:00:00 and combine it with the current date.""" + combined = datetime.combine(dt.start_of_local_day(), dt.parse_time(timestr)) + if combined < datetime.now(): + combined = combined + timedelta(days=1) + return combined.isoformat() diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index fcae8bd1f8c..642b774f1e5 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -37,6 +37,7 @@ from homeassistant.components.here_travel_time.sensor import ( TRAVEL_MODE_PUBLIC, TRAVEL_MODE_PUBLIC_TIME_TABLE, TRAVEL_MODE_TRUCK, + convert_time_to_isodate, ) from homeassistant.const import ATTR_ICON, EVENT_HOMEASSISTANT_START from homeassistant.setup import async_setup_component @@ -66,7 +67,7 @@ CAR_DESTINATION_LATITUDE = "39.0" CAR_DESTINATION_LONGITUDE = "-77.1" -def _build_mock_url(origin, destination, modes, api_key, departure): +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 = { @@ -74,9 +75,13 @@ def _build_mock_url(origin, destination, modes, api_key, departure): "waypoint1": f"geo!{destination}", "mode": ";".join(str(herepy.RouteMode[mode]) for mode in modes), "apikey": api_key, - "departure": departure, } + if arrival is not None: + parameters["arrival"] = arrival + if departure is not None: + parameters["departure"] = departure url = base_url + urllib.parse.urlencode(parameters) + print(url) return url @@ -117,7 +122,6 @@ def requests_mock_credentials_check(requests_mock): ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, API_KEY, - "now", ) requests_mock.get( response_url, text=load_fixture("here_travel_time/car_response.json") @@ -134,7 +138,6 @@ def requests_mock_truck_response(requests_mock_credentials_check): ",".join([TRUCK_DESTINATION_LATITUDE, TRUCK_DESTINATION_LONGITUDE]), modes, API_KEY, - "now", ) requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/truck_response.json") @@ -150,7 +153,6 @@ def requests_mock_car_disabled_response(requests_mock_credentials_check): ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, API_KEY, - "now", ) requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/car_response.json") @@ -214,7 +216,6 @@ async def test_traffic_mode_enabled(hass, requests_mock_credentials_check): ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, API_KEY, - "now", ) requests_mock_credentials_check.get( response_url, text=load_fixture("here_travel_time/car_enabled_response.json") @@ -272,7 +273,7 @@ async def test_route_mode_shortest(hass, requests_mock_credentials_check): 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, "now") + 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") ) @@ -303,7 +304,7 @@ async def test_route_mode_fastest(hass, requests_mock_credentials_check): 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, "now") + 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") ) @@ -357,7 +358,7 @@ async def test_public_transport(hass, requests_mock_credentials_check): 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, "now") + 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") ) @@ -406,7 +407,7 @@ async def test_public_transport_time_table(hass, requests_mock_credentials_check 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, "now") + 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"), @@ -456,7 +457,7 @@ async def test_pedestrian(hass, requests_mock_credentials_check): 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, "now") + 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") ) @@ -508,7 +509,7 @@ async def test_bicycle(hass, requests_mock_credentials_check): 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, "now") + 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") ) @@ -841,7 +842,7 @@ async def test_route_not_found(hass, requests_mock_credentials_check, caplog): 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, "now") + 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"), @@ -914,7 +915,6 @@ async def test_invalid_credentials(hass, requests_mock, caplog): ",".join([CAR_DESTINATION_LATITUDE, CAR_DESTINATION_LONGITUDE]), modes, API_KEY, - "now", ) requests_mock.get( response_url, @@ -942,7 +942,7 @@ async def test_attribution(hass, requests_mock_credentials_check): 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, "now") + 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") ) @@ -1051,3 +1051,123 @@ async def test_delayed_update(hass, requests_mock_truck_response, caplog): 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], + "api_key": API_KEY, + "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + "arrival": arrival, + } + } + assert await async_setup_component(hass, DOMAIN, config) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + 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) + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.test") + assert sensor.state == "80" + + +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], + "api_key": API_KEY, + "arrival": "01:00:00", + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "[arrival] is an invalid option" in caplog.text + + +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], + "api_key": API_KEY, + "arrival": "01:00:00", + "mode": TRAVEL_MODE_PUBLIC_TIME_TABLE, + "departure": "01:00:00", + } + } + assert await async_setup_component(hass, DOMAIN, config) + assert len(caplog.records) == 1 + assert "two or more values in the same group of exclusion" in caplog.text From a278cf3db240ebf17a7f04c6794c937481d14b65 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 16 Mar 2020 23:58:50 -0600 Subject: [PATCH 084/431] Improve IQVIA data/API management based on enabled entities (#32291) * Improve IQVIA data/API management based on enabled entities * Code review comments * Code review * Cleanup * Linting * Code review * Code review --- homeassistant/components/iqvia/__init__.py | 172 +++++++++++++++------ homeassistant/components/iqvia/const.py | 2 +- homeassistant/components/iqvia/sensor.py | 36 ++--- 3 files changed, 140 insertions(+), 70 deletions(-) diff --git a/homeassistant/components/iqvia/__init__.py b/homeassistant/components/iqvia/__init__.py index a33dabeadeb..1f487dd345c 100644 --- a/homeassistant/components/iqvia/__init__.py +++ b/homeassistant/components/iqvia/__init__.py @@ -4,7 +4,7 @@ from datetime import timedelta import logging from pyiqvia import Client -from pyiqvia.errors import InvalidZipError +from pyiqvia.errors import InvalidZipError, IQVIAError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT @@ -17,7 +17,6 @@ from homeassistant.helpers.dispatcher import ( ) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval -from homeassistant.util.decorator import Registry from .config_flow import configured_instances from .const import ( @@ -43,20 +42,20 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +API_CATEGORY_MAPPING = { + TYPE_ALLERGY_TODAY: TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_TOMORROW: TYPE_ALLERGY_INDEX, + TYPE_ALLERGY_TOMORROW: TYPE_ALLERGY_INDEX, + TYPE_ASTHMA_TODAY: TYPE_ASTHMA_INDEX, + TYPE_ASTHMA_TOMORROW: TYPE_ALLERGY_INDEX, + TYPE_DISEASE_TODAY: TYPE_DISEASE_INDEX, +} + DATA_CONFIG = "config" DEFAULT_ATTRIBUTION = "Data provided by IQVIA™" DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) -FETCHER_MAPPING = { - (TYPE_ALLERGY_FORECAST,): (TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_OUTLOOK), - (TYPE_ALLERGY_TODAY, TYPE_ALLERGY_TOMORROW): (TYPE_ALLERGY_INDEX,), - (TYPE_ASTHMA_FORECAST,): (TYPE_ASTHMA_FORECAST,), - (TYPE_ASTHMA_TODAY, TYPE_ASTHMA_TOMORROW): (TYPE_ASTHMA_INDEX,), - (TYPE_DISEASE_FORECAST,): (TYPE_DISEASE_FORECAST,), - (TYPE_DISEASE_TODAY,): (TYPE_DISEASE_INDEX,), -} - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.All( @@ -75,6 +74,12 @@ CONFIG_SCHEMA = vol.Schema( ) +@callback +def async_get_api_category(sensor_type): + """Return the API category that a particular sensor type should use.""" + return API_CATEGORY_MAPPING.get(sensor_type, sensor_type) + + async def async_setup(hass, config): """Set up the IQVIA component.""" hass.data[DOMAIN] = {} @@ -102,8 +107,9 @@ async def async_setup_entry(hass, config_entry): """Set up IQVIA as config entry.""" websession = aiohttp_client.async_get_clientsession(hass) + iqvia = IQVIAData(hass, Client(config_entry.data[CONF_ZIP_CODE], websession)) + try: - iqvia = IQVIAData(Client(config_entry.data[CONF_ZIP_CODE], websession)) await iqvia.async_update() except InvalidZipError: _LOGGER.error("Invalid ZIP code provided: %s", config_entry.data[CONF_ZIP_CODE]) @@ -115,16 +121,6 @@ async def async_setup_entry(hass, config_entry): hass.config_entries.async_forward_entry_setup(config_entry, "sensor") ) - async def refresh(event_time): - """Refresh IQVIA data.""" - _LOGGER.debug("Updating IQVIA data") - await iqvia.async_update() - async_dispatcher_send(hass, TOPIC_DATA_UPDATE) - - hass.data[DOMAIN][DATA_LISTENER][config_entry.entry_id] = async_track_time_interval( - hass, refresh, DEFAULT_SCAN_INTERVAL - ) - return True @@ -143,42 +139,99 @@ async def async_unload_entry(hass, config_entry): class IQVIAData: """Define a data object to retrieve info from IQVIA.""" - def __init__(self, client): + def __init__(self, hass, client): """Initialize.""" + self._async_cancel_time_interval_listener = None self._client = client + self._hass = hass self.data = {} self.zip_code = client.zip_code - self.fetchers = Registry() - self.fetchers.register(TYPE_ALLERGY_FORECAST)(self._client.allergens.extended) - self.fetchers.register(TYPE_ALLERGY_OUTLOOK)(self._client.allergens.outlook) - self.fetchers.register(TYPE_ALLERGY_INDEX)(self._client.allergens.current) - self.fetchers.register(TYPE_ASTHMA_FORECAST)(self._client.asthma.extended) - self.fetchers.register(TYPE_ASTHMA_INDEX)(self._client.asthma.current) - self.fetchers.register(TYPE_DISEASE_FORECAST)(self._client.disease.extended) - self.fetchers.register(TYPE_DISEASE_INDEX)(self._client.disease.current) + self._api_coros = { + TYPE_ALLERGY_FORECAST: client.allergens.extended, + TYPE_ALLERGY_INDEX: client.allergens.current, + TYPE_ALLERGY_OUTLOOK: client.allergens.outlook, + TYPE_ASTHMA_FORECAST: client.asthma.extended, + TYPE_ASTHMA_INDEX: client.asthma.current, + TYPE_DISEASE_FORECAST: client.disease.extended, + TYPE_DISEASE_INDEX: client.disease.current, + } + self._api_category_count = { + TYPE_ALLERGY_FORECAST: 0, + TYPE_ALLERGY_INDEX: 0, + TYPE_ALLERGY_OUTLOOK: 0, + TYPE_ASTHMA_FORECAST: 0, + TYPE_ASTHMA_INDEX: 0, + TYPE_DISEASE_FORECAST: 0, + TYPE_DISEASE_INDEX: 0, + } + self._api_category_locks = { + TYPE_ALLERGY_FORECAST: asyncio.Lock(), + TYPE_ALLERGY_INDEX: asyncio.Lock(), + TYPE_ALLERGY_OUTLOOK: asyncio.Lock(), + TYPE_ASTHMA_FORECAST: asyncio.Lock(), + TYPE_ASTHMA_INDEX: asyncio.Lock(), + TYPE_DISEASE_FORECAST: asyncio.Lock(), + TYPE_DISEASE_INDEX: asyncio.Lock(), + } + + async def _async_get_data_from_api(self, api_category): + """Update and save data for a particular API category.""" + if self._api_category_count[api_category] == 0: + return + + try: + self.data[api_category] = await self._api_coros[api_category]() + except IQVIAError as err: + _LOGGER.error("Unable to get %s data: %s", api_category, err) + self.data[api_category] = None + + async def _async_update_listener_action(self, now): + """Define an async_track_time_interval action to update data.""" + await self.async_update() + + @callback + def async_deregister_api_interest(self, sensor_type): + """Decrement the number of entities with data needs from an API category.""" + # If this deregistration should leave us with no registration at all, remove the + # time interval: + if sum(self._api_category_count.values()) == 0: + if self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener() + self._async_cancel_time_interval_listener = None + return + + api_category = async_get_api_category(sensor_type) + self._api_category_count[api_category] -= 1 + + async def async_register_api_interest(self, sensor_type): + """Increment the number of entities with data needs from an API category.""" + # If this is the first registration we have, start a time interval: + if not self._async_cancel_time_interval_listener: + self._async_cancel_time_interval_listener = async_track_time_interval( + self._hass, self._async_update_listener_action, DEFAULT_SCAN_INTERVAL, + ) + + api_category = async_get_api_category(sensor_type) + self._api_category_count[api_category] += 1 + + # If a sensor registers interest in a particular API call and the data doesn't + # exist for it yet, make the API call and grab the data: + async with self._api_category_locks[api_category]: + if api_category not in self.data: + await self._async_get_data_from_api(api_category) async def async_update(self): """Update IQVIA data.""" - tasks = {} + tasks = [ + self._async_get_data_from_api(api_category) + for api_category in self._api_coros + ] - for conditions, fetcher_types in FETCHER_MAPPING.items(): - if not any(c in SENSORS for c in conditions): - continue + await asyncio.gather(*tasks) - for fetcher_type in fetcher_types: - tasks[fetcher_type] = self.fetchers[fetcher_type]() - - results = await asyncio.gather(*tasks.values(), return_exceptions=True) - - for key, result in zip(tasks, results): - if isinstance(result, Exception): - _LOGGER.error("Unable to get %s data: %s", key, result) - self.data[key] = {} - continue - - _LOGGER.debug("Loaded new %s data", key) - self.data[key] = result + _LOGGER.debug("Received new data") + async_dispatcher_send(self._hass, TOPIC_DATA_UPDATE) class IQVIAEntity(Entity): @@ -245,13 +298,34 @@ class IQVIAEntity(Entity): @callback def update(): """Update the state.""" - self.async_schedule_update_ha_state(True) + self.update_from_latest_data() + self.async_write_ha_state() self._async_unsub_dispatcher_connect = async_dispatcher_connect( self.hass, TOPIC_DATA_UPDATE, update ) + await self._iqvia.async_register_api_interest(self._type) + if self._type == TYPE_ALLERGY_FORECAST: + # Entities that express interest in allergy forecast data should also + # express interest in allergy outlook data: + await self._iqvia.async_register_api_interest(TYPE_ALLERGY_OUTLOOK) + + self.update_from_latest_data() + async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() + self._async_unsub_dispatcher_connect = None + + self._iqvia.async_deregister_api_interest(self._type) + if self._type == TYPE_ALLERGY_FORECAST: + # Entities that lose interest in allergy forecast data should also lose + # interest in allergy outlook data: + self._iqvia.async_deregister_api_interest(TYPE_ALLERGY_OUTLOOK) + + @callback + def update_from_latest_data(self): + """Update the entity's state.""" + raise NotImplementedError() diff --git a/homeassistant/components/iqvia/const.py b/homeassistant/components/iqvia/const.py index 52e657bc2c0..95b03485597 100644 --- a/homeassistant/components/iqvia/const.py +++ b/homeassistant/components/iqvia/const.py @@ -25,9 +25,9 @@ SENSORS = { TYPE_ALLERGY_FORECAST: ("Allergy Index: Forecasted Average", "mdi:flower"), TYPE_ALLERGY_TODAY: ("Allergy Index: Today", "mdi:flower"), TYPE_ALLERGY_TOMORROW: ("Allergy Index: Tomorrow", "mdi:flower"), + TYPE_ASTHMA_FORECAST: ("Asthma Index: Forecasted Average", "mdi:flower"), TYPE_ASTHMA_TODAY: ("Asthma Index: Today", "mdi:flower"), TYPE_ASTHMA_TOMORROW: ("Asthma Index: Tomorrow", "mdi:flower"), - TYPE_ASTHMA_FORECAST: ("Asthma Index: Forecasted Average", "mdi:flower"), TYPE_DISEASE_FORECAST: ("Cold & Flu: Forecasted Average", "mdi:snowflake"), TYPE_DISEASE_TODAY: ("Cold & Flu Index: Today", "mdi:pill"), } diff --git a/homeassistant/components/iqvia/sensor.py b/homeassistant/components/iqvia/sensor.py index 1aae63a4908..5db4456b3c6 100644 --- a/homeassistant/components/iqvia/sensor.py +++ b/homeassistant/components/iqvia/sensor.py @@ -7,7 +7,6 @@ import numpy as np from homeassistant.components.iqvia import ( DATA_CLIENT, DOMAIN, - SENSORS, TYPE_ALLERGY_FORECAST, TYPE_ALLERGY_INDEX, TYPE_ALLERGY_OUTLOOK, @@ -23,6 +22,9 @@ from homeassistant.components.iqvia import ( IQVIAEntity, ) from homeassistant.const import ATTR_STATE +from homeassistant.core import callback + +from .const import SENSORS _LOGGER = logging.getLogger(__name__) @@ -65,13 +67,14 @@ async def async_setup_entry(hass, entry, async_add_entities): TYPE_DISEASE_TODAY: IndexSensor, } - sensors = [] - for sensor_type in SENSORS: - klass = sensor_class_mapping[sensor_type] - name, icon = SENSORS[sensor_type] - sensors.append(klass(iqvia, sensor_type, name, icon, iqvia.zip_code)) - - async_add_entities(sensors, True) + async_add_entities( + [ + sensor_class_mapping[sensor_type]( + iqvia, sensor_type, name, icon, iqvia.zip_code + ) + for sensor_type, (name, icon) in SENSORS.items() + ] + ) def calculate_trend(indices): @@ -93,9 +96,10 @@ def calculate_trend(indices): class ForecastSensor(IQVIAEntity): """Define sensor related to forecast data.""" - async def async_update(self): + @callback + def update_from_latest_data(self): """Update the sensor.""" - if not self._iqvia.data: + if not self._iqvia.data.get(self._type): return data = self._iqvia.data[self._type].get("Location") @@ -131,12 +135,10 @@ class ForecastSensor(IQVIAEntity): class IndexSensor(IQVIAEntity): """Define sensor related to indices.""" - async def async_update(self): + @callback + def update_from_latest_data(self): """Update the sensor.""" if not self._iqvia.data: - _LOGGER.warning( - "IQVIA didn't return data for %s; trying again later", self.name - ) return try: @@ -147,9 +149,6 @@ class IndexSensor(IQVIAEntity): elif self._type == TYPE_DISEASE_TODAY: data = self._iqvia.data[TYPE_DISEASE_INDEX].get("Location") except KeyError: - _LOGGER.warning( - "IQVIA didn't return data for %s; trying again later", self.name - ) return key = self._type.split("_")[-1].title() @@ -157,9 +156,6 @@ class IndexSensor(IQVIAEntity): try: [period] = [p for p in data["periods"] if p["Type"] == key] except ValueError: - _LOGGER.warning( - "IQVIA didn't return data for %s; trying again later", self.name - ) return [rating] = [ From 86d48c608ee4be31662cf6b3390ffe343d413180 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Tue, 17 Mar 2020 08:09:19 +0100 Subject: [PATCH 085/431] Deduplicate MQTT tests (#32874) --- tests/components/mqtt/common.py | 231 +++++++++++++++++- .../mqtt/test_alarm_control_panel.py | 152 ++++-------- tests/components/mqtt/test_binary_sensor.py | 161 +++--------- tests/components/mqtt/test_camera.py | 229 +++++------------ tests/components/mqtt/test_climate.py | 135 +++++----- tests/components/mqtt/test_cover.py | 175 +++---------- tests/components/mqtt/test_device_trigger.py | 36 ++- tests/components/mqtt/test_fan.py | 183 ++++---------- tests/components/mqtt/test_legacy_vacuum.py | 159 ++++-------- tests/components/mqtt/test_light.py | 164 ++++--------- tests/components/mqtt/test_light_json.py | 157 ++++-------- tests/components/mqtt/test_light_template.py | 172 ++++--------- tests/components/mqtt/test_lock.py | 168 ++++--------- tests/components/mqtt/test_sensor.py | 175 +++---------- tests/components/mqtt/test_state_vacuum.py | 148 ++++------- tests/components/mqtt/test_switch.py | 210 +++++----------- 16 files changed, 908 insertions(+), 1747 deletions(-) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/common.py index a29891d0b36..702a38928a2 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/common.py @@ -1,9 +1,11 @@ """Common test objects.""" +import copy import json from unittest.mock import ANY from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE from tests.common import ( MockConfigEntry, @@ -13,6 +15,123 @@ from tests.common import ( mock_registry, ) +DEFAULT_CONFIG_DEVICE_INFO_ID = { + "identifiers": ["helloworld"], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", +} + +DEFAULT_CONFIG_DEVICE_INFO_MAC = { + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", +} + + +async def help_test_availability_without_topic(hass, mqtt_mock, domain, config): + """Test availability without defined availability topic.""" + assert "availability_topic" not in config[domain] + assert await async_setup_component(hass, domain, config) + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + +async def help_test_default_availability_payload( + hass, + mqtt_mock, + domain, + config, + no_assumed_state=False, + state_topic=None, + state_message=None, +): + """Test availability by default payload with defined topic. + + This is a test helper for the MqttAvailability mixin. + """ + # Add availability settings to config + config = copy.deepcopy(config) + config[domain]["availability_topic"] = "availability-topic" + assert await async_setup_component(hass, domain, config,) + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic", "offline") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + if state_topic: + async_fire_mqtt_message(hass, state_topic, state_message) + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "online") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + +async def help_test_custom_availability_payload( + hass, + mqtt_mock, + domain, + config, + no_assumed_state=False, + state_topic=None, + state_message=None, +): + """Test availability by custom payload with defined topic. + + This is a test helper for the MqttAvailability mixin. + """ + # Add availability settings to config + config = copy.deepcopy(config) + config[domain]["availability_topic"] = "availability-topic" + config[domain]["payload_available"] = "good" + config[domain]["payload_not_available"] = "nogood" + assert await async_setup_component(hass, domain, config,) + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "good") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + if no_assumed_state: + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "availability-topic", "nogood") + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + if state_topic: + async_fire_mqtt_message(hass, state_topic, state_message) + + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, "availability-topic", "good") + + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + async def help_test_setting_attribute_via_mqtt_json_message( hass, mqtt_mock, domain, config @@ -21,6 +140,9 @@ async def help_test_setting_attribute_via_mqtt_json_message( This is a test helper for the MqttAttributes mixin. """ + # Add JSON attributes settings to config + config = copy.deepcopy(config) + config[domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component(hass, domain, config,) async_fire_mqtt_message(hass, "attr-topic", '{ "val": "100" }') @@ -29,6 +151,26 @@ async def help_test_setting_attribute_via_mqtt_json_message( assert state.attributes.get("val") == "100" +async def help_test_setting_attribute_with_template(hass, mqtt_mock, domain, config): + """Test the setting of attribute via MQTT with JSON payload. + + This is a test helper for the MqttAttributes mixin. + """ + # Add JSON attributes settings to config + config = copy.deepcopy(config) + config[domain]["json_attributes_topic"] = "attr-topic" + config[domain]["json_attributes_template"] = "{{ value_json['Timer1'] | tojson }}" + assert await async_setup_component(hass, domain, config,) + + async_fire_mqtt_message( + hass, "attr-topic", json.dumps({"Timer1": {"Arm": 0, "Time": "22:18"}}) + ) + state = hass.states.get(f"{domain}.test") + + assert state.attributes.get("Arm") == 0 + assert state.attributes.get("Time") == "22:18" + + async def help_test_update_with_json_attrs_not_dict( hass, mqtt_mock, caplog, domain, config ): @@ -36,6 +178,9 @@ async def help_test_update_with_json_attrs_not_dict( This is a test helper for the MqttAttributes mixin. """ + # Add JSON attributes settings to config + config = copy.deepcopy(config) + config[domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component(hass, domain, config,) async_fire_mqtt_message(hass, "attr-topic", '[ "list", "of", "things"]') @@ -52,6 +197,9 @@ async def help_test_update_with_json_attrs_bad_JSON( This is a test helper for the MqttAttributes mixin. """ + # Add JSON attributes settings to config + config = copy.deepcopy(config) + config[domain]["json_attributes_topic"] = "attr-topic" assert await async_setup_component(hass, domain, config,) async_fire_mqtt_message(hass, "attr-topic", "This is not JSON") @@ -61,13 +209,19 @@ async def help_test_update_with_json_attrs_bad_JSON( assert "Erroneous JSON: This is not JSON" in caplog.text -async def help_test_discovery_update_attr( - hass, mqtt_mock, caplog, domain, data1, data2 -): +async def help_test_discovery_update_attr(hass, mqtt_mock, caplog, domain, config): """Test update of discovered MQTTAttributes. This is a test helper for the MqttAttributes mixin. """ + # Add JSON attributes settings to config + config1 = copy.deepcopy(config) + config1[domain]["json_attributes_topic"] = "attr-topic1" + config2 = copy.deepcopy(config) + config2[domain]["json_attributes_topic"] = "attr-topic2" + data1 = json.dumps(config1[domain]) + data2 = json.dumps(config2[domain]) + entry = MockConfigEntry(domain=mqtt.DOMAIN) await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data1) @@ -173,6 +327,11 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, This is a test helper for the MqttDiscoveryUpdate mixin. """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + entry = MockConfigEntry(domain=mqtt.DOMAIN) entry.add_to_hass(hass) await async_start(hass, "homeassistant", {}, entry) @@ -185,6 +344,33 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, device = registry.async_get_device({("mqtt", "helloworld")}, set()) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} + assert device.manufacturer == "Whatever" + assert device.name == "Beer" + assert device.model == "Glass" + assert device.sw_version == "0.1-beta" + + +async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, config): + """Test device registry integration. + + This is a test helper for the MqttDiscoveryUpdate mixin. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_MAC) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + assert device is not None assert device.connections == {("mac", "02:5b:26:a8:dc:12")} assert device.manufacturer == "Whatever" assert device.name == "Beer" @@ -194,6 +380,11 @@ async def help_test_entity_device_info_with_identifier(hass, mqtt_mock, domain, async def help_test_entity_device_info_remove(hass, mqtt_mock, domain, config): """Test device registry remove.""" + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + entry = MockConfigEntry(domain=mqtt.DOMAIN) entry.add_to_hass(hass) await async_start(hass, "homeassistant", {}, entry) @@ -221,6 +412,11 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): This is a test helper for the MqttDiscoveryUpdate mixin. """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + entry = MockConfigEntry(domain=mqtt.DOMAIN) entry.add_to_hass(hass) await async_start(hass, "homeassistant", {}, entry) @@ -244,27 +440,36 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): assert device.name == "Milk" -async def help_test_entity_id_update(hass, mqtt_mock, domain, config): +async def help_test_entity_id_update(hass, mqtt_mock, domain, config, topics=None): """Test MQTT subscriptions are managed when entity_id is updated.""" + # Add unique_id to config + config = copy.deepcopy(config) + config[domain]["unique_id"] = "TOTALLY_UNIQUE" + + if topics is None: + # Add default topics to config + config[domain]["availability_topic"] = "avty-topic" + config[domain]["state_topic"] = "test-topic" + topics = ["avty-topic", "test-topic"] + assert len(topics) > 0 registry = mock_registry(hass, {}) mock_mqtt = await async_mock_mqtt_component(hass) assert await async_setup_component(hass, domain, config,) - state = hass.states.get(f"{domain}.beer") + state = hass.states.get(f"{domain}.test") assert state is not None - assert mock_mqtt.async_subscribe.call_count == 2 - mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, "utf-8") - mock_mqtt.async_subscribe.assert_any_call("avty-topic", ANY, 0, "utf-8") + assert mock_mqtt.async_subscribe.call_count == len(topics) + for topic in topics: + mock_mqtt.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) mock_mqtt.async_subscribe.reset_mock() - registry.async_update_entity(f"{domain}.beer", new_entity_id=f"{domain}.milk") + registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") await hass.async_block_till_done() - state = hass.states.get(f"{domain}.beer") + state = hass.states.get(f"{domain}.test") assert state is None state = hass.states.get(f"{domain}.milk") assert state is not None - assert mock_mqtt.async_subscribe.call_count == 2 - mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, "utf-8") - mock_mqtt.async_subscribe.assert_any_call("avty-topic", ANY, 0, "utf-8") + for topic in topics: + mock_mqtt.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index ffc1755afda..93036335e16 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -10,20 +10,24 @@ from homeassistant.const import ( STATE_ALARM_DISARMED, STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, - STATE_UNAVAILABLE, STATE_UNKNOWN, ) from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -47,16 +51,6 @@ DEFAULT_CONFIG = { } } -DEFAULT_CONFIG_ATTR = { - alarm_control_panel.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "alarm/state", - "command_topic": "alarm/command", - "json_attributes_topic": "attr-topic", - } -} - DEFAULT_CONFIG_CODE = { alarm_control_panel.DOMAIN: { "platform": "mqtt", @@ -68,22 +62,6 @@ DEFAULT_CONFIG_CODE = { } } -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} - async def test_fail_setup_without_state_topic(hass, mqtt_mock): """Test for failing with no state topic.""" @@ -331,48 +309,6 @@ async def test_disarm_not_publishes_mqtt_with_invalid_code_when_req(hass, mqtt_m assert mqtt_mock.async_publish.call_count == call_count -async def test_default_availability_payload(hass, mqtt_mock): - """Test availability by default payload with defined topic.""" - config = copy.deepcopy(DEFAULT_CONFIG_CODE) - config[alarm_control_panel.DOMAIN]["availability_topic"] = "availability-topic" - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) - - state = hass.states.get("alarm_control_panel.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("alarm_control_panel.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("alarm_control_panel.test") - assert state.state == STATE_UNAVAILABLE - - -async def test_custom_availability_payload(hass, mqtt_mock): - """Test availability by custom payload with defined topic.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config[alarm_control_panel.DOMAIN]["availability_topic"] = "availability-topic" - config[alarm_control_panel.DOMAIN]["payload_available"] = "good" - config[alarm_control_panel.DOMAIN]["payload_not_available"] = "nogood" - assert await async_setup_component(hass, alarm_control_panel.DOMAIN, config,) - - state = hass.states.get("alarm_control_panel.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("alarm_control_panel.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("alarm_control_panel.test") - assert state.state == STATE_UNAVAILABLE - - async def test_update_state_via_state_topic_template(hass, mqtt_mock): """Test updating with template_value via state topic.""" assert await async_setup_component( @@ -403,38 +339,59 @@ async def test_update_state_via_state_topic_template(hass, mqtt_mock): assert state.state == STATE_ALARM_ARMED_AWAY +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_CODE + ) + + 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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + ) + + +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, alarm_control_panel.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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, alarm_control_panel.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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[alarm_control_panel.DOMAIN]) - config1["json_attributes_topic"] = "attr-topic1" - config2["json_attributes_topic"] = "attr-topic2" - data1 = json.dumps(config1) - data2 = json.dumps(config2) - await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) @@ -496,49 +453,36 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT alarm control panel device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT alarm control panel device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, alarm_control_panel.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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } await help_test_entity_device_info_remove( - hass, mqtt_mock, alarm_control_panel.DOMAIN, config + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - alarm_control_panel.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } await help_test_entity_id_update( - hass, mqtt_mock, alarm_control_panel.DOMAIN, config + hass, mqtt_mock, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 9a20b9a3282..7b104089073 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -16,15 +16,20 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -40,30 +45,6 @@ DEFAULT_CONFIG = { } } -DEFAULT_CONFIG_ATTR = { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} - async def test_setting_sensor_value_expires_availability_topic(hass, mqtt_mock, caplog): """Test the expiration of the value.""" @@ -265,81 +246,24 @@ async def test_invalid_device_class(hass, mqtt_mock): async def test_availability_without_topic(hass, mqtt_mock): """Test availability without defined availability topic.""" - assert await async_setup_component( - hass, - binary_sensor.DOMAIN, - { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - } - }, + await help_test_availability_without_topic( + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("binary_sensor.test") - assert state.state != STATE_UNAVAILABLE - -async def test_availability_by_defaults(hass, mqtt_mock): - """Test availability by defaults with defined topic.""" - assert await async_setup_component( - hass, - binary_sensor.DOMAIN, - { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "availability_topic": "availability-topic", - } - }, +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("binary_sensor.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_UNAVAILABLE - - -async def test_availability_by_custom_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - assert await async_setup_component( - hass, - binary_sensor.DOMAIN, - { - binary_sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("binary_sensor.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("binary_sensor.test") - assert state.state == STATE_UNAVAILABLE - async def test_force_update_disabled(hass, mqtt_mock): """Test force update option.""" @@ -459,35 +383,35 @@ async def test_off_delay(hass, mqtt_mock): 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, binary_sensor.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + ) + + +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, binary_sensor.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, binary_sensor.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, binary_sensor.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, binary_sensor.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - config1 = copy.deepcopy(DEFAULT_CONFIG_ATTR[binary_sensor.DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG_ATTR[binary_sensor.DOMAIN]) - config1["json_attributes_topic"] = "attr-topic1" - config2["json_attributes_topic"] = "attr-topic2" - data1 = json.dumps(config1) - data2 = json.dumps(config2) - await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, binary_sensor.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, binary_sensor.DOMAIN, DEFAULT_CONFIG ) @@ -543,45 +467,36 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT binary sensor device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT binary sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, binary_sensor.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, binary_sensor.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } await help_test_entity_device_info_remove( - hass, mqtt_mock, binary_sensor.DOMAIN, config + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - binary_sensor.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, binary_sensor.DOMAIN, config) + await help_test_entity_id_update( + hass, mqtt_mock, binary_sensor.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 0e7d8ada759..f77e5945ae5 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -1,18 +1,32 @@ """The tests for mqtt camera component.""" import json -from unittest.mock import ANY from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component +from .common import ( + help_test_discovery_broken, + help_test_discovery_removal, + help_test_discovery_update, + 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, + help_test_unique_id, +) + from tests.common import ( MockConfigEntry, async_fire_mqtt_message, async_mock_mqtt_component, - mock_registry, ) +DEFAULT_CONFIG = { + camera.DOMAIN: {"platform": "mqtt", "name": "test", "topic": "test_topic"} +} + async def test_run_camera_setup(hass, aiohttp_client): """Test that it fetches the given payload.""" @@ -37,51 +51,29 @@ async def test_run_camera_setup(hass, aiohttp_client): async def test_unique_id(hass): """Test unique id option only creates one camera per unique_id.""" - await async_mock_mqtt_component(hass) - await async_setup_component( - hass, - "camera", - { - "camera": [ - { - "platform": "mqtt", - "name": "Test Camera 1", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - { - "platform": "mqtt", - "name": "Test Camera 2", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - }, - ] - }, - ) - - async_fire_mqtt_message(hass, "test-topic", "payload") - assert len(hass.states.async_all()) == 1 + config = { + camera.DOMAIN: [ + { + "platform": "mqtt", + "name": "Test 1", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + { + "platform": "mqtt", + "name": "Test 2", + "topic": "test-topic", + "unique_id": "TOTALLY_UNIQUE", + }, + ] + } + await help_test_unique_id(hass, camera.DOMAIN, config) async def test_discovery_removal_camera(hass, mqtt_mock, caplog): """Test removal of discovered camera.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - await async_start(hass, "homeassistant", {}, entry) - - data = '{ "name": "Beer",' ' "topic": "test_topic"}' - - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) - await hass.async_block_till_done() - - state = hass.states.get("camera.beer") - assert state is not None - assert state.name == "Beer" - - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", "") - await hass.async_block_till_done() - - state = hass.states.get("camera.beer") - assert state is None + data = json.dumps(DEFAULT_CONFIG[camera.DOMAIN]) + await help_test_discovery_removal(hass, mqtt_mock, caplog, camera.DOMAIN, data) async def test_discovery_update_camera(hass, mqtt_mock, caplog): @@ -92,21 +84,9 @@ async def test_discovery_update_camera(hass, mqtt_mock, caplog): data1 = '{ "name": "Beer",' ' "topic": "test_topic"}' data2 = '{ "name": "Milk",' ' "topic": "test_topic"}' - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data1) - await hass.async_block_till_done() - - state = hass.states.get("camera.beer") - assert state is not None - assert state.name == "Beer" - - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data2) - await hass.async_block_till_done() - - state = hass.states.get("camera.beer") - assert state is not None - assert state.name == "Milk" - state = hass.states.get("camera.milk") - assert state is None + await help_test_discovery_update( + hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 + ) async def test_discovery_broken(hass, mqtt_mock, caplog): @@ -117,130 +97,41 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): data1 = '{ "name": "Beer" }' data2 = '{ "name": "Milk",' ' "topic": "test_topic"}' - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data1) - await hass.async_block_till_done() - - state = hass.states.get("camera.beer") - assert state is None - - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data2) - await hass.async_block_till_done() - - state = hass.states.get("camera.milk") - assert state is not None - assert state.name == "Milk" - state = hass.states.get("camera.beer") - assert state is None - - -async def test_entity_id_update(hass, mqtt_mock): - """Test MQTT subscriptions are managed when entity_id is updated.""" - registry = mock_registry(hass, {}) - mock_mqtt = await async_mock_mqtt_component(hass) - assert await async_setup_component( - hass, - camera.DOMAIN, - { - camera.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "topic": "test-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - }, + await help_test_discovery_broken( + hass, mqtt_mock, caplog, camera.DOMAIN, data1, data2 ) - state = hass.states.get("camera.beer") - assert state is not None - assert mock_mqtt.async_subscribe.call_count == 1 - mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, None) - mock_mqtt.async_subscribe.reset_mock() - registry.async_update_entity("camera.beer", new_entity_id="camera.milk") - await hass.async_block_till_done() - - state = hass.states.get("camera.beer") - assert state is None - - state = hass.states.get("camera.milk") - assert state is not None - assert mock_mqtt.async_subscribe.call_count == 1 - mock_mqtt.async_subscribe.assert_any_call("test-topic", ANY, 0, None) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT camera device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT camera device registry integration.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - entry.add_to_hass(hass) - await async_start(hass, "homeassistant", {}, entry) - registry = await hass.helpers.device_registry.async_get_registry() - - data = json.dumps( - { - "platform": "mqtt", - "name": "Test 1", - "topic": "test-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", - } + await help_test_entity_device_info_with_identifier( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG ) - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) - await hass.async_block_till_done() - - device = registry.async_get_device({("mqtt", "helloworld")}, set()) - assert device is not None - assert device.identifiers == {("mqtt", "helloworld")} - assert device.connections == {("mac", "02:5b:26:a8:dc:12")} - assert device.manufacturer == "Whatever" - assert device.name == "Beer" - assert device.model == "Glass" - assert device.sw_version == "0.1-beta" async def test_entity_device_info_update(hass, mqtt_mock): """Test device registry update.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - entry.add_to_hass(hass) - await async_start(hass, "homeassistant", {}, entry) - registry = await hass.helpers.device_registry.async_get_registry() + await help_test_entity_device_info_update( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + ) - config = { - "platform": "mqtt", - "name": "Test 1", - "topic": "test-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", - } - data = json.dumps(config) - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) - await hass.async_block_till_done() +async def test_entity_device_info_remove(hass, mqtt_mock): + """Test device registry remove.""" + await help_test_entity_device_info_remove( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + ) - device = registry.async_get_device({("mqtt", "helloworld")}, set()) - assert device is not None - assert device.name == "Beer" - config["device"]["name"] = "Milk" - data = json.dumps(config) - async_fire_mqtt_message(hass, "homeassistant/camera/bla/config", data) - await hass.async_block_till_done() - - device = registry.async_get_device({("mqtt", "helloworld")}, set()) - assert device is not None - assert device.name == "Milk" +async def test_entity_id_update(hass, mqtt_mock): + """Test MQTT subscriptions are managed when entity_id is updated.""" + await help_test_entity_id_update( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 481b43002a0..29f97af7725 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -23,18 +23,23 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import STATE_OFF, STATE_UNAVAILABLE +from homeassistant.const import STATE_OFF from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -46,7 +51,7 @@ from tests.components.climate import common ENTITY_CLIMATE = "climate.test" DEFAULT_CONFIG = { - "climate": { + CLIMATE_DOMAIN: { "platform": "mqtt", "name": "test", "mode_command_topic": "mode-topic", @@ -61,32 +66,6 @@ DEFAULT_CONFIG = { } } -DEFAULT_CONFIG_ATTR = { - CLIMATE_DOMAIN: { - "platform": "mqtt", - "name": "test", - "power_state_topic": "test-topic", - "power_command_topic": "test_topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "power_state_topic": "test-topic", - "power_command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} - async def test_setup_params(hass, mqtt_mock): """Test the initial parameters.""" @@ -597,27 +576,25 @@ async def test_set_aux(hass, mqtt_mock): assert state.attributes.get("aux_heat") == "off" +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) + + async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - config = copy.deepcopy(DEFAULT_CONFIG) - config["climate"]["availability_topic"] = "availability-topic" - config["climate"]["payload_available"] = "good" - config["climate"]["payload_not_available"] = "nogood" - - assert await async_setup_component(hass, CLIMATE_DOMAIN, config) - - state = hass.states.get("climate.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("climate.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("climate.test") - assert state.state == STATE_UNAVAILABLE + await help_test_custom_availability_payload( + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) async def test_set_target_temperature_low_high_with_templates(hass, mqtt_mock, caplog): @@ -801,35 +778,35 @@ async def test_temp_step_custom(hass, mqtt_mock): 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, CLIMATE_DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) + + +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, CLIMATE_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, CLIMATE_DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, CLIMATE_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, CLIMATE_DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - config1 = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - config2 = copy.deepcopy(DEFAULT_CONFIG[CLIMATE_DOMAIN]) - config1["json_attributes_topic"] = "attr-topic1" - config2["json_attributes_topic"] = "attr-topic2" - data1 = json.dumps(config1) - data2 = json.dumps(config2) - await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, CLIMATE_DOMAIN, data1, data2 + hass, mqtt_mock, caplog, CLIMATE_DOMAIN, DEFAULT_CONFIG ) @@ -880,47 +857,47 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT climate device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT climate device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, CLIMATE_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, CLIMATE_DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "power_state_topic": "test-topic", - "power_command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, CLIMATE_DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - CLIMATE_DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "mode_state_topic": "test-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] + CLIMATE_DOMAIN: { + "platform": "mqtt", + "name": "test", + "mode_state_topic": "test-topic", + "availability_topic": "avty-topic", + } } - await help_test_entity_id_update(hass, mqtt_mock, CLIMATE_DOMAIN, config) + await help_test_entity_id_update( + hass, mqtt_mock, CLIMATE_DOMAIN, config, ["test-topic", "avty-topic"] + ) async def test_precision_default(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index d9b49a1fde6..2e5e232cdd5 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -18,21 +18,25 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPEN, STATE_OPENING, - STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -40,29 +44,8 @@ from .common import ( from tests.common import async_fire_mqtt_message -DEFAULT_CONFIG_ATTR = { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", +DEFAULT_CONFIG = { + cover.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} } @@ -1601,88 +1584,24 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): async def test_availability_without_topic(hass, mqtt_mock): """Test availability without defined availability topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - } - }, + await help_test_availability_without_topic( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("cover.test") - assert state.state != STATE_UNAVAILABLE - -async def test_availability_by_defaults(hass, mqtt_mock): - """Test availability by defaults with defined topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "availability_topic": "availability-topic", - } - }, +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("cover.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - await hass.async_block_till_done() - - state = hass.states.get("cover.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - await hass.async_block_till_done() - - state = hass.states.get("cover.test") - assert state.state == STATE_UNAVAILABLE - - -async def test_availability_by_custom_payload(hass, mqtt_mock): +async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("cover.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - await hass.async_block_till_done() - - state = hass.states.get("cover.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - await hass.async_block_till_done() - - state = hass.states.get("cover.test") - assert state.state == STATE_UNAVAILABLE - async def test_valid_device_class(hass, mqtt_mock): """Test the setting of a valid sensor class.""" @@ -1725,39 +1644,35 @@ async def test_invalid_device_class(hass, mqtt_mock): 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, cover.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + ) + + +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, cover.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, cover.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, cover.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, cover.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) - await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, cover.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, cover.DOMAIN, DEFAULT_CONFIG ) @@ -1806,44 +1721,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT cover device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT cover device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, cover.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, cover.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, cover.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - cover.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, cover.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index ebdbabae83b..6766002717d 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -757,6 +757,40 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock): assert len(calls) == 0 +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT device registry integration.""" + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps( + { + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": { + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + } + ) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + assert device is not None + assert device.connections == {("mac", "02:5b:26:a8:dc:12")} + assert device.manufacturer == "Whatever" + assert device.name == "Beer" + assert device.model == "Glass" + assert device.sw_version == "0.1-beta" + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT device registry integration.""" entry = MockConfigEntry(domain=DOMAIN) @@ -772,7 +806,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): "subtype": "bar", "device": { "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", @@ -786,7 +819,6 @@ async def test_entity_device_info_with_identifier(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}, set()) assert device is not None assert device.identifiers == {("mqtt", "helloworld")} - assert device.connections == {("mac", "02:5b:26:a8:dc:12")} assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 37c48fbcc93..0ecc6a25d6f 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -1,23 +1,23 @@ """Test MQTT fans.""" from homeassistant.components import fan -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -26,31 +26,15 @@ from .common import ( from tests.common import async_fire_mqtt_message from tests.components.fan import common -DEFAULT_CONFIG_ATTR = { +DEFAULT_CONFIG = { fan.DOMAIN: { "platform": "mqtt", "name": "test", - "command_topic": "test-topic", - "json_attributes_topic": "attr-topic", + "state_topic": "state-topic", + "command_topic": "command-topic", } } -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} - async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): """Test if command fails with command topic.""" @@ -385,125 +369,59 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_default_availability_payload(hass, mqtt_mock): - """Test the availability payload.""" - assert await async_setup_component( - hass, - fan.DOMAIN, - { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "availability_topic": "availability_topic", - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("fan.test") - assert state.state is STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability_topic", "online") - - state = hass.states.get("fan.test") - assert state.state is not STATE_UNAVAILABLE - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "availability_topic", "offline") - - state = hass.states.get("fan.test") - assert state.state is STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "state-topic", "1") - - state = hass.states.get("fan.test") - assert state.state is STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability_topic", "online") - - state = hass.states.get("fan.test") - assert state.state is not STATE_UNAVAILABLE +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" + ) async def test_custom_availability_payload(hass, mqtt_mock): - """Test the availability payload.""" - assert await async_setup_component( - hass, - fan.DOMAIN, - { - fan.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "availability_topic": "availability_topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG, True, "state-topic", "1" ) - state = hass.states.get("fan.test") - assert state.state is STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability_topic", "good") - - state = hass.states.get("fan.test") - assert state.state is not STATE_UNAVAILABLE - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "availability_topic", "nogood") - - state = hass.states.get("fan.test") - assert state.state is STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "state-topic", "1") - - state = hass.states.get("fan.test") - assert state.state is STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability_topic", "good") - - state = hass.states.get("fan.test") - assert state.state is not STATE_UNAVAILABLE - 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, fan.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + ) + + +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, fan.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, fan.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, fan.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, fan.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, fan.DOMAIN, DEFAULT_CONFIG ) @@ -550,45 +468,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): await help_test_discovery_broken(hass, mqtt_mock, caplog, fan.DOMAIN, data1, data2) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT fan device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT fan device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, fan.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, fan.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - fan.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, fan.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 86c111bf0cd..1bbefa35478 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -16,25 +16,24 @@ from homeassistant.components.vacuum import ( ATTR_FAN_SPEED, ATTR_STATUS, ) -from homeassistant.const import ( - CONF_NAME, - CONF_PLATFORM, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -64,28 +63,7 @@ DEFAULT_CONFIG = { mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], } -DEFAULT_CONFIG_ATTR = { - vacuum.DOMAIN: { - "platform": "mqtt", - "name": "test", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} +DEFAULT_CONFIG_2 = {vacuum.DOMAIN: {"platform": "mqtt", "name": "test"}} async def test_default_supported_features(hass, mqtt_mock): @@ -499,89 +477,59 @@ async def test_missing_fan_speed_template(hass, mqtt_mock): assert state is None +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + async def test_default_availability_payload(hass, mqtt_mock): """Test availability by default payload with defined topic.""" - config = deepcopy(DEFAULT_CONFIG) - config.update({"availability_topic": "availability-topic"}) - - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("vacuum.mqtttest") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE + await help_test_default_availability_payload( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - config = deepcopy(DEFAULT_CONFIG) - config.update( - { - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } + await help_test_custom_availability_payload( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("vacuum.mqtttest") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE - 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, vacuum.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + +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, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -608,7 +556,7 @@ async def test_unique_id(hass, mqtt_mock): async def test_discovery_removal_vacuum(hass, mqtt_mock, caplog): """Test removal of discovered vacuum.""" - data = json.dumps(DEFAULT_CONFIG_ATTR[vacuum.DOMAIN]) + data = json.dumps(DEFAULT_CONFIG_2[vacuum.DOMAIN]) await help_test_discovery_removal(hass, mqtt_mock, caplog, vacuum.DOMAIN, data) @@ -630,45 +578,46 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT vacuum device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_update(hass, mqtt_mock): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { - vacuum.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "battery_level_topic": "test-topic", - "battery_level_template": "{{ value_json.battery_level }}", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] + vacuum.DOMAIN: { + "platform": "mqtt", + "name": "test", + "battery_level_topic": "test-topic", + "battery_level_template": "{{ value_json.battery_level }}", + "command_topic": "command-topic", + "availability_topic": "avty-topic", + } } - await help_test_entity_id_update(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_entity_id_update( + hass, mqtt_mock, vacuum.DOMAIN, config, ["test-topic", "avty-topic"] + ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 1296915039a..895995e06d9 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -158,25 +158,25 @@ from unittest.mock import patch from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -190,29 +190,8 @@ from tests.common import ( ) from tests.components.light import common -DEFAULT_CONFIG_ATTR = { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", +DEFAULT_CONFIG = { + light.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} } @@ -1034,105 +1013,59 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) -async def test_default_availability_payload(hass, mqtt_mock): - """Test availability by default payload with defined topic.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light/set", - "brightness_command_topic": "test_light/bright", - "rgb_command_topic": "test_light/rgb", - "availability_topic": "availability-topic", - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("light.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test_light/set", - "brightness_command_topic": "test_light/bright", - "rgb_command_topic": "test_light/rgb", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("light.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - 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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) + + +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, light.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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, light.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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG ) @@ -1213,45 +1146,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT light device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, light.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, light.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 860da1e1f30..412c757e059 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -98,21 +98,25 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON, - STATE_UNAVAILABLE, ) import homeassistant.core as ha from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -121,33 +125,15 @@ from .common import ( from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_coro from tests.components.light import common -DEFAULT_CONFIG_ATTR = { +DEFAULT_CONFIG = { light.DOMAIN: { "platform": "mqtt", "schema": "json", "name": "test", "command_topic": "test-topic", - "json_attributes_topic": "attr-topic", } } -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} - class JsonValidator(object): """Helper to compare JSON.""" @@ -883,107 +869,59 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): assert state.attributes.get("white_value") == 255 -async def test_default_availability_payload(hass, mqtt_mock): - """Test availability by default payload with defined topic.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "json", - "name": "test", - "state_topic": "test_light_rgb", - "command_topic": "test_light_rgb/set", - "availability_topic": "availability-topic", - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("light.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "json", - "name": "test", - "state_topic": "test_light_rgb", - "command_topic": "test_light_rgb/set", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("light.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - 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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) + + +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, light.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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, light.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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "schema": "json",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "schema": "json",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG ) @@ -1067,47 +1005,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT light device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, light.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, light.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "schema": "json", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index f7e4e10bf04..dac15e5ef53 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -30,25 +30,25 @@ from unittest.mock import patch from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -61,7 +61,7 @@ from tests.common import ( mock_coro, ) -DEFAULT_CONFIG_ATTR = { +DEFAULT_CONFIG = { light.DOMAIN: { "platform": "mqtt", "schema": "template", @@ -69,29 +69,9 @@ DEFAULT_CONFIG_ATTR = { "command_topic": "test-topic", "command_on_template": "on,{{ transition }}", "command_off_template": "off,{{ transition|d }}", - "json_attributes_topic": "attr-topic", } } -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "schema": "template", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", -} - async def test_setup_fails(hass, mqtt_mock): """Test that setup fails with missing required configuration items.""" @@ -485,113 +465,59 @@ async def test_invalid_values(hass, mqtt_mock): assert state.attributes.get("effect") == "rainbow" -async def test_default_availability_payload(hass, mqtt_mock): - """Test availability by default payload with defined topic.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_light_rgb/set", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", - "availability_topic": "availability-topic", - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("light.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - assert await async_setup_component( - hass, - light.DOMAIN, - { - light.DOMAIN: { - "platform": "mqtt", - "schema": "template", - "name": "test", - "command_topic": "test_light_rgb/set", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("light.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("light.test") - assert state.state == STATE_UNAVAILABLE - 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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) + + +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, light.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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, light.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, light.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "schema": "template",' - ' "command_topic": "test_topic",' - ' "command_on_template": "on",' - ' "command_off_template": "off",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "schema": "template",' - ' "command_topic": "test_topic",' - ' "command_on_template": "on",' - ' "command_off_template": "off",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, light.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, light.DOMAIN, DEFAULT_CONFIG ) @@ -691,48 +617,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT light device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT light device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, light.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, light.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - light.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "schema": "template", - "state_topic": "test-topic", - "command_topic": "command-topic", - "command_on_template": "on,{{ transition }}", - "command_off_template": "off,{{ transition|d }}", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index f4b7431b0ae..4c34db6ea20 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -1,23 +1,23 @@ """The tests for the MQTT lock platform.""" from homeassistant.components import lock -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - STATE_LOCKED, - STATE_UNAVAILABLE, - STATE_UNLOCKED, -) +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -26,29 +26,8 @@ from .common import ( from tests.common import async_fire_mqtt_message from tests.components.lock import common -DEFAULT_CONFIG_ATTR = { - lock.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", +DEFAULT_CONFIG = { + lock.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} } @@ -270,109 +249,59 @@ async def test_sending_mqtt_commands_and_explicit_optimistic(hass, mqtt_mock): assert state.attributes.get(ATTR_ASSUMED_STATE) -async def test_default_availability_payload(hass, mqtt_mock): - """Test availability by default payload with defined 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", - "availability_topic": "availability-topic", - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("lock.test") - assert state.state is STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("lock.test") - assert state.state is not STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("lock.test") - assert state.state is STATE_UNAVAILABLE +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined 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", - "state_locked": "LOCKED", - "state_unlocked": "UNLOCKED", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("lock.test") - assert state.state is STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("lock.test") - assert state.state is not STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("lock.test") - assert state.state is STATE_UNAVAILABLE - 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, lock.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG + ) + + +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, lock.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, lock.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, lock.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, lock.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, lock.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, lock.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, lock.DOMAIN, DEFAULT_CONFIG ) @@ -429,45 +358,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): await help_test_discovery_broken(hass, mqtt_mock, caplog, lock.DOMAIN, data1, data2) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT lock device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT lock device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, lock.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, lock.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, lock.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - lock.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, lock.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 2666b3bbdb0..5e472efcc89 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -6,21 +6,26 @@ from unittest.mock import patch from homeassistant.components import mqtt from homeassistant.components.mqtt.discovery import async_start import homeassistant.components.sensor as sensor -from homeassistant.const import EVENT_STATE_CHANGED, STATE_UNAVAILABLE +from homeassistant.const import EVENT_STATE_CHANGED import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -32,28 +37,8 @@ from tests.common import ( async_fire_time_changed, ) -DEFAULT_CONFIG_ATTR = { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", +DEFAULT_CONFIG = { + sensor.DOMAIN: {"platform": "mqtt", "name": "test", "state_topic": "test-topic"} } @@ -234,65 +219,26 @@ async def test_force_update_enabled(hass, mqtt_mock): assert len(events) == 2 -async def test_default_availability_payload(hass, mqtt_mock): - """Test availability by default payload with defined topic.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "availability_topic": "availability-topic", - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("sensor.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("sensor.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("sensor.test") - assert state.state == STATE_UNAVAILABLE +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_custom_availability_payload( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("sensor.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("sensor.test") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("sensor.test") - assert state.state == STATE_UNAVAILABLE - async def test_setting_sensor_attribute_via_legacy_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" @@ -432,72 +378,36 @@ async def test_valid_device_class(hass, mqtt_mock): async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" - config = { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } - } await help_test_setting_attribute_via_mqtt_json_message( - hass, mqtt_mock, sensor.DOMAIN, config + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_setting_attribute_with_template(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "json_attributes_topic": "attr-topic", - "json_attributes_template": "{{ value_json['Timer1'] | tojson }}", - } - }, + await help_test_setting_attribute_with_template( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) - async_fire_mqtt_message( - hass, "attr-topic", json.dumps({"Timer1": {"Arm": 0, "Time": "22:18"}}) - ) - state = hass.states.get("sensor.test") - - assert state.attributes.get("Arm") == 0 - assert state.attributes.get("Time") == "22:18" - 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, sensor.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "state_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "state_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, sensor.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, sensor.DOMAIN, DEFAULT_CONFIG ) @@ -546,46 +456,37 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT sensor device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT sensor device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, sensor.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - sensor.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, sensor.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) async def test_entity_device_info_with_hub(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 6aa61fdc7ef..1b1150985a2 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -26,21 +26,25 @@ from homeassistant.const import ( CONF_NAME, CONF_PLATFORM, ENTITY_MATCH_ALL, - STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -64,29 +68,8 @@ DEFAULT_CONFIG = { mqttvacuum.CONF_FAN_SPEED_LIST: ["min", "medium", "high", "max"], } -DEFAULT_CONFIG_ATTR = { - vacuum.DOMAIN: { - "platform": "mqtt", - "schema": "state", - "name": "test", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "schema": "state", - "name": "Test 1", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", +DEFAULT_CONFIG_2 = { + vacuum.DOMAIN: {"platform": "mqtt", "schema": "state", "name": "test"} } @@ -327,91 +310,59 @@ async def test_status_invalid_json(hass, mqtt_mock): assert state.state == STATE_UNKNOWN +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + async def test_default_availability_payload(hass, mqtt_mock): """Test availability by default payload with defined topic.""" - config = deepcopy(DEFAULT_CONFIG) - config.update({"availability_topic": "availability-topic"}) - - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "online") - - state = hass.states.get("vacuum.mqtttest") - assert STATE_UNAVAILABLE != state.state - - async_fire_mqtt_message(hass, "availability-topic", "offline") - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE + await help_test_default_availability_payload( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) async def test_custom_availability_payload(hass, mqtt_mock): """Test availability by custom payload with defined topic.""" - config = deepcopy(DEFAULT_CONFIG) - config.update( - { - "availability_topic": "availability-topic", - "payload_available": "good", - "payload_not_available": "nogood", - } + await help_test_custom_availability_payload( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) - assert await async_setup_component(hass, vacuum.DOMAIN, {vacuum.DOMAIN: config}) - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "good") - - state = hass.states.get("vacuum.mqtttest") - assert state.state != STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability-topic", "nogood") - - state = hass.states.get("vacuum.mqtttest") - assert state.state == STATE_UNAVAILABLE - 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, vacuum.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + +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, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) 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, vacuum.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "schema": "state",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "schema": "state",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, vacuum.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) @@ -462,47 +413,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT vacuum device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT vacuum device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_update(hass, mqtt_mock): """Test device registry update.""" await help_test_entity_device_info_update( - hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "schema": "state", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - vacuum.DOMAIN: [ - { - "platform": "mqtt", - "schema": "state", - "name": "beer", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, vacuum.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index b923e3431c1..5f5c69d5a22 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -3,25 +3,25 @@ from asynctest import patch import pytest from homeassistant.components import switch -from homeassistant.const import ( - ATTR_ASSUMED_STATE, - STATE_OFF, - STATE_ON, - STATE_UNAVAILABLE, -) +from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component from .common import ( + 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_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, help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, help_test_update_with_json_attrs_bad_JSON, help_test_update_with_json_attrs_not_dict, @@ -30,29 +30,8 @@ from .common import ( from tests.common import async_fire_mqtt_message, async_mock_mqtt_component, mock_coro from tests.components.switch import common -DEFAULT_CONFIG_ATTR = { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "test-topic", - "json_attributes_topic": "attr-topic", - } -} - -DEFAULT_CONFIG_DEVICE_INFO = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": { - "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], - "manufacturer": "Whatever", - "name": "Beer", - "model": "Glass", - "sw_version": "0.1-beta", - }, - "unique_id": "veryunique", +DEFAULT_CONFIG = { + switch.DOMAIN: {"platform": "mqtt", "name": "test", "command_topic": "test-topic"} } @@ -171,92 +150,47 @@ async def test_controlling_state_via_topic_and_json_message(hass, mock_publish): assert state.state == STATE_OFF -async def test_default_availability_payload(hass, mock_publish): - """Test the availability payload.""" - assert await async_setup_component( - hass, - switch.DOMAIN, - { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "availability_topic": "availability_topic", - "payload_on": 1, - "payload_off": 0, - } - }, +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG ) - state = hass.states.get("switch.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability_topic", "online") +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + config = { + switch.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } + } - state = hass.states.get("switch.test") - assert state.state == STATE_OFF - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "availability_topic", "offline") - - state = hass.states.get("switch.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "state-topic", "1") - - state = hass.states.get("switch.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability_topic", "online") - - state = hass.states.get("switch.test") - assert state.state == STATE_ON - - -async def test_custom_availability_payload(hass, mock_publish): - """Test the availability payload.""" - assert await async_setup_component( - hass, - switch.DOMAIN, - { - switch.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "state-topic", - "command_topic": "command-topic", - "availability_topic": "availability_topic", - "payload_on": 1, - "payload_off": 0, - "payload_available": "good", - "payload_not_available": "nogood", - } - }, + await help_test_default_availability_payload( + hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" ) - state = hass.states.get("switch.test") - assert state.state == STATE_UNAVAILABLE - async_fire_mqtt_message(hass, "availability_topic", "good") +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + config = { + switch.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "payload_on": 1, + "payload_off": 0, + } + } - state = hass.states.get("switch.test") - assert state.state == STATE_OFF - assert not state.attributes.get(ATTR_ASSUMED_STATE) - - async_fire_mqtt_message(hass, "availability_topic", "nogood") - - state = hass.states.get("switch.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "state-topic", "1") - - state = hass.states.get("switch.test") - assert state.state == STATE_UNAVAILABLE - - async_fire_mqtt_message(hass, "availability_topic", "good") - - state = hass.states.get("switch.test") - assert state.state == STATE_ON + await help_test_custom_availability_payload( + hass, mqtt_mock, switch.DOMAIN, config, True, "state-topic", "1" + ) async def test_custom_state_payload(hass, mock_publish): @@ -296,38 +230,35 @@ async def test_custom_state_payload(hass, mock_publish): 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, switch.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + ) + + +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, switch.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, switch.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, switch.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, switch.DOMAIN, DEFAULT_CONFIG_ATTR + hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG ) async def test_discovery_update_attr(hass, mqtt_mock, caplog): """Test update of discovered MQTTAttributes.""" - data1 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic1" }' - ) - data2 = ( - '{ "name": "test",' - ' "command_topic": "test_topic",' - ' "json_attributes_topic": "attr-topic2" }' - ) await help_test_discovery_update_attr( - hass, mqtt_mock, caplog, switch.DOMAIN, data1, data2 + hass, mqtt_mock, caplog, switch.DOMAIN, DEFAULT_CONFIG ) @@ -394,45 +325,34 @@ async def test_discovery_broken(hass, mqtt_mock, caplog): ) +async def test_entity_device_info_with_connection(hass, mqtt_mock): + """Test MQTT switch device registry integration.""" + await help_test_entity_device_info_with_connection( + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + ) + + async def test_entity_device_info_with_identifier(hass, mqtt_mock): """Test MQTT switch device registry integration.""" await help_test_entity_device_info_with_identifier( - hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, switch.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, switch.DOMAIN, DEFAULT_CONFIG_DEVICE_INFO + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG ) async def test_entity_device_info_remove(hass, mqtt_mock): """Test device registry remove.""" - config = { - "platform": "mqtt", - "name": "Test 1", - "state_topic": "test-topic", - "command_topic": "test-command-topic", - "device": {"identifiers": ["helloworld"]}, - "unique_id": "veryunique", - } - await help_test_entity_device_info_remove(hass, mqtt_mock, switch.DOMAIN, config) + await help_test_entity_device_info_remove( + hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_id_update(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - config = { - switch.DOMAIN: [ - { - "platform": "mqtt", - "name": "beer", - "state_topic": "test-topic", - "command_topic": "command-topic", - "availability_topic": "avty-topic", - "unique_id": "TOTALLY_UNIQUE", - } - ] - } - await help_test_entity_id_update(hass, mqtt_mock, switch.DOMAIN, config) + await help_test_entity_id_update(hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG) From abd1909e2b75b7874c2b51a3f0b924c063c76b39 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Mar 2020 03:59:39 -0700 Subject: [PATCH 086/431] Make zone dependency of device tracker an after dep (#32880) * Make zone dependency of device tracker an after dep * Fix test --- .../components/device_tracker/manifest.json | 4 +-- tests/components/mobile_app/test_webhook.py | 6 ++-- tests/components/unifi/test_device_tracker.py | 24 +++++++-------- tests/components/unifi/test_sensor.py | 4 +-- tests/components/unifi/test_switch.py | 30 +++++++++---------- 5 files changed, 35 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 4bd9846f76d..2d0e9a82a53 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -3,8 +3,8 @@ "name": "Device Tracker", "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], - "dependencies": ["zone"], - "after_dependencies": [], + "dependencies": [], + "after_dependencies": ["zone"], "codeowners": [], "quality_scale": "internal" } diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 39837543a47..974fb577606 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -160,8 +160,10 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie assert resp.status == 200 json = await resp.json() - assert len(json) == 1 - assert json[0]["entity_id"] == "zone.home" + assert len(json) == 2 + zones = sorted(json, key=lambda entry: entry["entity_id"]) + assert zones[0]["entity_id"] == "zone.home" + assert zones[1]["entity_id"] == "zone.test" async def test_webhook_handle_get_config(hass, create_registrations, webhook_client): diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index bb15ff65fb9..e9ffad5a8b4 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -109,7 +109,7 @@ async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" await setup_unifi_integration(hass) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 async def test_tracked_devices(hass): @@ -124,7 +124,7 @@ async def test_tracked_devices(hass): devices_response=[DEVICE_1, DEVICE_2], known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 5 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -185,7 +185,7 @@ async def test_controller_state_change(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 2 # Controller unavailable controller.async_unifi_signalling_callback( @@ -215,7 +215,7 @@ async def test_option_track_clients(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -260,7 +260,7 @@ async def test_option_track_wired_clients(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -305,7 +305,7 @@ async def test_option_track_devices(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -350,7 +350,7 @@ async def test_option_ssid_filter(hass): controller = await setup_unifi_integration( hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3], ) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 # SSID filter active client_3 = hass.states.get("device_tracker.client_3") @@ -388,7 +388,7 @@ async def test_wireless_client_go_wired_issue(hass): client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) controller = await setup_unifi_integration(hass, clients_response=[client_1_client]) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -461,7 +461,7 @@ async def test_restoring_client(hass): clients_response=[CLIENT_2], clients_all_response=[CLIENT_1], ) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 2 device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None @@ -475,7 +475,7 @@ async def test_dont_track_clients(hass): clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is None @@ -493,7 +493,7 @@ async def test_dont_track_devices(hass): clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -510,7 +510,7 @@ async def test_dont_track_wired_clients(hass): options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, clients_response=[CLIENT_1, CLIENT_2], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index 7d0600f5885..a858bc9a649 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -55,7 +55,7 @@ async def test_no_clients(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 async def test_sensors(hass): @@ -71,7 +71,7 @@ async def test_sensors(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_all()) == 4 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx.state == "1234.0" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index bc30161b77f..a06be14024b 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -209,7 +209,7 @@ async def test_no_clients(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 async def test_controller_not_client(hass): @@ -222,7 +222,7 @@ async def test_controller_not_client(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None @@ -240,7 +240,7 @@ async def test_not_admin(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 async def test_switches(hass): @@ -258,7 +258,7 @@ async def test_switches(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 3 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None @@ -312,7 +312,7 @@ async def test_new_client_discovered_on_block_control(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 blocked = hass.states.get("switch.block_client_1") assert blocked is None @@ -324,7 +324,7 @@ async def test_new_client_discovered_on_block_control(hass): controller.api.session_handler("data") await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 blocked = hass.states.get("switch.block_client_1") assert blocked is not None @@ -336,7 +336,7 @@ async def test_option_block_clients(hass): options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, clients_all_response=[BLOCKED, UNBLOCKED], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 # Add a second switch hass.config_entries.async_update_entry( @@ -344,28 +344,28 @@ async def test_option_block_clients(hass): options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 2 # Remove the second switch again hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 # Enable one and remove another one hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 # Remove one hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: []}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_all()) == 0 async def test_new_client_discovered_on_poe_control(hass): @@ -378,7 +378,7 @@ async def test_new_client_discovered_on_poe_control(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_all()) == 1 controller.api.websocket._data = { "meta": {"message": "sta:sync"}, @@ -391,7 +391,7 @@ async def test_new_client_discovered_on_poe_control(hass): "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) assert len(controller.mock_requests) == 5 - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 2 assert controller.mock_requests[4] == { "json": { "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] @@ -430,7 +430,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_all()) == 3 switch_1 = hass.states.get("switch.poe_client_1") switch_2 = hass.states.get("switch.poe_client_2") @@ -481,7 +481,7 @@ async def test_restoring_client(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_all()) == 2 device_1 = hass.states.get("switch.client_1") assert device_1 is not None From 0c49c8578b3d9a30e7e6b4558c15872925ea2eb7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Mar 2020 05:00:54 -0600 Subject: [PATCH 087/431] Remove unnecessary awaits in RainMachine (#32884) * Remove unnecessary awaits in RainMachine * Cleanup --- homeassistant/components/rainmachine/__init__.py | 9 ++++++++- .../components/rainmachine/binary_sensor.py | 16 +++++++++------- homeassistant/components/rainmachine/sensor.py | 16 +++++++++------- homeassistant/components/rainmachine/switch.py | 7 +++++-- 4 files changed, 31 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 4844a9e68c8..4cf32185dc9 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -455,9 +455,16 @@ class RainMachineEntity(Entity): @callback def _update_state(self): """Update the state.""" - self.async_schedule_update_ha_state(True) + self.update_from_latest_data() + self.async_write_ha_state() async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" for handler in self._dispatcher_handlers: handler() + self._dispatcher_handlers = [] + + @callback + def update_from_latest_data(self): + """Update the entity.""" + raise NotImplementedError diff --git a/homeassistant/components/rainmachine/binary_sensor.py b/homeassistant/components/rainmachine/binary_sensor.py index 34b8de80b88..409ad0c9980 100644 --- a/homeassistant/components/rainmachine/binary_sensor.py +++ b/homeassistant/components/rainmachine/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import RainMachineEntity @@ -129,9 +130,15 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state) ) await self.rainmachine.async_register_sensor_api_interest(self._api_category) - await self.async_update() + self.update_from_latest_data() - async def async_update(self): + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listeners and deregister API interest.""" + super().async_will_remove_from_hass() + self.rainmachine.async_deregister_sensor_api_interest(self._api_category) + + @callback + def update_from_latest_data(self): """Update the state.""" if self._sensor_type == TYPE_FLOW_SENSOR: self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( @@ -157,8 +164,3 @@ class RainMachineBinarySensor(RainMachineEntity, BinarySensorDevice): self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["rainSensor"] elif self._sensor_type == TYPE_WEEKDAY: self._state = self.rainmachine.data[DATA_RESTRICTIONS_CURRENT]["weekDay"] - - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listeners and deregister API interest.""" - super().async_will_remove_from_hass() - self.rainmachine.async_deregister_sensor_api_interest(self._api_category) diff --git a/homeassistant/components/rainmachine/sensor.py b/homeassistant/components/rainmachine/sensor.py index 8487628a32b..371ba00dfd0 100644 --- a/homeassistant/components/rainmachine/sensor.py +++ b/homeassistant/components/rainmachine/sensor.py @@ -1,6 +1,7 @@ """This platform provides support for sensor data from RainMachine.""" import logging +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import RainMachineEntity @@ -146,9 +147,15 @@ class RainMachineSensor(RainMachineEntity): async_dispatcher_connect(self.hass, SENSOR_UPDATE_TOPIC, self._update_state) ) await self.rainmachine.async_register_sensor_api_interest(self._api_category) - await self.async_update() + self.update_from_latest_data() - async def async_update(self): + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listeners and deregister API interest.""" + super().async_will_remove_from_hass() + self.rainmachine.async_deregister_sensor_api_interest(self._api_category) + + @callback + def update_from_latest_data(self): """Update the sensor's state.""" if self._sensor_type == TYPE_FLOW_SENSOR_CLICK_M3: self._state = self.rainmachine.data[DATA_PROVISION_SETTINGS]["system"].get( @@ -178,8 +185,3 @@ class RainMachineSensor(RainMachineEntity): self._state = self.rainmachine.data[DATA_RESTRICTIONS_UNIVERSAL][ "freezeProtectTemp" ] - - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listeners and deregister API interest.""" - super().async_will_remove_from_hass() - self.rainmachine.async_deregister_sensor_api_interest(self._api_category) diff --git a/homeassistant/components/rainmachine/switch.py b/homeassistant/components/rainmachine/switch.py index 2bf63dbf495..264de1d6782 100644 --- a/homeassistant/components/rainmachine/switch.py +++ b/homeassistant/components/rainmachine/switch.py @@ -6,6 +6,7 @@ from regenmaschine.errors import RequestError from homeassistant.components.switch import SwitchDevice from homeassistant.const import ATTR_ID +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import RainMachineEntity @@ -205,7 +206,8 @@ class RainMachineProgram(RainMachineSwitch): self.rainmachine.controller.programs.start(self._rainmachine_entity_id) ) - async def async_update(self) -> None: + @callback + def update_from_latest_data(self) -> None: """Update info for the program.""" [self._switch_data] = [ p @@ -269,7 +271,8 @@ class RainMachineZone(RainMachineSwitch): ) ) - async def async_update(self) -> None: + @callback + def update_from_latest_data(self) -> None: """Update info for the zone.""" [self._switch_data] = [ z From 1da35e29398a17984d9eadf488be17c203bd0968 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Mar 2020 05:01:40 -0600 Subject: [PATCH 088/431] Add cleanup to Notion (#32887) * Add cleanup to Notion * Base update method --- homeassistant/components/notion/__init__.py | 11 ++++++++++- homeassistant/components/notion/binary_sensor.py | 4 +++- homeassistant/components/notion/sensor.py | 5 ++++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index f387e820253..e9c45c62816 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -306,13 +306,22 @@ class NotionEntity(Entity): def update(): """Update the entity.""" self.hass.async_create_task(self._update_bridge_id()) - self.async_schedule_update_ha_state(True) + self.update_from_latest_data() + self.async_write_ha_state() self._async_unsub_dispatcher_connect = async_dispatcher_connect( self.hass, TOPIC_DATA_UPDATE, update ) + self.update_from_latest_data() + async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() + self._async_unsub_dispatcher_connect = None + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + raise NotImplementedError diff --git a/homeassistant/components/notion/binary_sensor.py b/homeassistant/components/notion/binary_sensor.py index 5079348e821..53a98204704 100644 --- a/homeassistant/components/notion/binary_sensor.py +++ b/homeassistant/components/notion/binary_sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.core import callback from . import ( BINARY_SENSOR_TYPES, @@ -75,7 +76,8 @@ class NotionBinarySensor(NotionEntity, BinarySensorDevice): if task["task_type"] == SENSOR_SMOKE_CO: return self._state != "no_alarm" - async def async_update(self): + @callback + def update_from_latest_data(self): """Fetch new state data for the sensor.""" task = self._notion.tasks[self._task_id] diff --git a/homeassistant/components/notion/sensor.py b/homeassistant/components/notion/sensor.py index 0e1c9f25cb2..918ee8c5f95 100644 --- a/homeassistant/components/notion/sensor.py +++ b/homeassistant/components/notion/sensor.py @@ -1,6 +1,8 @@ """Support for Notion sensors.""" import logging +from homeassistant.core import callback + from . import SENSOR_TEMPERATURE, SENSOR_TYPES, NotionEntity from .const import DATA_CLIENT, DOMAIN @@ -58,7 +60,8 @@ class NotionSensor(NotionEntity): """Return the unit of measurement.""" return self._unit - async def async_update(self): + @callback + def update_from_latest_data(self): """Fetch new state data for the sensor.""" task = self._notion.tasks[self._task_id] From d98432c328b6a2680e1df4f75c3f209264f6c5ba Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Mar 2020 05:02:05 -0600 Subject: [PATCH 089/431] Add cleanup to Ambient PWS (#32888) --- homeassistant/components/ambient_station/__init__.py | 11 ++++++++++- .../components/ambient_station/binary_sensor.py | 4 +++- homeassistant/components/ambient_station/sensor.py | 4 +++- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index 4fd6590b286..b840a1b7171 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -520,13 +520,22 @@ class AmbientWeatherEntity(Entity): @callback def update(): """Update the state.""" - self.async_schedule_update_ha_state(True) + self.update_from_latest_data() + self.async_write_ha_state() self._async_unsub_dispatcher_connect = async_dispatcher_connect( self.hass, f"ambient_station_data_update_{self._mac_address}", update ) + self.update_from_latest_data() + async def async_will_remove_from_hass(self): """Disconnect dispatcher listener when removed.""" if self._async_unsub_dispatcher_connect: self._async_unsub_dispatcher_connect() + self._async_unsub_dispatcher_connect = None + + @callback + def update_from_latest_data(self): + """Update the entity from the latest data.""" + raise NotImplementedError diff --git a/homeassistant/components/ambient_station/binary_sensor.py b/homeassistant/components/ambient_station/binary_sensor.py index e4c1c8ccdac..d1b1f9b8f1d 100644 --- a/homeassistant/components/ambient_station/binary_sensor.py +++ b/homeassistant/components/ambient_station/binary_sensor.py @@ -3,6 +3,7 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.const import ATTR_NAME +from homeassistant.core import callback from . import ( SENSOR_TYPES, @@ -76,7 +77,8 @@ class AmbientWeatherBinarySensor(AmbientWeatherEntity, BinarySensorDevice): return self._state == 1 - async def async_update(self): + @callback + def update_from_latest_data(self): """Fetch new state data for the entity.""" self._state = self._ambient.stations[self._mac_address][ATTR_LAST_DATA].get( self._sensor_type diff --git a/homeassistant/components/ambient_station/sensor.py b/homeassistant/components/ambient_station/sensor.py index c400d2ec97b..b3b76715368 100644 --- a/homeassistant/components/ambient_station/sensor.py +++ b/homeassistant/components/ambient_station/sensor.py @@ -2,6 +2,7 @@ import logging from homeassistant.const import ATTR_NAME +from homeassistant.core import callback from . import ( SENSOR_TYPES, @@ -74,7 +75,8 @@ class AmbientWeatherSensor(AmbientWeatherEntity): """Return the unit of measurement.""" return self._unit - async def async_update(self): + @callback + def update_from_latest_data(self): """Fetch new state data for the sensor.""" if self._sensor_type == TYPE_SOLARRADIATION_LX: # If the user requests the solarradiation_lx sensor, use the From a3e250447029388b61b8436fe86ce1a9f829922d Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Mar 2020 05:02:30 -0600 Subject: [PATCH 090/431] Add cleanup to SimpliSafe (#32889) --- homeassistant/components/simplisafe/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 9f014a44a23..bf12951e2ae 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -642,13 +642,17 @@ class SimpliSafeEntity(Entity): @callback def update(): """Update the state.""" - self.async_schedule_update_ha_state(True) + self.update_from_latest_data() + self.async_write_ha_state() self._async_unsub_dispatcher_connect = async_dispatcher_connect( self.hass, TOPIC_UPDATE.format(self._system.system_id), update ) - async def async_update(self): + self.update_from_latest_data() + + @callback + def update_from_latest_data(self): """Update the entity.""" self.async_update_from_rest_api() From 4c32fd12fc23a9d50821f73335d766a325f9f114 Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 17 Mar 2020 17:46:30 +0100 Subject: [PATCH 091/431] Bump iCloud to 0.9.5 (#32901) --- homeassistant/components/icloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 76b6b9b39ae..b4ef46cfbaf 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,7 +3,7 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.4"], + "requirements": ["pyicloud==0.9.5"], "dependencies": [], "codeowners": ["@Quentame"] } diff --git a/requirements_all.txt b/requirements_all.txt index 3e427ad270d..e11a2913588 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1315,7 +1315,7 @@ pyhomeworks==0.0.6 pyialarm==0.3 # homeassistant.components.icloud -pyicloud==0.9.4 +pyicloud==0.9.5 # homeassistant.components.intesishome pyintesishome==1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 598758949df..699e66b8f75 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -486,7 +486,7 @@ pyheos==0.6.0 pyhomematic==0.1.65 # homeassistant.components.icloud -pyicloud==0.9.4 +pyicloud==0.9.5 # homeassistant.components.ipma pyipma==2.0.5 From 433b89de501f386273c6fe9995d32da584a4914e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 17 Mar 2020 19:04:01 +0200 Subject: [PATCH 092/431] Add Huawei LTE WiFi status binary sensors (#32553) --- .../components/huawei_lte/__init__.py | 4 + .../components/huawei_lte/binary_sensor.py | 84 +++++++++++++++++-- homeassistant/components/huawei_lte/const.py | 3 +- 3 files changed, 85 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/huawei_lte/__init__.py b/homeassistant/components/huawei_lte/__init__.py index e4291ae7e67..5d618c1fdb5 100644 --- a/homeassistant/components/huawei_lte/__init__.py +++ b/homeassistant/components/huawei_lte/__init__.py @@ -69,6 +69,7 @@ from .const import ( KEY_NET_CURRENT_PLMN, KEY_NET_NET_MODE, KEY_WLAN_HOST_LIST, + KEY_WLAN_WIFI_FEATURE_SWITCH, NOTIFY_SUPPRESS_TIMEOUT, SERVICE_CLEAR_TRAFFIC_STATISTICS, SERVICE_REBOOT, @@ -243,6 +244,9 @@ class Router: self._get_data(KEY_NET_CURRENT_PLMN, self.client.net.current_plmn) self._get_data(KEY_NET_NET_MODE, self.client.net.net_mode) self._get_data(KEY_WLAN_HOST_LIST, self.client.wlan.host_list) + self._get_data( + KEY_WLAN_WIFI_FEATURE_SWITCH, self.client.wlan.wifi_feature_switch + ) self.signal_update() diff --git a/homeassistant/components/huawei_lte/binary_sensor.py b/homeassistant/components/huawei_lte/binary_sensor.py index 104933fe714..af6ed75d591 100644 --- a/homeassistant/components/huawei_lte/binary_sensor.py +++ b/homeassistant/components/huawei_lte/binary_sensor.py @@ -13,7 +13,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_URL from . import HuaweiLteBaseEntity -from .const import DOMAIN, KEY_MONITORING_STATUS +from .const import DOMAIN, KEY_MONITORING_STATUS, KEY_WLAN_WIFI_FEATURE_SWITCH _LOGGER = logging.getLogger(__name__) @@ -25,6 +25,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if router.data.get(KEY_MONITORING_STATUS): entities.append(HuaweiLteMobileConnectionBinarySensor(router)) + entities.append(HuaweiLteWifiStatusBinarySensor(router)) + entities.append(HuaweiLteWifi24ghzStatusBinarySensor(router)) + entities.append(HuaweiLteWifi5ghzStatusBinarySensor(router)) async_add_entities(entities, True) @@ -37,6 +40,15 @@ class HuaweiLteBaseBinarySensor(HuaweiLteBaseEntity, BinarySensorDevice): item: str _raw_state: Optional[str] = attr.ib(init=False, default=None) + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return False + + @property + def _device_unique_id(self) -> str: + return f"{self.key}.{self.item}" + async def async_added_to_hass(self): """Subscribe to needed data on add.""" await super().async_added_to_hass() @@ -83,10 +95,6 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): def _entity_name(self) -> str: return "Mobile connection" - @property - def _device_unique_id(self) -> str: - return f"{self.key}.{self.item}" - @property def is_on(self) -> bool: """Return whether the binary sensor is on.""" @@ -109,6 +117,11 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): """Return mobile connectivity sensor icon.""" return "mdi:signal" if self.is_on else "mdi:signal-off" + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return True + @property def device_state_attributes(self): """Get additional attributes related to connection status.""" @@ -120,3 +133,64 @@ class HuaweiLteMobileConnectionBinarySensor(HuaweiLteBaseBinarySensor): self._raw_state ] return attributes + + +class HuaweiLteBaseWifiStatusBinarySensor(HuaweiLteBaseBinarySensor): + """Huawei LTE WiFi status binary sensor base class.""" + + @property + def is_on(self) -> bool: + """Return whether the binary sensor is on.""" + return self._raw_state is not None and int(self._raw_state) == 1 + + @property + def assumed_state(self) -> bool: + """Return True if real state is assumed, not known.""" + return self._raw_state is None + + @property + def icon(self): + """Return WiFi status sensor icon.""" + return "mdi:wifi" if self.is_on else "mdi:wifi-off" + + +@attr.s +class HuaweiLteWifiStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): + """Huawei LTE WiFi status binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_MONITORING_STATUS + self.item = "WifiStatus" + + @property + def _entity_name(self) -> str: + return "WiFi status" + + +@attr.s +class HuaweiLteWifi24ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): + """Huawei LTE 2.4GHz WiFi status binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_WLAN_WIFI_FEATURE_SWITCH + self.item = "wifi24g_switch_enable" + + @property + def _entity_name(self) -> str: + return "2.4GHz WiFi status" + + +@attr.s +class HuaweiLteWifi5ghzStatusBinarySensor(HuaweiLteBaseWifiStatusBinarySensor): + """Huawei LTE 5GHz WiFi status binary sensor.""" + + def __attrs_post_init__(self): + """Initialize identifiers.""" + self.key = KEY_WLAN_WIFI_FEATURE_SWITCH + self.item = "wifi5g_enabled" + + @property + def _entity_name(self) -> str: + return "5GHz WiFi status" diff --git a/homeassistant/components/huawei_lte/const.py b/homeassistant/components/huawei_lte/const.py index 5279dd65b92..583c1c7d6f1 100644 --- a/homeassistant/components/huawei_lte/const.py +++ b/homeassistant/components/huawei_lte/const.py @@ -33,8 +33,9 @@ KEY_MONITORING_TRAFFIC_STATISTICS = "monitoring_traffic_statistics" KEY_NET_CURRENT_PLMN = "net_current_plmn" KEY_NET_NET_MODE = "net_net_mode" KEY_WLAN_HOST_LIST = "wlan_host_list" +KEY_WLAN_WIFI_FEATURE_SWITCH = "wlan_wifi_feature_switch" -BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS} +BINARY_SENSOR_KEYS = {KEY_MONITORING_STATUS, KEY_WLAN_WIFI_FEATURE_SWITCH} DEVICE_TRACKER_KEYS = {KEY_WLAN_HOST_LIST} From 5c83367bb01c4bbc00a39505d426a4a72073588c Mon Sep 17 00:00:00 2001 From: Quentame Date: Tue, 17 Mar 2020 18:16:58 +0100 Subject: [PATCH 093/431] Fix iCloud init while pending (#32750) * Fix iCloud init while pending Continue if device is pending while setup Create devices and fetch 15s if pending, otherwise determine interval to fetch. * Add retried_fetch guard --- homeassistant/components/icloud/account.py | 31 +++++++++------------- 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index 6c4d9c5c25f..50a3e74f78f 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -97,6 +97,7 @@ class IcloudAccount: self._owner_fullname = None self._family_members_fullname = {} self._devices = {} + self._retried_fetch = False self.listeners = [] @@ -122,10 +123,6 @@ class IcloudAccount: _LOGGER.error("No iCloud device found") raise ConfigEntryNotReady - if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending": - _LOGGER.warning("Pending devices, trying again ...") - raise ConfigEntryNotReady - self._owner_fullname = f"{user_info['firstName']} {user_info['lastName']}" self._family_members_fullname = {} @@ -157,28 +154,15 @@ class IcloudAccount: ) return - if DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending": - _LOGGER.warning("Pending devices, trying again in 15s") - self._fetch_interval = 0.25 - dispatcher_send(self.hass, self.signal_device_update) - track_point_in_utc_time( - self.hass, - self.keep_alive, - utcnow() + timedelta(minutes=self._fetch_interval), - ) - return - # Gets devices infos new_device = False for device in api_devices: status = device.status(DEVICE_STATUS_SET) device_id = status[DEVICE_ID] device_name = status[DEVICE_NAME] - device_status = DEVICE_STATUS_CODES.get(status[DEVICE_STATUS], "error") if ( - device_status == "pending" - or status[DEVICE_BATTERY_STATUS] == "Unknown" + status[DEVICE_BATTERY_STATUS] == "Unknown" or status.get(DEVICE_BATTERY_LEVEL) is None ): continue @@ -198,7 +182,16 @@ class IcloudAccount: self._devices[device_id].update(status) new_device = True - self._fetch_interval = self._determine_interval() + if ( + DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending" + and not self._retried_fetch + ): + _LOGGER.warning("Pending devices, trying again in 15s") + self._fetch_interval = 0.25 + self._retried_fetch = True + else: + self._fetch_interval = self._determine_interval() + self._retried_fetch = False dispatcher_send(self.hass, self.signal_device_update) if new_device: From 3910ab6cabb27348e26bd67424c08b8a2303052f Mon Sep 17 00:00:00 2001 From: brubaked <37672083+brubaked@users.noreply.github.com> Date: Tue, 17 Mar 2020 10:17:18 -0700 Subject: [PATCH 094/431] Changed Sensor icons to be more emotionally sensitive (#32904) The existing sensor icons, while descriptive - dead = dead - are perhaps too matter of fact and don't accurately convey the tragedy. I changed emoticon-dead-outline to emoticon-cry-outline, as I think it better conveys the reality of the situation along with the emotions tied to the statistic. --- homeassistant/components/coronavirus/sensor.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/coronavirus/sensor.py b/homeassistant/components/coronavirus/sensor.py index 3885dbebf24..2887427ec6b 100644 --- a/homeassistant/components/coronavirus/sensor.py +++ b/homeassistant/components/coronavirus/sensor.py @@ -7,9 +7,9 @@ from .const import ATTRIBUTION, OPTION_WORLDWIDE SENSORS = { "confirmed": "mdi:emoticon-neutral-outline", - "current": "mdi:emoticon-frown-outline", + "current": "mdi:emoticon-sad-outline", "recovered": "mdi:emoticon-happy-outline", - "deaths": "mdi:emoticon-dead-outline", + "deaths": "mdi:emoticon-cry-outline", } From ff582721dd46febdc024b1c9e90d37d4de8927ef Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Mar 2020 10:30:56 -0700 Subject: [PATCH 095/431] Bump cast to 4.2.0 (#32906) --- 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 51558e78266..be0b64dc0b1 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==4.1.1"], + "requirements": ["pychromecast==4.2.0"], "dependencies": [], "after_dependencies": ["cloud"], "zeroconf": ["_googlecast._tcp.local."], diff --git a/requirements_all.txt b/requirements_all.txt index e11a2913588..96364414e59 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1188,7 +1188,7 @@ pycfdns==0.0.1 pychannels==1.0.0 # homeassistant.components.cast -pychromecast==4.1.1 +pychromecast==4.2.0 # homeassistant.components.cmus pycmus==0.1.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 699e66b8f75..ea3c27e5b1e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -446,7 +446,7 @@ pyblackbird==0.5 pybotvac==0.0.17 # homeassistant.components.cast -pychromecast==4.1.1 +pychromecast==4.2.0 # homeassistant.components.coolmaster pycoolmasternet==0.0.4 From 7bbffa6e6d1b24abdde774ec2180af81471f3823 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Mar 2020 12:14:17 -0700 Subject: [PATCH 096/431] Blacklist auto_backup (#32912) * Blacklist auto_backup * Mock with a set --- homeassistant/setup.py | 8 ++++++++ tests/test_setup.py | 13 +++++++++++++ 2 files changed, 21 insertions(+) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 40d767728d3..5c3d2bcd7fd 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -20,6 +20,8 @@ DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 +BLACKLIST = set(["auto_backup"]) + def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: """Set up a component and all its dependencies.""" @@ -38,6 +40,12 @@ async def async_setup_component( if domain in hass.config.components: return True + if domain in BLACKLIST: + _LOGGER.error( + "Integration %s is blacklisted because it is causing issues.", domain + ) + return False + setup_tasks = hass.data.setdefault(DATA_SETUP, {}) if domain in setup_tasks: diff --git a/tests/test_setup.py b/tests/test_setup.py index f90a7269752..95fd1e0a15d 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -6,6 +6,7 @@ import os import threading from unittest import mock +from asynctest import patch import voluptuous as vol from homeassistant import setup @@ -535,3 +536,15 @@ async def test_setup_import_blows_up(hass): "homeassistant.loader.Integration.get_component", side_effect=ValueError ): assert not await setup.async_setup_component(hass, "sun", {}) + + +async def test_blacklist(caplog): + """Test setup blacklist.""" + with patch("homeassistant.setup.BLACKLIST", {"bad_integration"}): + assert not await setup.async_setup_component( + mock.Mock(config=mock.Mock(components=[])), "bad_integration", {} + ) + assert ( + "Integration bad_integration is blacklisted because it is causing issues." + in caplog.text + ) From aece76f6cd5ae5a237d2fe9c366ae2b809444dd3 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 17 Mar 2020 20:17:59 +0100 Subject: [PATCH 097/431] Bump wled 0.3.0, reduce calls stability improvements (#32903) --- homeassistant/components/wled/__init__.py | 6 +++--- homeassistant/components/wled/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/wled/__init__.py | 14 +++++++++++++- 5 files changed, 19 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 19901ce297d..91c130a7a81 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -26,7 +26,7 @@ from .const import ( DOMAIN, ) -SCAN_INTERVAL = timedelta(seconds=10) +SCAN_INTERVAL = timedelta(seconds=5) WLED_COMPONENTS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) _LOGGER = logging.getLogger(__name__) @@ -94,7 +94,7 @@ def wled_exception_handler(func): async def handler(self, *args, **kwargs): try: await func(self, *args, **kwargs) - await self.coordinator.async_refresh() + self.coordinator.update_listeners() except WLEDConnectionError as error: _LOGGER.error("Error communicating with API: %s", error) @@ -128,7 +128,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator): async def _async_update_data(self) -> WLEDDevice: """Fetch data from WLED.""" try: - return await self.wled.update() + return await self.wled.update(full_update=not self.last_update_success) except WLEDError as error: raise UpdateFailed(f"Invalid response from API: {error}") diff --git a/homeassistant/components/wled/manifest.json b/homeassistant/components/wled/manifest.json index 4b501d0c67c..d501edbd631 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.2.1"], + "requirements": ["wled==0.3.0"], "dependencies": [], "zeroconf": ["_wled._tcp.local."], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index 96364414e59..5b7e4438ed8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2105,7 +2105,7 @@ wirelesstagpy==0.4.0 withings-api==2.1.3 # homeassistant.components.wled -wled==0.2.1 +wled==0.3.0 # homeassistant.components.wunderlist wunderpy2==0.1.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ea3c27e5b1e..baa3b4dfffb 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -736,7 +736,7 @@ watchdog==0.8.3 withings-api==2.1.3 # homeassistant.components.wled -wled==0.2.1 +wled==0.3.0 # homeassistant.components.bluesound # homeassistant.components.rest diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py index 41cbbf01074..b4b01c66d8a 100644 --- a/tests/components/wled/__init__.py +++ b/tests/components/wled/__init__.py @@ -25,7 +25,19 @@ async def init_integration( aioclient_mock.post( "http://example.local:80/json/state", - json={"success": True}, + json={}, + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + "http://example.local:80/json/info", + json={}, + headers={"Content-Type": "application/json"}, + ) + + aioclient_mock.get( + "http://example.local:80/json/state", + json={}, headers={"Content-Type": "application/json"}, ) From 097b056324ce187722ff56cf34554860387d9d77 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 17 Mar 2020 20:42:55 +0100 Subject: [PATCH 098/431] Fix input text reload (#32911) * Fix input text reload * FIx schema instead --- .../components/input_text/__init__.py | 41 ++++++++----------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/input_text/__init__.py b/homeassistant/components/input_text/__init__.py index 692a0101249..c512bc221db 100644 --- a/homeassistant/components/input_text/__init__.py +++ b/homeassistant/components/input_text/__init__.py @@ -88,24 +88,22 @@ def _cv_input_text(cfg): CONFIG_SCHEMA = vol.Schema( { DOMAIN: cv.schema_with_slug_keys( - vol.Any( - vol.All( - { - vol.Optional(CONF_NAME): cv.string, - vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), - vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), - vol.Optional(CONF_INITIAL, ""): cv.string, - vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, - vol.Optional(CONF_PATTERN): cv.string, - vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( - [MODE_TEXT, MODE_PASSWORD] - ), - }, - _cv_input_text, - ), - None, - ) + vol.All( + lambda value: value or {}, + { + vol.Optional(CONF_NAME): cv.string, + vol.Optional(CONF_MIN, default=CONF_MIN_VALUE): vol.Coerce(int), + vol.Optional(CONF_MAX, default=CONF_MAX_VALUE): vol.Coerce(int), + vol.Optional(CONF_INITIAL, ""): cv.string, + vol.Optional(CONF_ICON): cv.icon, + vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, + vol.Optional(CONF_PATTERN): cv.string, + vol.Optional(CONF_MODE, default=MODE_TEXT): vol.In( + [MODE_TEXT, MODE_PASSWORD] + ), + }, + _cv_input_text, + ), ) }, extra=vol.ALLOW_EXTRA, @@ -203,13 +201,6 @@ class InputText(RestoreEntity): @classmethod def from_yaml(cls, config: typing.Dict) -> "InputText": """Return entity instance initialized from yaml storage.""" - # set defaults for empty config - config = { - CONF_MAX: CONF_MAX_VALUE, - CONF_MIN: CONF_MIN_VALUE, - CONF_MODE: MODE_TEXT, - **config, - } input_text = cls(config) input_text.entity_id = f"{DOMAIN}.{config[CONF_ID]}" input_text.editable = False From e97d21aec007e0735daafaabff5ca122038b3ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Tue, 17 Mar 2020 22:03:43 +0200 Subject: [PATCH 099/431] Run gen_requirements_all and hassfest in pre-commit (#32792) Use generate mode for easy commit amendment with readily generated files on failure. --- .pre-commit-config.yaml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7d55224c335..017d0145da1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -59,3 +59,17 @@ repos: types: [python] require_serial: true files: ^homeassistant/.+\.py$ + - id: gen_requirements_all + name: gen_requirements_all + entry: python3 -m script.gen_requirements_all + pass_filenames: false + language: system + types: [json] + files: ^homeassistant/.+/manifest\.json$ + - id: hassfest + name: hassfest + entry: python3 -m script.hassfest + pass_filenames: false + language: system + types: [json] + files: ^homeassistant/.+/manifest\.json$ From 9146f76b01d102e046ae3b43e7300fd749207b3e Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 17 Mar 2020 15:15:41 -0500 Subject: [PATCH 100/431] Apply recommendations from roku code review (#32883) * avoid patching integration methods * Update config_flow.py * apply recommendations from roku code review * Update config_flow.py --- .../components/roku/.translations/en.json | 6 ++--- homeassistant/components/roku/config_flow.py | 25 +++++++++++++------ homeassistant/components/roku/strings.json | 6 ++--- tests/components/roku/test_config_flow.py | 25 ++++++++++++------- 4 files changed, 39 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/roku/.translations/en.json b/homeassistant/components/roku/.translations/en.json index 8dccd065121..45a2dd8dcba 100644 --- a/homeassistant/components/roku/.translations/en.json +++ b/homeassistant/components/roku/.translations/en.json @@ -1,11 +1,11 @@ { "config": { "abort": { - "already_configured": "Roku device is already configured" + "already_configured": "Roku device is already configured", + "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/config_flow.py b/homeassistant/components/roku/config_flow.py index 32e66901e0f..eec5683c95d 100644 --- a/homeassistant/components/roku/config_flow.py +++ b/homeassistant/components/roku/config_flow.py @@ -34,8 +34,13 @@ def validate_input(data: Dict) -> Dict: Data has the keys from DATA_SCHEMA with values provided by the user. """ - roku = Roku(data["host"]) - device_info = roku.device_info + try: + roku = Roku(data["host"]) + device_info = roku.device_info + except (SocketGIAError, RequestException, RokuException) as exception: + raise CannotConnect from exception + except Exception as exception: # pylint: disable=broad-except + raise UnknownError from exception return { "title": data["host"], @@ -74,11 +79,11 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): try: info = await self.hass.async_add_executor_job(validate_input, user_input) - except (SocketGIAError, RequestException, RokuException): + except CannotConnect: errors["base"] = ERROR_CANNOT_CONNECT return self._show_form(errors) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error trying to connect.") + except UnknownError: + _LOGGER.exception("Unknown error trying to connect") return self.async_abort(reason=ERROR_UNKNOWN) await self.async_set_unique_id(info["serial_num"]) @@ -119,10 +124,10 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): try: await self.hass.async_add_executor_job(validate_input, user_input) return self.async_create_entry(title=name, data=user_input) - except (SocketGIAError, RequestException, RokuException): + except CannotConnect: return self.async_abort(reason=ERROR_CANNOT_CONNECT) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unknown error trying to connect.") + except UnknownError: + _LOGGER.exception("Unknown error trying to connect") return self.async_abort(reason=ERROR_UNKNOWN) return self.async_show_form( @@ -132,3 +137,7 @@ class RokuConfigFlow(ConfigFlow, domain=DOMAIN): class CannotConnect(HomeAssistantError): """Error to indicate we cannot connect.""" + + +class UnknownError(HomeAssistantError): + """Error to indicate we encountered an unknown error.""" diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 0069728d14a..2beba3433b5 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -17,11 +17,11 @@ } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "abort": { - "already_configured": "Roku device is already configured" + "already_configured": "Roku device is already configured", + "unknown": "Unexpected error" } } } diff --git a/tests/components/roku/test_config_flow.py b/tests/components/roku/test_config_flow.py index 93d3fbb938d..9aa60d8594c 100644 --- a/tests/components/roku/test_config_flow.py +++ b/tests/components/roku/test_config_flow.py @@ -124,10 +124,12 @@ async def test_form_cannot_connect(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.roku.config_flow.validate_input", + "homeassistant.components.roku.config_flow.Roku._call", side_effect=RokuException, ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={CONF_HOST: HOST} + ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} @@ -143,10 +145,12 @@ async def test_form_cannot_connect_request(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.roku.config_flow.validate_input", + "homeassistant.components.roku.config_flow.Roku._call", side_effect=RequestException, ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={CONF_HOST: HOST} + ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} @@ -162,10 +166,12 @@ async def test_form_cannot_connect_socket(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.roku.config_flow.validate_input", + "homeassistant.components.roku.config_flow.Roku._call", side_effect=SocketGIAError, ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={CONF_HOST: HOST} + ) assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} @@ -181,10 +187,11 @@ async def test_form_unknown_error(hass: HomeAssistantType) -> None: ) with patch( - "homeassistant.components.roku.config_flow.validate_input", - side_effect=Exception, + "homeassistant.components.roku.config_flow.Roku._call", side_effect=Exception, ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) + result = await hass.config_entries.flow.async_configure( + flow_id=result["flow_id"], user_input={CONF_HOST: HOST} + ) assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "unknown" From 576970d1ad8da40cc9b33526d577c0e18661056d Mon Sep 17 00:00:00 2001 From: Rami Mosleh Date: Tue, 17 Mar 2020 22:19:42 +0200 Subject: [PATCH 101/431] Fix setting up options due to config data freeze (#32872) --- homeassistant/components/mikrotik/hub.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/mikrotik/hub.py b/homeassistant/components/mikrotik/hub.py index 300d73b6b11..023bdc74a7e 100644 --- a/homeassistant/components/mikrotik/hub.py +++ b/homeassistant/components/mikrotik/hub.py @@ -332,16 +332,17 @@ class MikrotikHub: async def async_add_options(self): """Populate default options for Mikrotik.""" if not self.config_entry.options: + data = dict(self.config_entry.data) options = { - CONF_ARP_PING: self.config_entry.data.pop(CONF_ARP_PING, False), - CONF_FORCE_DHCP: self.config_entry.data.pop(CONF_FORCE_DHCP, False), - CONF_DETECTION_TIME: self.config_entry.data.pop( + CONF_ARP_PING: data.pop(CONF_ARP_PING, False), + CONF_FORCE_DHCP: data.pop(CONF_FORCE_DHCP, False), + CONF_DETECTION_TIME: data.pop( CONF_DETECTION_TIME, DEFAULT_DETECTION_TIME ), } self.hass.config_entries.async_update_entry( - self.config_entry, options=options + self.config_entry, data=data, options=options ) async def request_update(self): From cd79720a149daa5e208f8b5cec67a61640ba6600 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Tue, 17 Mar 2020 21:19:57 +0100 Subject: [PATCH 102/431] Introduce safe scan_interval for vicare (#32915) --- homeassistant/components/vicare/climate.py | 4 ++++ homeassistant/components/vicare/water_heater.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index 1b101cc7612..ef5533523f8 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,4 +1,5 @@ """Viessmann ViCare climate device.""" +from datetime import timedelta import logging import requests @@ -79,6 +80,9 @@ HA_TO_VICARE_PRESET_HEATING = { PYVICARE_ERROR = "error" +# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit +SCAN_INTERVAL = timedelta(seconds=900) + def setup_platform(hass, config, add_entities, discovery_info=None): """Create the ViCare climate devices.""" diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index eea3d81faf6..fdac2962739 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,4 +1,5 @@ """Viessmann ViCare water_heater device.""" +from datetime import timedelta import logging import requests @@ -42,6 +43,9 @@ HA_TO_VICARE_HVAC_DHW = { PYVICARE_ERROR = "error" +# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit +SCAN_INTERVAL = timedelta(seconds=900) + def setup_platform(hass, config, add_entities, discovery_info=None): """Create the ViCare water_heater devices.""" From a2ac3352229b98cd3a217d0d374f68c68ada6e17 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 17 Mar 2020 22:15:11 +0100 Subject: [PATCH 103/431] Fix device condition for alarm_control_panel (#32916) --- .../components/alarm_control_panel/device_condition.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/alarm_control_panel/device_condition.py b/homeassistant/components/alarm_control_panel/device_condition.py index 068c665ca5e..c4d43d1b051 100644 --- a/homeassistant/components/alarm_control_panel/device_condition.py +++ b/homeassistant/components/alarm_control_panel/device_condition.py @@ -142,7 +142,7 @@ def async_condition_from_config( """Create a function to test a device condition.""" if config_validation: config = CONDITION_SCHEMA(config) - elif config[CONF_TYPE] == CONDITION_TRIGGERED: + if config[CONF_TYPE] == CONDITION_TRIGGERED: state = STATE_ALARM_TRIGGERED elif config[CONF_TYPE] == CONDITION_DISARMED: state = STATE_ALARM_DISARMED From 52a4c169804c5718917420f163ca06c57e7a3302 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Mar 2020 00:29:12 +0100 Subject: [PATCH 104/431] Improve MQTT light test coverage (#32907) --- .../components/mqtt/light/schema_json.py | 4 +- .../components/mqtt/light/schema_template.py | 3 + tests/components/mqtt/test_light.py | 173 ++++++++- tests/components/mqtt/test_light_json.py | 115 +++++- tests/components/mqtt/test_light_template.py | 331 +++++++++++++++++- 5 files changed, 613 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index 60ecf80fb63..0af5aaf2c76 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -284,7 +284,7 @@ class MqttLightJson( ) except KeyError: pass - except ValueError: + except (TypeError, ValueError): _LOGGER.warning("Invalid brightness value received") if self._color_temp is not None: @@ -300,8 +300,6 @@ class MqttLightJson( self._effect = values["effect"] except KeyError: pass - except ValueError: - _LOGGER.warning("Invalid effect value received") if self._white_value is not None: try: diff --git a/homeassistant/components/mqtt/light/schema_template.py b/homeassistant/components/mqtt/light/schema_template.py index 853e7f4411f..cd3e704f624 100644 --- a/homeassistant/components/mqtt/light/schema_template.py +++ b/homeassistant/components/mqtt/light/schema_template.py @@ -434,6 +434,9 @@ class MqttTemplate( if ATTR_EFFECT in kwargs: values["effect"] = kwargs.get(ATTR_EFFECT) + if self._optimistic: + self._effect = kwargs[ATTR_EFFECT] + if ATTR_FLASH in kwargs: values["flash"] = kwargs.get(ATTR_FLASH) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index 895995e06d9..ba4078f5374 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -335,6 +335,105 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert light_state.attributes.get("xy_color") == (0.672, 0.324) +async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): + """Test handling of empty data via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "white_value_state_topic": "test_light_rgb/white_value/status", + "white_value_command_topic": "test_light_rgb/white_value/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") == 150 + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") == (0, 0) + assert state.attributes.get("white_value") == 255 + assert state.attributes.get("xy_color") == (0.323, 0.329) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "") + assert "Ignoring empty state message" in caplog.text + light_state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") + assert "Ignoring empty brightness message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["brightness"] == 255 + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") + assert "Ignoring empty color temp message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["color_temp"] == 150 + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") + assert "Ignoring empty effect message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "") + assert "Ignoring empty white value message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["white_value"] == 255 + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") + assert "Ignoring empty rgb message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (255, 255, 255) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") + assert "Ignoring empty hs message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "bad,bad") + assert "Failed to parse hs state update" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") + assert "Ignoring empty xy-color message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.323, 0.329) + + async def test_brightness_controlling_scale(hass, mqtt_mock): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): @@ -756,7 +855,7 @@ async def test_show_color_temp_only_if_command_topic(hass, mqtt_mock): async def test_show_effect_only_if_command_topic(hass, mqtt_mock): - """Test the color temp only if a command topic is present.""" + """Test the effect only if a command topic is present.""" config = { light.DOMAIN: { "platform": "mqtt", @@ -1013,6 +1112,78 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) +async def test_on_command_rgb_template(hass, mqtt_mock): + """Test on command in RGB brightness mode with RGB template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgb_command_topic": "test_light/rgb", + "rgb_command_template": "{{ red }}/{{ green }}/{{ blue }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + mock.call("test_light/rgb", "127/127/127", 0, False), + mock.call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_effect(hass, mqtt_mock): + """Test effect.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "effect_command_topic": "test_light/effect/set", + "effect_list": ["rainbow", "colorloop"], + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", effect="rainbow") + + # Should get the following MQTT messages. + # test_light/effect/set: 'rainbow' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + mock.call("test_light/effect/set", "rainbow", 0, False), + mock.call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + async def test_availability_without_topic(hass, mqtt_mock): """Test availability without defined availability topic.""" await help_test_availability_without_topic( diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 412c757e059..6a8bf10dc3d 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -364,6 +364,18 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_ON + await common.async_turn_on(hass, "light.test", color_temp=90) + + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "color_temp": 90}'), + 2, + False, + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + await common.async_turn_off(hass, "light.test") mqtt_mock.async_publish.assert_called_once_with( @@ -666,6 +678,64 @@ async def test_sending_xy_color(hass, mqtt_mock): ) +async def test_effect(hass, mqtt_mock): + """Test for effect being sent when included.""" + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "json", + "name": "test", + "command_topic": "test_light_rgb/set", + "effect": True, + "qos": 0, + } + }, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + + await common.async_turn_on(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", JsonValidator('{"state": "ON"}'), 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "none" + + await common.async_turn_on(hass, "light.test", effect="rainbow") + + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "effect": "rainbow"}'), + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "rainbow" + + await common.async_turn_on(hass, "light.test", effect="colorloop") + + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", + JsonValidator('{"state": "ON", "effect": "colorloop"}'), + 0, + False, + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "colorloop" + + async def test_flash_short_and_long(hass, mqtt_mock): """Test for flash length being sent when included.""" assert await async_setup_component( @@ -792,8 +862,8 @@ async def test_brightness_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 255 -async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): - """Test that invalid color/brightness/white values are ignored.""" +async def test_invalid_values(hass, mqtt_mock): + """Test that invalid color/brightness/white/etc. values are ignored.""" assert await async_setup_component( hass, light.DOMAIN, @@ -805,6 +875,7 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): "state_topic": "test_light_rgb", "command_topic": "test_light_rgb/set", "brightness": True, + "color_temp": True, "rgb": True, "white_value": True, "qos": "0", @@ -814,10 +885,11 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF - assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 185 + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 187 assert state.attributes.get("rgb_color") is None assert state.attributes.get("brightness") is None assert state.attributes.get("white_value") is None + assert state.attributes.get("color_temp") is None assert not state.attributes.get(ATTR_ASSUMED_STATE) # Turn on the light @@ -827,7 +899,9 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): '{"state":"ON",' '"color":{"r":255,"g":255,"b":255},' '"brightness": 255,' - '"white_value": 255}', + '"white_value": 255,' + '"color_temp": 100,' + '"effect": "rainbow"}', ) state = hass.states.get("light.test") @@ -835,8 +909,19 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): assert state.attributes.get("rgb_color") == (255, 255, 255) assert state.attributes.get("brightness") == 255 assert state.attributes.get("white_value") == 255 + assert state.attributes.get("color_temp") == 100 - # Bad color values + # Bad HS color values + async_fire_mqtt_message( + hass, "test_light_rgb", '{"state":"ON",' '"color":{"h":"bad","s":"val"}}', + ) + + # Color should not have changed + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + + # Bad RGB color values async_fire_mqtt_message( hass, "test_light_rgb", @@ -848,6 +933,16 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): assert state.state == STATE_ON assert state.attributes.get("rgb_color") == (255, 255, 255) + # Bad XY color values + async_fire_mqtt_message( + hass, "test_light_rgb", '{"state":"ON",' '"color":{"x":"bad","y":"val"}}', + ) + + # Color should not have changed + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + # Bad brightness values async_fire_mqtt_message( hass, "test_light_rgb", '{"state":"ON",' '"brightness": "badValue"}' @@ -868,6 +963,16 @@ async def test_invalid_color_brightness_and_white_values(hass, mqtt_mock): assert state.state == STATE_ON assert state.attributes.get("white_value") == 255 + # Bad color temperature + async_fire_mqtt_message( + hass, "test_light_rgb", '{"state":"ON",' '"color_temp": "badValue"}' + ) + + # Color temperature should not have changed + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 100 + async def test_availability_without_topic(hass, mqtt_mock): """Test availability without defined availability topic.""" diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index dac15e5ef53..f3965479c14 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -30,7 +30,12 @@ from unittest.mock import patch from homeassistant.components import light, mqtt from homeassistant.components.mqtt.discovery import async_start -from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON +from homeassistant.const import ( + ATTR_ASSUMED_STATE, + ATTR_SUPPORTED_FEATURES, + STATE_OFF, + STATE_ON, +) import homeassistant.core as ha from homeassistant.setup import async_setup_component @@ -60,6 +65,7 @@ from tests.common import ( async_fire_mqtt_message, mock_coro, ) +from tests.components.light import common DEFAULT_CONFIG = { light.DOMAIN: { @@ -269,8 +275,8 @@ async def test_state_brightness_color_effect_temp_white_change_via_topic( assert light_state.attributes.get("effect") == "rainbow" -async def test_optimistic(hass, mqtt_mock): - """Test optimistic mode.""" +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test the sending of command in optimistic mode.""" fake_state = ha.State( "light.test", "on", @@ -320,9 +326,284 @@ async def test_optimistic(hass, mqtt_mock): assert state.attributes.get("white_value") == 50 assert state.attributes.get(ATTR_ASSUMED_STATE) + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "off", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,,--", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + + # Set color_temp + await common.async_turn_on(hass, "light.test", color_temp=70) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,70,,--", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("color_temp") == 70 + + # Set full brightness + await common.async_turn_on(hass, "light.test", brightness=255) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,255,,,--", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 255 + + # Full brightness - no scaling of RGB values sent over MQTT + await common.async_turn_on( + hass, "light.test", rgb_color=[255, 128, 0], white_value=80 + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,80,255-128-0", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 80 + assert state.attributes.get("rgb_color") == (255, 128, 0) + + # Full brightness - normalization of RGB values sent over MQTT + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,,255-127-0", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 127, 0) + + # Set half brightness + await common.async_turn_on(hass, "light.test", brightness=128) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,128,,,--", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 128 + + # Half brightness - scaling of RGB values sent over MQTT + await common.async_turn_on( + hass, "light.test", rgb_color=[0, 255, 128], white_value=40 + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,40,0-128-64", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 40 + assert state.attributes.get("rgb_color") == (0, 255, 128) + + # Half brightness - normalization+scaling of RGB values sent over MQTT + await common.async_turn_on( + hass, "light.test", rgb_color=[0, 32, 16], white_value=40 + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,40,0-128-64", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("white_value") == 40 + assert state.attributes.get("rgb_color") == (0, 255, 127) + + +async def test_sending_mqtt_commands_non_optimistic_brightness_template( + hass, mqtt_mock +): + """Test the sending of command in optimistic mode.""" + with assert_setup_component(1, light.DOMAIN): + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "template", + "name": "test", + "effect_list": ["rainbow", "colorloop"], + "state_topic": "test_light_rgb", + "command_topic": "test_light_rgb/set", + "command_on_template": "on," + "{{ brightness|d }}," + "{{ color_temp|d }}," + "{{ white_value|d }}," + "{{ red|d }}-" + "{{ green|d }}-" + "{{ blue|d }}", + "command_off_template": "off", + "state_template": '{{ value.split(",")[0] }}', + "brightness_template": '{{ value.split(",")[1] }}', + "color_temp_template": '{{ value.split(",")[2] }}', + "white_value_template": '{{ value.split(",")[3] }}', + "red_template": '{{ value.split(",")[4].' 'split("-")[0] }}', + "green_template": '{{ value.split(",")[4].' 'split("-")[1] }}', + "blue_template": '{{ value.split(",")[4].' 'split("-")[2] }}', + "effect_template": '{{ value.split(",")[5] }}', + } + }, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get("brightness") + assert not state.attributes.get("hs_color") + assert not state.attributes.get("effect") + assert not state.attributes.get("color_temp") + assert not state.attributes.get("white_value") + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "off", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,,--", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + # Set color_temp + await common.async_turn_on(hass, "light.test", color_temp=70) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,70,,--", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get("color_temp") + + # Set full brightness + await common.async_turn_on(hass, "light.test", brightness=255) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,255,,,--", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get("brightness") + + # Full brightness - no scaling of RGB values sent over MQTT + await common.async_turn_on( + hass, "light.test", rgb_color=[255, 128, 0], white_value=80 + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,80,255-128-0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert not state.attributes.get("white_value") + assert not state.attributes.get("rgb_color") + + # Full brightness - normalization of RGB values sent over MQTT + await common.async_turn_on(hass, "light.test", rgb_color=[128, 64, 0]) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,,255-127-0", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Set half brightness + await common.async_turn_on(hass, "light.test", brightness=128) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,128,,,--", 0, False + ) + mqtt_mock.async_publish.reset_mock() + + # Half brightness - no scaling of RGB values sent over MQTT + await common.async_turn_on( + hass, "light.test", rgb_color=[0, 255, 128], white_value=40 + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,40,0-255-128", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + + # Half brightness - normalization but no scaling of RGB values sent over MQTT + await common.async_turn_on( + hass, "light.test", rgb_color=[0, 32, 16], white_value=40 + ) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,,,40,0-255-127", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + + +async def test_effect(hass, mqtt_mock): + """Test effect sent over MQTT in optimistic mode.""" + with assert_setup_component(1, light.DOMAIN): + assert await async_setup_component( + hass, + light.DOMAIN, + { + light.DOMAIN: { + "platform": "mqtt", + "schema": "template", + "effect_list": ["rainbow", "colorloop"], + "name": "test", + "command_topic": "test_light_rgb/set", + "command_on_template": "on,{{ effect }}", + "command_off_template": "off", + "qos": 0, + } + }, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 44 + + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert not state.attributes.get("effect") + + await common.async_turn_on(hass, "light.test", effect="rainbow") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,rainbow", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "rainbow" + + await common.async_turn_on(hass, "light.test", effect="colorloop") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,colorloop", 0, False + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "colorloop" + async def test_flash(hass, mqtt_mock): - """Test flash.""" + """Test flash sent over MQTT in optimistic mode.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( hass, @@ -342,6 +623,30 @@ async def test_flash(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + + await common.async_turn_on(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + + await common.async_turn_on(hass, "light.test", flash="short") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,short", 0, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + + await common.async_turn_on(hass, "light.test", flash="long") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,long", 0, False + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON async def test_transition(hass, mqtt_mock): @@ -358,6 +663,7 @@ async def test_transition(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "command_on_template": "on,{{ transition }}", "command_off_template": "off,{{ transition|d }}", + "qos": 1, } }, ) @@ -365,6 +671,23 @@ async def test_transition(hass, mqtt_mock): state = hass.states.get("light.test") assert state.state == STATE_OFF + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == 40 + + await common.async_turn_on(hass, "light.test", transition=10) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "on,10", 1, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + + await common.async_turn_off(hass, "light.test", transition=20) + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "off,20", 1, False + ) + state = hass.states.get("light.test") + assert state.state == STATE_OFF + async def test_invalid_values(hass, mqtt_mock): """Test that invalid values are ignored.""" From 505de3dce384369bd821c77c1b19d0d68c78facb Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 18 Mar 2020 00:49:50 +0100 Subject: [PATCH 105/431] Add attributes and availability to MQTT camera (#32908) --- homeassistant/components/mqtt/camera.py | 36 +++++++++----- tests/components/mqtt/test_camera.py | 64 +++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/mqtt/camera.py b/homeassistant/components/mqtt/camera.py index 9bbb1503196..a75ae33f861 100644 --- a/homeassistant/components/mqtt/camera.py +++ b/homeassistant/components/mqtt/camera.py @@ -4,7 +4,7 @@ import logging import voluptuous as vol from homeassistant.components import camera, mqtt -from homeassistant.components.camera import PLATFORM_SCHEMA, Camera +from homeassistant.components.camera import Camera from homeassistant.const import CONF_DEVICE, CONF_NAME from homeassistant.core import callback from homeassistant.helpers import config_validation as cv @@ -13,7 +13,10 @@ from homeassistant.helpers.typing import ConfigType, HomeAssistantType from . import ( ATTR_DISCOVERY_HASH, + CONF_QOS, CONF_UNIQUE_ID, + MqttAttributes, + MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, subscription, @@ -25,13 +28,17 @@ _LOGGER = logging.getLogger(__name__) CONF_TOPIC = "topic" DEFAULT_NAME = "MQTT Camera" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, - vol.Optional(CONF_UNIQUE_ID): cv.string, - vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, - } +PLATFORM_SCHEMA = ( + mqtt.MQTT_BASE_PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_DEVICE): mqtt.MQTT_ENTITY_DEVICE_INFO_SCHEMA, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_TOPIC): mqtt.valid_subscribe_topic, + vol.Optional(CONF_UNIQUE_ID): cv.string, + } + ) + .extend(mqtt.MQTT_AVAILABILITY_SCHEMA.schema) + .extend(mqtt.MQTT_JSON_ATTRS_SCHEMA.schema) ) @@ -69,7 +76,9 @@ async def _async_setup_entity( async_add_entities([MqttCamera(config, config_entry, discovery_data)]) -class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): +class MqttCamera( + MqttAttributes, MqttAvailability, MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera +): """representation of a MQTT camera.""" def __init__(self, config, config_entry, discovery_data): @@ -78,12 +87,13 @@ class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): self._unique_id = config.get(CONF_UNIQUE_ID) self._sub_state = None - self._qos = 0 self._last_image = None device_config = config.get(CONF_DEVICE) Camera.__init__(self) + MqttAttributes.__init__(self, config) + MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) MqttEntityDeviceInfo.__init__(self, device_config, config_entry) @@ -96,6 +106,8 @@ class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): """Handle updated discovery message.""" config = PLATFORM_SCHEMA(discovery_payload) self._config = config + await self.attributes_discovery_update(config) + await self.availability_discovery_update(config) await self.device_info_discovery_update(config) await self._subscribe_topics() self.async_write_ha_state() @@ -115,7 +127,7 @@ class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): "state_topic": { "topic": self._config[CONF_TOPIC], "msg_callback": message_received, - "qos": self._qos, + "qos": self._config[CONF_QOS], "encoding": None, } }, @@ -126,6 +138,8 @@ class MqttCamera(MqttDiscoveryUpdate, MqttEntityDeviceInfo, Camera): self._sub_state = await subscription.async_unsubscribe_topics( self.hass, self._sub_state ) + await MqttAttributes.async_will_remove_from_hass(self) + await MqttAvailability.async_will_remove_from_hass(self) await MqttDiscoveryUpdate.async_will_remove_from_hass(self) async def async_camera_image(self): diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index f77e5945ae5..96ea9b3005f 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -6,15 +6,23 @@ from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component from .common import ( + 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_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, + help_test_setting_attribute_via_mqtt_json_message, + help_test_setting_attribute_with_template, help_test_unique_id, + help_test_update_with_json_attrs_bad_JSON, + help_test_update_with_json_attrs_not_dict, ) from tests.common import ( @@ -49,6 +57,62 @@ async def test_run_camera_setup(hass, aiohttp_client): assert body == "beer" +async def test_availability_without_topic(hass, mqtt_mock): + """Test availability without defined availability topic.""" + await help_test_availability_without_topic( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_default_availability_payload(hass, mqtt_mock): + """Test availability by default payload with defined topic.""" + await help_test_default_availability_payload( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_custom_availability_payload(hass, mqtt_mock): + """Test availability by custom payload with defined topic.""" + await help_test_custom_availability_payload( + hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG + ) + + +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, camera.DOMAIN, DEFAULT_CONFIG + ) + + +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, camera.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, camera.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, camera.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, camera.DOMAIN, DEFAULT_CONFIG + ) + + async def test_unique_id(hass): """Test unique id option only creates one camera per unique_id.""" config = { From a9d16d4276adb82fc59dee93b7b443d8423842cf Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Mar 2020 16:57:10 -0700 Subject: [PATCH 106/431] Remove mentioning YAML in Roku config flow (#32920) --- homeassistant/components/roku/strings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/roku/strings.json b/homeassistant/components/roku/strings.json index 2beba3433b5..17072850259 100644 --- a/homeassistant/components/roku/strings.json +++ b/homeassistant/components/roku/strings.json @@ -12,7 +12,7 @@ }, "ssdp_confirm": { "title": "Roku", - "description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.", + "description": "Do you want to set up {name}?", "data": {} } }, From 82c8f18bc7dc527a58a41f07997a391b1e1e6372 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 17 Mar 2020 17:54:57 -0700 Subject: [PATCH 107/431] Fix hassio panel load (#32922) * Fix loading hassio panel * Remove blacklist --- homeassistant/components/hassio/__init__.py | 19 +++++++++---------- homeassistant/setup.py | 8 -------- tests/test_setup.py | 13 ------------- 3 files changed, 9 insertions(+), 31 deletions(-) diff --git a/homeassistant/components/hassio/__init__.py b/homeassistant/components/hassio/__init__.py index cc03f26085c..bcb751faa64 100644 --- a/homeassistant/components/hassio/__init__.py +++ b/homeassistant/components/hassio/__init__.py @@ -190,16 +190,15 @@ async def async_setup(hass, config): hass.http.register_view(HassIOView(host, websession)) - if "frontend" in hass.config.components: - 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, - ) + 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, + ) await hassio.update_hass_api(config.get("http", {}), refresh_token) diff --git a/homeassistant/setup.py b/homeassistant/setup.py index 5c3d2bcd7fd..40d767728d3 100644 --- a/homeassistant/setup.py +++ b/homeassistant/setup.py @@ -20,8 +20,6 @@ DATA_DEPS_REQS = "deps_reqs_processed" SLOW_SETUP_WARNING = 10 -BLACKLIST = set(["auto_backup"]) - def setup_component(hass: core.HomeAssistant, domain: str, config: ConfigType) -> bool: """Set up a component and all its dependencies.""" @@ -40,12 +38,6 @@ async def async_setup_component( if domain in hass.config.components: return True - if domain in BLACKLIST: - _LOGGER.error( - "Integration %s is blacklisted because it is causing issues.", domain - ) - return False - setup_tasks = hass.data.setdefault(DATA_SETUP, {}) if domain in setup_tasks: diff --git a/tests/test_setup.py b/tests/test_setup.py index 95fd1e0a15d..f90a7269752 100644 --- a/tests/test_setup.py +++ b/tests/test_setup.py @@ -6,7 +6,6 @@ import os import threading from unittest import mock -from asynctest import patch import voluptuous as vol from homeassistant import setup @@ -536,15 +535,3 @@ async def test_setup_import_blows_up(hass): "homeassistant.loader.Integration.get_component", side_effect=ValueError ): assert not await setup.async_setup_component(hass, "sun", {}) - - -async def test_blacklist(caplog): - """Test setup blacklist.""" - with patch("homeassistant.setup.BLACKLIST", {"bad_integration"}): - assert not await setup.async_setup_component( - mock.Mock(config=mock.Mock(components=[])), "bad_integration", {} - ) - assert ( - "Integration bad_integration is blacklisted because it is causing issues." - in caplog.text - ) From f02c5f66d617d4758bd6311fcb9749f2b0b18ddd Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Tue, 17 Mar 2020 17:58:05 -0700 Subject: [PATCH 108/431] Clean up Abode unused automation events (#32825) --- homeassistant/components/abode/__init__.py | 19 +++---------------- homeassistant/components/abode/switch.py | 7 ++----- 2 files changed, 5 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index 666c8481bfb..687d0d31263 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -263,7 +263,6 @@ def setup_abode_events(hass): TIMELINE.TEST_GROUP, TIMELINE.CAPTURE_GROUP, TIMELINE.DEVICE_GROUP, - TIMELINE.AUTOMATION_EDIT_GROUP, ] for event in events: @@ -343,21 +342,14 @@ class AbodeDevice(Entity): class AbodeAutomation(Entity): """Representation of an Abode automation.""" - def __init__(self, data, automation, event=None): + def __init__(self, data, automation): """Initialize for Abode automation.""" self._data = data self._automation = automation - self._event = event async def async_added_to_hass(self): - """Subscribe to a group of Abode timeline events.""" - if self._event: - self.hass.async_add_job( - self._data.abode.events.add_event_callback, - self._event, - self._update_callback, - ) - self.hass.data[DOMAIN].entity_ids.add(self.entity_id) + """Set up automation entity.""" + self.hass.data[DOMAIN].entity_ids.add(self.entity_id) @property def should_poll(self): @@ -385,8 +377,3 @@ class AbodeAutomation(Entity): def unique_id(self): """Return a unique ID to use for this automation.""" return self._automation.automation_id - - def _update_callback(self, device): - """Update the automation state.""" - self._automation.refresh() - self.schedule_update_ha_state() diff --git a/homeassistant/components/abode/switch.py b/homeassistant/components/abode/switch.py index e29deb72f82..b57f3fbe143 100644 --- a/homeassistant/components/abode/switch.py +++ b/homeassistant/components/abode/switch.py @@ -1,6 +1,5 @@ """Support for Abode Security System switches.""" import abodepy.helpers.constants as CONST -import abodepy.helpers.timeline as TIMELINE from homeassistant.components.switch import SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -24,9 +23,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(AbodeSwitch(data, device)) for automation in data.abode.get_automations(): - entities.append( - AbodeAutomationSwitch(data, automation, TIMELINE.AUTOMATION_EDIT_GROUP) - ) + entities.append(AbodeAutomationSwitch(data, automation)) async_add_entities(entities) @@ -52,7 +49,7 @@ class AbodeAutomationSwitch(AbodeAutomation, SwitchDevice): """A switch implementation for Abode automations.""" async def async_added_to_hass(self): - """Subscribe Abode events.""" + """Set up trigger automation service.""" await super().async_added_to_hass() signal = f"abode_trigger_automation_{self.entity_id}" From 609263e1bbc7b695c023c16604c4a8a7f648b284 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 17 Mar 2020 21:03:16 -0600 Subject: [PATCH 109/431] Add cleanup to OpenUV (#32886) * Add cleanup to OpenUV * Linting --- homeassistant/components/openuv/__init__.py | 33 +++++++++++++++++-- .../components/openuv/binary_sensor.py | 22 ++----------- homeassistant/components/openuv/sensor.py | 22 ++----------- 3 files changed, 35 insertions(+), 42 deletions(-) diff --git a/homeassistant/components/openuv/__init__.py b/homeassistant/components/openuv/__init__.py index f130872da5f..008b46d96f2 100644 --- a/homeassistant/components/openuv/__init__.py +++ b/homeassistant/components/openuv/__init__.py @@ -16,9 +16,13 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SENSORS, ) +from homeassistant.core import callback from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv -from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.service import verify_domain_control @@ -104,7 +108,6 @@ async def async_setup(hass, config): async def async_setup_entry(hass, config_entry): """Set up OpenUV as config entry.""" - _verify_domain_control = verify_domain_control(hass, DOMAIN) try: @@ -230,6 +233,7 @@ class OpenUvEntity(Entity): def __init__(self, openuv): """Initialize.""" + self._async_unsub_dispatcher_connect = None self._attrs = {ATTR_ATTRIBUTION: DEFAULT_ATTRIBUTION} self._available = True self._name = None @@ -249,3 +253,28 @@ class OpenUvEntity(Entity): def name(self): """Return the name of the entity.""" return self._name + + async def async_added_to_hass(self): + """Register callbacks.""" + + @callback + def update(): + """Update the state.""" + self.update_from_latest_data() + self.async_write_ha_state() + + self._async_unsub_dispatcher_connect = async_dispatcher_connect( + self.hass, TOPIC_UPDATE, update + ) + + self.update_from_latest_data() + + async def async_will_remove_from_hass(self): + """Disconnect dispatcher listener when removed.""" + if self._async_unsub_dispatcher_connect: + self._async_unsub_dispatcher_connect() + self._async_unsub_dispatcher_connect = None + + def update_from_latest_data(self): + """Update the sensor using the latest data.""" + raise NotImplementedError diff --git a/homeassistant/components/openuv/binary_sensor.py b/homeassistant/components/openuv/binary_sensor.py index 6bd3dda13fd..6e403a59b43 100644 --- a/homeassistant/components/openuv/binary_sensor.py +++ b/homeassistant/components/openuv/binary_sensor.py @@ -3,14 +3,12 @@ import logging from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_local, parse_datetime, utcnow from . import ( DATA_OPENUV_CLIENT, DATA_PROTECTION_WINDOW, DOMAIN, - TOPIC_UPDATE, TYPE_PROTECTION_WINDOW, OpenUvEntity, ) @@ -75,24 +73,8 @@ class OpenUvBinarySensor(OpenUvEntity, BinarySensorDevice): """Return a unique, Home Assistant friendly identifier for this entity.""" return f"{self._latitude}_{self._longitude}_{self._sensor_type}" - async def async_added_to_hass(self): - """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() - - async def async_update(self): + @callback + def update_from_latest_data(self): """Update the state.""" data = self.openuv.data[DATA_PROTECTION_WINDOW] diff --git a/homeassistant/components/openuv/sensor.py b/homeassistant/components/openuv/sensor.py index a375cfa10d7..0d4a8b73a08 100644 --- a/homeassistant/components/openuv/sensor.py +++ b/homeassistant/components/openuv/sensor.py @@ -3,14 +3,12 @@ import logging from homeassistant.const import TIME_MINUTES from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.util.dt import as_local, parse_datetime from . import ( DATA_OPENUV_CLIENT, DATA_UV, DOMAIN, - TOPIC_UPDATE, TYPE_CURRENT_OZONE_LEVEL, TYPE_CURRENT_UV_INDEX, TYPE_CURRENT_UV_LEVEL, @@ -135,24 +133,8 @@ class OpenUvSensor(OpenUvEntity): """Return the unit the value is expressed in.""" return self._unit - async def async_added_to_hass(self): - """Register callbacks.""" - - @callback - def update(): - """Update the state.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_dispatcher_connect = async_dispatcher_connect( - self.hass, TOPIC_UPDATE, update - ) - - async def async_will_remove_from_hass(self): - """Disconnect dispatcher listener when removed.""" - if self._async_unsub_dispatcher_connect: - self._async_unsub_dispatcher_connect() - - async def async_update(self): + @callback + def update_from_latest_data(self): """Update the state.""" data = self.openuv.data[DATA_UV].get("result") From c1ceab09e5281dcf6b42592bb2e35cb6518f4885 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 18 Mar 2020 04:21:56 +0100 Subject: [PATCH 110/431] Migrate Brother to use DataUpdateCoordinator (#32800) * Use DataCoordinator to update Brother data * Simplify error text * Improve tests * Rename DEFAULT_SCAN_INTERVAL to SCAN_INTERVAL * Add entity_registry_enabled_default property * Add quality_scale to manifest.json file * Remove misleading coordinator.data.data --- .coveragerc | 3 - homeassistant/components/brother/__init__.py | 59 ++++----- .../components/brother/manifest.json | 3 +- homeassistant/components/brother/sensor.py | 61 ++++++---- tests/components/brother/test_init.py | 112 ++++++++++++++++++ tests/fixtures/brother_printer_data.json | 18 ++- 6 files changed, 188 insertions(+), 68 deletions(-) create mode 100644 tests/components/brother/test_init.py diff --git a/.coveragerc b/.coveragerc index 555dccadde7..a1ad48e1d22 100644 --- a/.coveragerc +++ b/.coveragerc @@ -84,9 +84,6 @@ omit = homeassistant/components/broadlink/remote.py homeassistant/components/broadlink/sensor.py homeassistant/components/broadlink/switch.py - homeassistant/components/brother/__init__.py - homeassistant/components/brother/sensor.py - homeassistant/components/brother/const.py homeassistant/components/brottsplatskartan/sensor.py homeassistant/components/browser/* homeassistant/components/brunt/cover.py diff --git a/homeassistant/components/brother/__init__.py b/homeassistant/components/brother/__init__.py index ada740c5f10..5daf54a568c 100644 --- a/homeassistant/components/brother/__init__.py +++ b/homeassistant/components/brother/__init__.py @@ -9,20 +9,19 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_TYPE from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN PLATFORMS = ["sensor"] -DEFAULT_SCAN_INTERVAL = timedelta(seconds=30) +SCAN_INTERVAL = timedelta(seconds=30) _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: Config): """Set up the Brother component.""" - hass.data[DOMAIN] = {} return True @@ -31,14 +30,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): host = entry.data[CONF_HOST] kind = entry.data[CONF_TYPE] - brother = BrotherPrinterData(host, kind) + coordinator = BrotherDataUpdateCoordinator(hass, host=host, kind=kind) + await coordinator.async_refresh() - await brother.async_update() + if not coordinator.last_update_success: + raise ConfigEntryNotReady - if not brother.available: - raise ConfigEntryNotReady() - - hass.data[DOMAIN][entry.entry_id] = brother + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][entry.entry_id] = coordinator for component in PLATFORMS: hass.async_create_task( @@ -64,39 +63,21 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -class BrotherPrinterData: - """Define an object to hold sensor data.""" +class BrotherDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching Brother data from the printer.""" - def __init__(self, host, kind): + def __init__(self, hass, host, kind): """Initialize.""" - self._brother = Brother(host, kind=kind) - self.host = host - self.model = None - self.serial = None - self.firmware = None - self.available = False - self.data = {} - self.unavailable_logged = False + self.brother = Brother(host, kind=kind) - @Throttle(DEFAULT_SCAN_INTERVAL) - async def async_update(self): + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self): """Update data via library.""" try: - await self._brother.async_update() + await self.brother.async_update() except (ConnectionError, SnmpError, UnsupportedModel) as error: - if not self.unavailable_logged: - _LOGGER.error( - "Could not fetch data from %s, error: %s", self.host, error - ) - self.unavailable_logged = True - self.available = self._brother.available - return - - self.model = self._brother.model - self.serial = self._brother.serial - self.firmware = self._brother.firmware - self.available = self._brother.available - self.data = self._brother.data - if self.available and self.unavailable_logged: - _LOGGER.info("Printer %s is available again", self.host) - self.unavailable_logged = False + raise UpdateFailed(error) + return self.brother.data diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 4528e3e6d1f..24150c513df 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -6,5 +6,6 @@ "codeowners": ["@bieniu"], "requirements": ["brother==0.1.9"], "zeroconf": ["_printer._tcp.local."], - "config_flow": true + "config_flow": true, + "quality_scale": "platinum" } diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index e118e65e9a5..aa108bf0ac7 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -28,54 +28,55 @@ from .const import ( ) ATTR_COUNTER = "counter" +ATTR_FIRMWARE = "firmware" +ATTR_MODEL = "model" ATTR_REMAINING_PAGES = "remaining_pages" +ATTR_SERIAL = "serial" _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Add Brother entities from a config_entry.""" - brother = hass.data[DOMAIN][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id] sensors = [] - name = brother.model device_info = { - "identifiers": {(DOMAIN, brother.serial)}, - "name": brother.model, + "identifiers": {(DOMAIN, coordinator.data[ATTR_SERIAL])}, + "name": coordinator.data[ATTR_MODEL], "manufacturer": ATTR_MANUFACTURER, - "model": brother.model, - "sw_version": brother.firmware, + "model": coordinator.data[ATTR_MODEL], + "sw_version": coordinator.data.get(ATTR_FIRMWARE), } for sensor in SENSOR_TYPES: - if sensor in brother.data: - sensors.append(BrotherPrinterSensor(brother, name, sensor, device_info)) - async_add_entities(sensors, True) + if sensor in coordinator.data: + sensors.append(BrotherPrinterSensor(coordinator, sensor, device_info)) + async_add_entities(sensors, False) class BrotherPrinterSensor(Entity): """Define an Brother Printer sensor.""" - def __init__(self, printer, name, kind, device_info): + def __init__(self, coordinator, kind, device_info): """Initialize.""" - self.printer = printer - self._name = name + self._name = f"{coordinator.data[ATTR_MODEL]} {SENSOR_TYPES[kind][ATTR_LABEL]}" + self._unique_id = f"{coordinator.data[ATTR_SERIAL].lower()}_{kind}" self._device_info = device_info - self._unique_id = f"{self.printer.serial.lower()}_{kind}" + self.coordinator = coordinator self.kind = kind - self._state = None self._attrs = {} @property def name(self): """Return the name.""" - return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + return self._name @property def state(self): """Return the state.""" - return self._state + return self.coordinator.data.get(self.kind) @property def device_state_attributes(self): @@ -98,8 +99,10 @@ class BrotherPrinterSensor(Entity): remaining_pages = ATTR_YELLOW_DRUM_REMAINING_PAGES drum_counter = ATTR_YELLOW_DRUM_COUNTER if remaining_pages and drum_counter: - self._attrs[ATTR_REMAINING_PAGES] = self.printer.data.get(remaining_pages) - self._attrs[ATTR_COUNTER] = self.printer.data.get(drum_counter) + self._attrs[ATTR_REMAINING_PAGES] = self.coordinator.data.get( + remaining_pages + ) + self._attrs[ATTR_COUNTER] = self.coordinator.data.get(drum_counter) return self._attrs @property @@ -120,15 +123,27 @@ class BrotherPrinterSensor(Entity): @property def available(self): """Return True if entity is available.""" - return self.printer.available + return self.coordinator.last_update_success + + @property + def should_poll(self): + """Return the polling requirement of the entity.""" + return False @property def device_info(self): """Return the device info.""" return self._device_info - async def async_update(self): - """Update the data from printer.""" - await self.printer.async_update() + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return True - self._state = self.printer.data.get(self.kind) + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py new file mode 100644 index 00000000000..ea9a255f75d --- /dev/null +++ b/tests/components/brother/test_init.py @@ -0,0 +1,112 @@ +"""Test init of Brother integration.""" +from datetime import timedelta +import json + +from asynctest import patch +import pytest + +import homeassistant.components.brother as brother +from homeassistant.components.brother.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.util.dt import utcnow + +from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="HL-L2340DW 0123456789", + data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, + ) + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "waiting" + + +async def test_config_not_ready(hass): + """Test for setup failure if connection to broker is missing.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="HL-L2340DW 0123456789", + data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, + ) + with patch( + "brother.Brother._get_data", side_effect=ConnectionError() + ), pytest.raises(ConfigEntryNotReady): + await brother.async_setup_entry(hass, entry) + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="HL-L2340DW 0123456789", + data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, + ) + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.data[DOMAIN][entry.entry_id] + + assert await hass.config_entries.async_unload(entry.entry_id) + assert not hass.data[DOMAIN] + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when device is offline.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="HL-L2340DW 0123456789", + data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, + ) + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "waiting" + + future = utcnow() + timedelta(minutes=5) + with patch("brother.Brother._get_data", side_effect=ConnectionError()): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state is not None + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=10) + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "waiting" diff --git a/tests/fixtures/brother_printer_data.json b/tests/fixtures/brother_printer_data.json index d1b631d7548..70e7add3c10 100644 --- a/tests/fixtures/brother_printer_data.json +++ b/tests/fixtures/brother_printer_data.json @@ -8,10 +8,24 @@ "31010400000001", "6f010400001d4c", "81010400000050", - "8601040000000a" + "8601040000000a", + "7e01040000064b", + "7301040000064b", + "7401040000064b", + "7501040000064b", + "790104000023f0", + "7a0104000023f0", + "7b0104000023f0", + "800104000023f0" ], "1.3.6.1.4.1.2435.2.3.9.1.1.7.0": "MFG:Brother;CMD:PJL,HBP,URF;MDL:HL-L2340DW series;CLS:PRINTER;CID:Brother Laser Type1;URF:W8,CP1,IS4-1,MT1-3-4-5-8,OB10,PQ4,RS300-600,V1.3,DM1;", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.11.0": ["82010400002b06"], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.11.0": [ + "82010400002b06", + "a4010400004005", + "a5010400004005", + "a6010400004005", + "a7010400004005" + ], "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.1.0": "0123456789", "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING " } \ No newline at end of file From 4517f0d59ab20735c3cae760803c117241c044bb Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 18 Mar 2020 11:10:40 +0100 Subject: [PATCH 111/431] Handle unique WLED device using zeroconf properties (#32897) --- homeassistant/components/wled/config_flow.py | 37 +++++++++++--------- tests/components/wled/test_config_flow.py | 30 +++++++++++++--- 2 files changed, 46 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index dbcd55a7b17..da1193b1a01 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -44,7 +44,12 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( - {CONF_HOST: host, CONF_NAME: name, "title_placeholders": {"name": name}} + { + CONF_HOST: host, + CONF_NAME: name, + CONF_MAC: user_input["properties"].get(CONF_MAC), + "title_placeholders": {"name": name}, + } ) # Prepare configuration flow @@ -72,23 +77,22 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): if source == SOURCE_ZEROCONF: # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 user_input[CONF_HOST] = self.context.get(CONF_HOST) + user_input[CONF_MAC] = self.context.get(CONF_MAC) - errors = {} - session = async_get_clientsession(self.hass) - wled = WLED(user_input[CONF_HOST], session=session) - - try: - device = await wled.update() - except WLEDConnectionError: - if source == SOURCE_ZEROCONF: - return self.async_abort(reason="connection_error") - errors["base"] = "connection_error" - return self._show_setup_form(errors) + if user_input.get(CONF_MAC) is None or not prepare: + session = async_get_clientsession(self.hass) + wled = WLED(user_input[CONF_HOST], session=session) + try: + device = await wled.update() + except WLEDConnectionError: + if source == SOURCE_ZEROCONF: + return self.async_abort(reason="connection_error") + return self._show_setup_form({"base": "connection_error"}) + user_input[CONF_MAC] = device.info.mac_address # Check if already configured - mac_address = device.info.mac_address - await self.async_set_unique_id(device.info.mac_address) - self._abort_if_unique_id_configured() + await self.async_set_unique_id(user_input[CONF_MAC]) + self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) title = user_input[CONF_HOST] if source == SOURCE_ZEROCONF: @@ -99,7 +103,8 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return await self.async_step_zeroconf_confirm() return self.async_create_entry( - title=title, data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: mac_address} + title=title, + data={CONF_HOST: user_input[CONF_HOST], CONF_MAC: user_input[CONF_MAC]}, ) def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 4a43706dde2..521a7b67a46 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -48,7 +48,9 @@ async def test_show_zerconf_form( flow = config_flow.WLEDFlowHandler() flow.hass = hass flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf({"hostname": "example.local."}) + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "properties": {}} + ) assert flow.context[CONF_HOST] == "example.local" assert flow.context[CONF_NAME] == "example" @@ -83,7 +85,7 @@ async def test_zeroconf_connection_error( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local."}, + data={"hostname": "example.local.", "properties": {}}, ) assert result["reason"] == "connection_error" @@ -103,7 +105,7 @@ async def test_zeroconf_confirm_connection_error( CONF_HOST: "example.com", CONF_NAME: "test", }, - data={"hostname": "example.com."}, + data={"hostname": "example.com.", "properties": {}}, ) assert result["reason"] == "connection_error" @@ -147,7 +149,23 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local."}, + data={"hostname": "example.local.", "properties": {}}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_with_mac_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"hostname": "example.local.", "properties": {CONF_MAC: "aabbccddeeff"}}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -194,7 +212,9 @@ async def test_full_zeroconf_flow_implementation( flow = config_flow.WLEDFlowHandler() flow.hass = hass flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf({"hostname": "example.local."}) + result = await flow.async_step_zeroconf( + {"hostname": "example.local.", "properties": {}} + ) assert flow.context[CONF_HOST] == "example.local" assert flow.context[CONF_NAME] == "example" From 4e4fd90455dd095f47c5ab7eadf96260624bd4ac Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 18 Mar 2020 11:55:09 +0100 Subject: [PATCH 112/431] Upgrade shodan to 1.22.0 (#32928) --- homeassistant/components/shodan/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/shodan/manifest.json b/homeassistant/components/shodan/manifest.json index 1b04da721b1..86006191942 100644 --- a/homeassistant/components/shodan/manifest.json +++ b/homeassistant/components/shodan/manifest.json @@ -2,7 +2,7 @@ "domain": "shodan", "name": "Shodan", "documentation": "https://www.home-assistant.io/integrations/shodan", - "requirements": ["shodan==1.21.3"], + "requirements": ["shodan==1.22.0"], "dependencies": [], "codeowners": ["@fabaff"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5b7e4438ed8..e2b882e42f9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1846,7 +1846,7 @@ sentry-sdk==0.13.5 sharp_aquos_rc==0.3.2 # homeassistant.components.shodan -shodan==1.21.3 +shodan==1.22.0 # homeassistant.components.sighthound simplehound==0.3 From 7d23a734fc6432f5d208d4dbd47251791c5d69cf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 18 Mar 2020 13:02:27 +0100 Subject: [PATCH 113/431] Updated frontend to 20200318.0 (#32931) --- 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 174bab5a189..fc9cd188565 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==20200316.1" + "home-assistant-frontend==20200318.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index f4b15cdec87..cc1486944b4 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200316.1 +home-assistant-frontend==20200318.0 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index e2b882e42f9..962e5d2bc45 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -696,7 +696,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200316.1 +home-assistant-frontend==20200318.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index baa3b4dfffb..297f3d8f173 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200316.1 +home-assistant-frontend==20200318.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 From 7c79adad8f7e047a578ac1db4fa180029d4abb56 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 18 Mar 2020 15:12:55 +0000 Subject: [PATCH 114/431] Refactor and simplify homekit_controller entity setup (#32927) --- .../components/homekit_controller/__init__.py | 8 -- .../components/homekit_controller/climate.py | 102 +++++++----------- .../components/homekit_controller/cover.py | 38 ++++--- .../components/homekit_controller/fan.py | 27 +++-- .../components/homekit_controller/light.py | 28 ++--- .../homekit_controller/manifest.json | 2 +- .../homekit_controller/media_player.py | 82 +++++++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 9 files changed, 131 insertions(+), 160 deletions(-) diff --git a/homeassistant/components/homekit_controller/__init__.py b/homeassistant/components/homekit_controller/__init__.py index 2089471f288..cf1cf28fc32 100644 --- a/homeassistant/components/homekit_controller/__init__.py +++ b/homeassistant/components/homekit_controller/__init__.py @@ -132,14 +132,6 @@ class HomeKitEntity(Entity): if CharacteristicPermissions.events in char.perms: self.watchable_characteristics.append((self._aid, char.iid)) - # Callback to allow entity to configure itself based on this - # characteristics metadata (valid values, value ranges, features, etc) - setup_fn_name = escape_characteristic_name(char.type_name) - setup_fn = getattr(self, f"_setup_{setup_fn_name}", None) - if not setup_fn: - return - setup_fn(char.to_accessory_and_service_list()) - @property def unique_id(self) -> str: """Return the ID of this device.""" diff --git a/homeassistant/components/homekit_controller/climate.py b/homeassistant/components/homekit_controller/climate.py index 133c100b125..2262fa54770 100644 --- a/homeassistant/components/homekit_controller/climate.py +++ b/homeassistant/components/homekit_controller/climate.py @@ -1,7 +1,12 @@ """Support for Homekit climate devices.""" import logging -from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics import ( + CharacteristicsTypes, + HeatingCoolingCurrentValues, + HeatingCoolingTargetValues, +) +from aiohomekit.utils import clamp_enum_to_char from homeassistant.components.climate import ( DEFAULT_MAX_HUMIDITY, @@ -28,21 +33,19 @@ _LOGGER = logging.getLogger(__name__) # Map of Homekit operation modes to hass modes MODE_HOMEKIT_TO_HASS = { - 0: HVAC_MODE_OFF, - 1: HVAC_MODE_HEAT, - 2: HVAC_MODE_COOL, - 3: HVAC_MODE_HEAT_COOL, + HeatingCoolingTargetValues.OFF: HVAC_MODE_OFF, + HeatingCoolingTargetValues.HEAT: HVAC_MODE_HEAT, + HeatingCoolingTargetValues.COOL: HVAC_MODE_COOL, + HeatingCoolingTargetValues.AUTO: HVAC_MODE_HEAT_COOL, } # Map of hass operation modes to homekit modes MODE_HASS_TO_HOMEKIT = {v: k for k, v in MODE_HOMEKIT_TO_HASS.items()} -DEFAULT_VALID_MODES = list(MODE_HOMEKIT_TO_HASS) - CURRENT_MODE_HOMEKIT_TO_HASS = { - 0: CURRENT_HVAC_IDLE, - 1: CURRENT_HVAC_HEAT, - 2: CURRENT_HVAC_COOL, + HeatingCoolingCurrentValues.IDLE: CURRENT_HVAC_IDLE, + HeatingCoolingCurrentValues.HEATING: CURRENT_HVAC_HEAT, + HeatingCoolingCurrentValues.COOLING: CURRENT_HVAC_COOL, } @@ -65,15 +68,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): """Representation of a Homekit climate device.""" - def __init__(self, *args): - """Initialise the device.""" - self._valid_modes = [] - self._min_target_temp = None - self._max_target_temp = None - self._min_target_humidity = DEFAULT_MIN_HUMIDITY - self._max_target_humidity = DEFAULT_MAX_HUMIDITY - super().__init__(*args) - def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -85,44 +79,6 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET, ] - def _setup_heating_cooling_target(self, characteristic): - if "valid-values" in characteristic: - valid_values = [ - val - for val in DEFAULT_VALID_MODES - if val in characteristic["valid-values"] - ] - else: - valid_values = DEFAULT_VALID_MODES - if "minValue" in characteristic: - valid_values = [ - val for val in valid_values if val >= characteristic["minValue"] - ] - if "maxValue" in characteristic: - valid_values = [ - val for val in valid_values if val <= characteristic["maxValue"] - ] - - self._valid_modes = [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] - - def _setup_temperature_target(self, characteristic): - self._features |= SUPPORT_TARGET_TEMPERATURE - - if "minValue" in characteristic: - self._min_target_temp = characteristic["minValue"] - - if "maxValue" in characteristic: - self._max_target_temp = characteristic["maxValue"] - - def _setup_relative_humidity_target(self, characteristic): - self._features |= SUPPORT_TARGET_HUMIDITY - - if "minValue" in characteristic: - self._min_target_humidity = characteristic["minValue"] - - if "maxValue" in characteristic: - self._max_target_humidity = characteristic["maxValue"] - async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temp = kwargs.get(ATTR_TEMPERATURE) @@ -160,15 +116,17 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): @property def min_temp(self): """Return the minimum target temp.""" - if self._max_target_temp: - return self._min_target_temp + if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): + char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET] + return char.minValue return super().min_temp @property def max_temp(self): """Return the maximum target temp.""" - if self._max_target_temp: - return self._max_target_temp + if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): + char = self.service[CharacteristicsTypes.TEMPERATURE_TARGET] + return char.maxValue return super().max_temp @property @@ -184,12 +142,14 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): @property def min_humidity(self): """Return the minimum humidity.""" - return self._min_target_humidity + char = self.service[CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET] + return char.minValue or DEFAULT_MIN_HUMIDITY @property def max_humidity(self): """Return the maximum humidity.""" - return self._max_target_humidity + char = self.service[CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET] + return char.maxValue or DEFAULT_MAX_HUMIDITY @property def hvac_action(self): @@ -213,12 +173,24 @@ class HomeKitClimateDevice(HomeKitEntity, ClimateDevice): @property def hvac_modes(self): """Return the list of available hvac operation modes.""" - return self._valid_modes + valid_values = clamp_enum_to_char( + HeatingCoolingTargetValues, + self.service[CharacteristicsTypes.HEATING_COOLING_TARGET], + ) + return [MODE_HOMEKIT_TO_HASS[mode] for mode in valid_values] @property def supported_features(self): """Return the list of supported features.""" - return self._features + features = 0 + + if self.service.has(CharacteristicsTypes.TEMPERATURE_TARGET): + features |= SUPPORT_TARGET_TEMPERATURE + + if self.service.has(CharacteristicsTypes.RELATIVE_HUMIDITY_TARGET): + features |= SUPPORT_TARGET_HUMIDITY + + return features @property def temperature_unit(self): diff --git a/homeassistant/components/homekit_controller/cover.py b/homeassistant/components/homekit_controller/cover.py index 9b73846d6a7..88885d49b8e 100644 --- a/homeassistant/components/homekit_controller/cover.py +++ b/homeassistant/components/homekit_controller/cover.py @@ -131,12 +131,6 @@ class HomeKitGarageDoorCover(HomeKitEntity, CoverDevice): class HomeKitWindowCover(HomeKitEntity, CoverDevice): """Representation of a HomeKit Window or Window Covering.""" - def __init__(self, accessory, discovery_info): - """Initialise the Cover.""" - super().__init__(accessory, discovery_info) - - self._features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION - def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -151,23 +145,27 @@ class HomeKitWindowCover(HomeKitEntity, CoverDevice): CharacteristicsTypes.OBSTRUCTION_DETECTED, ] - def _setup_position_hold(self, char): - self._features |= SUPPORT_STOP - - def _setup_vertical_tilt_current(self, char): - self._features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION - ) - - def _setup_horizontal_tilt_current(self, char): - self._features |= ( - SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION - ) - @property def supported_features(self): """Flag supported features.""" - return self._features + features = SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION + + if self.service.has(CharacteristicsTypes.POSITION_HOLD): + features |= SUPPORT_STOP + + supports_tilt = any( + ( + self.service.has(CharacteristicsTypes.VERTICAL_TILT_CURRENT), + self.service.has(CharacteristicsTypes.HORIZONTAL_TILT_CURRENT), + ) + ) + + if supports_tilt: + features |= ( + SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT | SUPPORT_SET_TILT_POSITION + ) + + return features @property def current_cover_position(self): diff --git a/homeassistant/components/homekit_controller/fan.py b/homeassistant/components/homekit_controller/fan.py index f0d6967684c..e3b392ea107 100644 --- a/homeassistant/components/homekit_controller/fan.py +++ b/homeassistant/components/homekit_controller/fan.py @@ -44,11 +44,6 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): # that controls whether the fan is on or off. on_characteristic = None - def __init__(self, *args): - """Initialise the fan.""" - self._features = 0 - super().__init__(*args) - def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -58,15 +53,6 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): self.on_characteristic, ] - def _setup_rotation_direction(self, char): - self._features |= SUPPORT_DIRECTION - - def _setup_rotation_speed(self, char): - self._features |= SUPPORT_SET_SPEED - - def _setup_swing_mode(self, char): - self._features |= SUPPORT_OSCILLATE - @property def is_on(self): """Return true if device is on.""" @@ -113,7 +99,18 @@ class BaseHomeKitFan(HomeKitEntity, FanEntity): @property def supported_features(self): """Flag supported features.""" - return self._features + features = 0 + + if self.service.has(CharacteristicsTypes.ROTATION_DIRECTION): + features |= SUPPORT_DIRECTION + + if self.service.has(CharacteristicsTypes.ROTATION_SPEED): + features |= SUPPORT_SET_SPEED + + if self.service.has(CharacteristicsTypes.SWING_MODE): + features |= SUPPORT_OSCILLATE + + return features async def async_set_direction(self, direction): """Set the direction of the fan.""" diff --git a/homeassistant/components/homekit_controller/light.py b/homeassistant/components/homekit_controller/light.py index 14ed74cc085..e78ed48ad0c 100644 --- a/homeassistant/components/homekit_controller/light.py +++ b/homeassistant/components/homekit_controller/light.py @@ -48,18 +48,6 @@ class HomeKitLight(HomeKitEntity, Light): CharacteristicsTypes.SATURATION, ] - def _setup_brightness(self, char): - self._features |= SUPPORT_BRIGHTNESS - - def _setup_color_temperature(self, char): - self._features |= SUPPORT_COLOR_TEMP - - def _setup_hue(self, char): - self._features |= SUPPORT_COLOR - - def _setup_saturation(self, char): - self._features |= SUPPORT_COLOR - @property def is_on(self): """Return true if device is on.""" @@ -86,7 +74,21 @@ class HomeKitLight(HomeKitEntity, Light): @property def supported_features(self): """Flag supported features.""" - return self._features + features = 0 + + if self.service.has(CharacteristicsTypes.BRIGHTNESS): + features |= SUPPORT_BRIGHTNESS + + if self.service.has(CharacteristicsTypes.COLOR_TEMPERATURE): + features |= SUPPORT_COLOR_TEMP + + if self.service.has(CharacteristicsTypes.HUE): + features |= SUPPORT_COLOR + + if self.service.has(CharacteristicsTypes.SATURATION): + features |= SUPPORT_COLOR + + return features async def async_turn_on(self, **kwargs): """Turn the specified light on.""" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index a73d68227c7..8d669174c24 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[IP]==0.2.29.1"], + "requirements": ["aiohomekit[IP]==0.2.34"], "dependencies": [], "zeroconf": ["_hap._tcp.local."], "codeowners": ["@Jc2k"] diff --git a/homeassistant/components/homekit_controller/media_player.py b/homeassistant/components/homekit_controller/media_player.py index 3a1a7359e13..3d5e194ed94 100644 --- a/homeassistant/components/homekit_controller/media_player.py +++ b/homeassistant/components/homekit_controller/media_player.py @@ -57,14 +57,6 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): """Representation of a HomeKit Controller Television.""" - def __init__(self, accessory, discovery_info): - """Initialise the TV.""" - self._state = None - self._features = 0 - self._supported_target_media_state = set() - self._supported_remote_key = set() - super().__init__(accessory, discovery_info) - def get_characteristic_types(self): """Define the homekit characteristics the entity cares about.""" return [ @@ -78,28 +70,6 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): CharacteristicsTypes.IDENTIFIER, ] - def _setup_active_identifier(self, char): - self._features |= SUPPORT_SELECT_SOURCE - - def _setup_target_media_state(self, char): - self._supported_target_media_state = clamp_enum_to_char( - TargetMediaStateValues, char - ) - - if TargetMediaStateValues.PAUSE in self._supported_target_media_state: - self._features |= SUPPORT_PAUSE - - if TargetMediaStateValues.PLAY in self._supported_target_media_state: - self._features |= SUPPORT_PLAY - - if TargetMediaStateValues.STOP in self._supported_target_media_state: - self._features |= SUPPORT_STOP - - def _setup_remote_key(self, char): - self._supported_remote_key = clamp_enum_to_char(RemoteKeyValues, char) - if RemoteKeyValues.PLAY_PAUSE in self._supported_remote_key: - self._features |= SUPPORT_PAUSE | SUPPORT_PLAY - @property def device_class(self): """Define the device class for a HomeKit enabled TV.""" @@ -108,7 +78,47 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): @property def supported_features(self): """Flag media player features that are supported.""" - return self._features + features = 0 + + if self.service.has(CharacteristicsTypes.ACTIVE_IDENTIFIER): + features |= SUPPORT_SELECT_SOURCE + + if self.service.has(CharacteristicsTypes.TARGET_MEDIA_STATE): + if TargetMediaStateValues.PAUSE in self.supported_media_states: + features |= SUPPORT_PAUSE + + if TargetMediaStateValues.PLAY in self.supported_media_states: + features |= SUPPORT_PLAY + + if TargetMediaStateValues.STOP in self.supported_media_states: + features |= SUPPORT_STOP + + if self.service.has(CharacteristicsTypes.REMOTE_KEY): + if RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys: + features |= SUPPORT_PAUSE | SUPPORT_PLAY + + return features + + @property + def supported_media_states(self): + """Mediate state flags that are supported.""" + if not self.service.has(CharacteristicsTypes.TARGET_MEDIA_STATE): + return frozenset() + + return clamp_enum_to_char( + TargetMediaStateValues, + self.service[CharacteristicsTypes.TARGET_MEDIA_STATE], + ) + + @property + def supported_remote_keys(self): + """Remote key buttons that are supported.""" + if not self.service.has(CharacteristicsTypes.REMOTE_KEY): + return frozenset() + + return clamp_enum_to_char( + RemoteKeyValues, self.service[CharacteristicsTypes.REMOTE_KEY] + ) @property def source_list(self): @@ -164,11 +174,11 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): _LOGGER.debug("Cannot play while already playing") return - if TargetMediaStateValues.PLAY in self._supported_target_media_state: + if TargetMediaStateValues.PLAY in self.supported_media_states: await self.async_put_characteristics( {CharacteristicsTypes.TARGET_MEDIA_STATE: TargetMediaStateValues.PLAY} ) - elif RemoteKeyValues.PLAY_PAUSE in self._supported_remote_key: + elif RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys: await self.async_put_characteristics( {CharacteristicsTypes.REMOTE_KEY: RemoteKeyValues.PLAY_PAUSE} ) @@ -179,11 +189,11 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): _LOGGER.debug("Cannot pause while already paused") return - if TargetMediaStateValues.PAUSE in self._supported_target_media_state: + if TargetMediaStateValues.PAUSE in self.supported_media_states: await self.async_put_characteristics( {CharacteristicsTypes.TARGET_MEDIA_STATE: TargetMediaStateValues.PAUSE} ) - elif RemoteKeyValues.PLAY_PAUSE in self._supported_remote_key: + elif RemoteKeyValues.PLAY_PAUSE in self.supported_remote_keys: await self.async_put_characteristics( {CharacteristicsTypes.REMOTE_KEY: RemoteKeyValues.PLAY_PAUSE} ) @@ -194,7 +204,7 @@ class HomeKitTelevision(HomeKitEntity, MediaPlayerDevice): _LOGGER.debug("Cannot stop when already idle") return - if TargetMediaStateValues.STOP in self._supported_target_media_state: + if TargetMediaStateValues.STOP in self.supported_media_states: await self.async_put_characteristics( {CharacteristicsTypes.TARGET_MEDIA_STATE: TargetMediaStateValues.STOP} ) diff --git a/requirements_all.txt b/requirements_all.txt index 962e5d2bc45..235558575e1 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioftp==0.12.0 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.29.1 +aiohomekit[IP]==0.2.34 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 297f3d8f173..e71929dadbd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -65,7 +65,7 @@ aioesphomeapi==2.6.1 aiofreepybox==0.0.8 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.29.1 +aiohomekit[IP]==0.2.34 # homeassistant.components.emulated_hue # homeassistant.components.http From 05abf370461b887b32e0f37bd8d84ed4530a5613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Wed, 18 Mar 2020 19:27:25 +0200 Subject: [PATCH 115/431] Type hint improvements (#32905) * Complete helpers.entity_component type hints * Add discovery info type --- .../components/directv/config_flow.py | 3 +- homeassistant/components/gtfs/sensor.py | 8 +- .../components/here_travel_time/sensor.py | 3 +- homeassistant/components/remote/__init__.py | 4 +- homeassistant/components/switch/light.py | 10 ++- .../components/switcher_kis/__init__.py | 3 +- homeassistant/components/vizio/config_flow.py | 5 +- homeassistant/components/zone/__init__.py | 2 +- homeassistant/helpers/collection.py | 2 +- homeassistant/helpers/discovery.py | 6 +- homeassistant/helpers/entity_component.py | 68 ++++++++++------ homeassistant/helpers/entity_platform.py | 81 +++++++++++-------- homeassistant/helpers/typing.py | 1 + 13 files changed, 120 insertions(+), 76 deletions(-) diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index d1b3a6cbe62..b7d1604622e 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -12,6 +12,7 @@ from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DEFAULT_PORT from .const import DOMAIN # pylint: disable=unused-import @@ -83,7 +84,7 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=info["title"], data=user_input) async def async_step_ssdp( - self, discovery_info: Optional[Dict] = None + self, discovery_info: Optional[DiscoveryInfoType] = None ) -> Dict[str, Any]: """Handle a flow initialized by discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname diff --git a/homeassistant/components/gtfs/sensor.py b/homeassistant/components/gtfs/sensor.py index 2bd0ce1b09f..08550ed80b8 100644 --- a/homeassistant/components/gtfs/sensor.py +++ b/homeassistant/components/gtfs/sensor.py @@ -19,7 +19,11 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) from homeassistant.util import slugify import homeassistant.util.dt as dt_util @@ -332,7 +336,7 @@ def setup_platform( hass: HomeAssistantType, config: ConfigType, add_entities: Callable[[list], None], - discovery_info: Optional[dict] = None, + discovery_info: Optional[DiscoveryInfoType] = None, ) -> None: """Set up the GTFS sensor.""" gtfs_dir = hass.config.path(DEFAULT_PATH) diff --git a/homeassistant/components/here_travel_time/sensor.py b/homeassistant/components/here_travel_time/sensor.py index d93cfdf7053..c88aeb8e5a0 100644 --- a/homeassistant/components/here_travel_time/sensor.py +++ b/homeassistant/components/here_travel_time/sensor.py @@ -24,6 +24,7 @@ from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers import location import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import DiscoveryInfoType import homeassistant.util.dt as dt _LOGGER = logging.getLogger(__name__) @@ -144,7 +145,7 @@ async def async_setup_platform( hass: HomeAssistant, config: Dict[str, Union[str, bool]], async_add_entities: Callable, - discovery_info: None = None, + discovery_info: Optional[DiscoveryInfoType] = None, ) -> None: """Set up the HERE travel time platform.""" diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 3a71ebb94d1..0a598ae345d 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -114,9 +114,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up a config entry.""" - return cast( - bool, await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) - ) + return await cast(EntityComponent, hass.data[DOMAIN]).async_setup_entry(entry) async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: diff --git a/homeassistant/components/switch/light.py b/homeassistant/components/switch/light.py index 5486b8d880c..92b64b36b93 100644 --- a/homeassistant/components/switch/light.py +++ b/homeassistant/components/switch/light.py @@ -1,6 +1,6 @@ """Light support for switch entities.""" import logging -from typing import Callable, Dict, Optional, Sequence, cast +from typing import Callable, Optional, Sequence, cast import voluptuous as vol @@ -17,7 +17,11 @@ from homeassistant.core import CALLBACK_TYPE, State, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_state_change -from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -37,7 +41,7 @@ async def async_setup_platform( hass: HomeAssistantType, config: ConfigType, async_add_entities: Callable[[Sequence[Entity], bool], None], - discovery_info: Optional[Dict] = None, + discovery_info: Optional[DiscoveryInfoType] = None, ) -> None: """Initialize Light Switch platform.""" async_add_entities( diff --git a/homeassistant/components/switcher_kis/__init__.py b/homeassistant/components/switcher_kis/__init__.py index 63f2aa47a3a..0545687b003 100644 --- a/homeassistant/components/switcher_kis/__init__.py +++ b/homeassistant/components/switcher_kis/__init__.py @@ -20,6 +20,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ( ContextType, + DiscoveryInfoType, EventType, HomeAssistantType, ServiceCallType, @@ -115,7 +116,7 @@ async def async_setup(hass: HomeAssistantType, config: Dict) -> bool: hass.data[DOMAIN] = {DATA_DEVICE: device_data} async def async_switch_platform_discovered( - platform: str, discovery_info: Optional[Dict] + platform: str, discovery_info: DiscoveryInfoType ) -> None: """Use for registering services after switch platform is discovered.""" if platform != DOMAIN: diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 7dc4b19fa83..ba3ac5107bb 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Vizio.""" import copy import logging -from typing import Any, Dict +from typing import Any, Dict, Optional from pyvizio import VizioAsync, async_guess_device_type import voluptuous as vol @@ -23,6 +23,7 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import DiscoveryInfoType from .const import ( CONF_APPS, @@ -318,7 +319,7 @@ 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: Dict[str, Any] = None + self, discovery_info: Optional[DiscoveryInfoType] = None ) -> Dict[str, Any]: """Handle zeroconf discovery.""" diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index c71026ea79c..08304df8137 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -221,7 +221,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: home_zone = Zone(_home_conf(hass), True,) home_zone.entity_id = ENTITY_ID_HOME - await component.async_add_entities([home_zone]) # type: ignore + await component.async_add_entities([home_zone]) async def core_config_updated(_: Event) -> None: """Handle core config updated.""" diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index 8234dd6ec87..e720887eb70 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -266,7 +266,7 @@ def attach_entity_component_collection( """Handle a collection change.""" if change_type == CHANGE_ADDED: entity = create_entity(config) - await entity_component.async_add_entities([entity]) # type: ignore + await entity_component.async_add_entities([entity]) entities[item_id] = entity return diff --git a/homeassistant/helpers/discovery.py b/homeassistant/helpers/discovery.py index 806540e57ce..ea20d8c9216 100644 --- a/homeassistant/helpers/discovery.py +++ b/homeassistant/helpers/discovery.py @@ -5,7 +5,7 @@ There are two different types of discoveries that can be fired/listened for. - listen_platform/discover_platform is for platforms. These are used by components to allow discovery of their platforms. """ -from typing import Callable, Collection, Union +from typing import Any, Callable, Collection, Dict, Optional, Union from homeassistant import core, setup from homeassistant.const import ATTR_DISCOVERED, ATTR_SERVICE, EVENT_PLATFORM_DISCOVERED @@ -90,7 +90,9 @@ def listen_platform( @bind_hass def async_listen_platform( - hass: core.HomeAssistant, component: str, callback: Callable + hass: core.HomeAssistant, + component: str, + callback: Callable[[str, Optional[Dict[str, Any]]], Any], ) -> None: """Register a platform loader listener. diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index f6c473dd418..71c57dc13f1 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -4,12 +4,14 @@ from datetime import timedelta from itertools import chain import logging from types import ModuleType -from typing import Dict, Optional, cast +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Union + +import voluptuous as vol from homeassistant import config as conf_util from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_ENTITY_NAMESPACE, CONF_SCAN_INTERVAL -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import HomeAssistant, ServiceCall, callback from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import ( config_per_platform, @@ -18,13 +20,12 @@ from homeassistant.helpers import ( entity, service, ) +from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType from homeassistant.loader import async_get_integration, bind_hass from homeassistant.setup import async_prepare_setup_platform from .entity_platform import EntityPlatform -# mypy: allow-untyped-defs, no-check-untyped-defs - DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) DATA_INSTANCES = "entity_components" @@ -75,18 +76,18 @@ class EntityComponent: self.domain = domain self.scan_interval = scan_interval - self.config = None + self.config: Optional[ConfigType] = None - self._platforms: Dict[str, EntityPlatform] = { - domain: self._async_init_entity_platform(domain, None) - } + self._platforms: Dict[ + Union[str, Tuple[str, Optional[timedelta], Optional[str]]], EntityPlatform + ] = {domain: self._async_init_entity_platform(domain, None)} self.async_add_entities = self._platforms[domain].async_add_entities self.add_entities = self._platforms[domain].add_entities hass.data.setdefault(DATA_INSTANCES, {})[domain] = self @property - def entities(self): + def entities(self) -> Iterable[entity.Entity]: """Return an iterable that returns all entities.""" return chain.from_iterable( platform.entities.values() for platform in self._platforms.values() @@ -95,19 +96,23 @@ class EntityComponent: def get_entity(self, entity_id: str) -> Optional[entity.Entity]: """Get an entity.""" for platform in self._platforms.values(): - entity_obj = cast(Optional[entity.Entity], platform.entities.get(entity_id)) + entity_obj = platform.entities.get(entity_id) if entity_obj is not None: return entity_obj return None - def setup(self, config): + def setup(self, config: ConfigType) -> None: """Set up a full entity component. This doesn't block the executor to protect from deadlocks. """ - self.hass.add_job(self.async_setup(config)) + self.hass.add_job( + self.async_setup( # type: ignore + config + ) + ) - async def async_setup(self, config): + async def async_setup(self, config: ConfigType) -> None: """Set up a full entity component. Loads the platforms from the config and will listen for supported @@ -127,7 +132,9 @@ class EntityComponent: # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.components.discovery.load_platform() - async def component_platform_discovered(platform, info): + async def component_platform_discovered( + platform: str, info: Optional[Dict[str, Any]] + ) -> None: """Handle the loading of a platform.""" await self.async_setup_platform(platform, {}, info) @@ -135,7 +142,7 @@ class EntityComponent: self.hass, self.domain, component_platform_discovered ) - async def async_setup_entry(self, config_entry): + async def async_setup_entry(self, config_entry: ConfigEntry) -> bool: """Set up a config entry.""" platform_type = config_entry.domain platform = await async_prepare_setup_platform( @@ -161,7 +168,7 @@ class EntityComponent: scan_interval=getattr(platform, "SCAN_INTERVAL", None), ) - return await self._platforms[key].async_setup_entry(config_entry) + return await self._platforms[key].async_setup_entry(config_entry) # type: ignore async def async_unload_entry(self, config_entry: ConfigEntry) -> bool: """Unload a config entry.""" @@ -175,24 +182,32 @@ class EntityComponent: await platform.async_reset() return True - async def async_extract_from_service(self, service_call, expand_group=True): + async def async_extract_from_service( + self, service_call: ServiceCall, expand_group: bool = True + ) -> List[entity.Entity]: """Extract all known and available entities from a service call. Will return an empty list if entities specified but unknown. This method must be run in the event loop. """ - return await service.async_extract_entities( + return await service.async_extract_entities( # type: ignore self.hass, self.entities, service_call, expand_group ) @callback - def async_register_entity_service(self, name, schema, func, required_features=None): + def async_register_entity_service( + self, + name: str, + schema: Union[Dict[str, Any], vol.Schema], + func: str, + required_features: Optional[int] = None, + ) -> None: """Register an entity service.""" if isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) - async def handle_service(call): + async def handle_service(call: Callable) -> None: """Handle the service.""" await self.hass.helpers.service.entity_service_call( self._platforms.values(), func, call, required_features @@ -201,8 +216,11 @@ class EntityComponent: self.hass.services.async_register(self.domain, name, handle_service, schema) async def async_setup_platform( - self, platform_type, platform_config, discovery_info=None - ): + self, + platform_type: str, + platform_config: ConfigType, + discovery_info: Optional[DiscoveryInfoType] = None, + ) -> None: """Set up a platform for this component.""" if self.config is None: raise RuntimeError("async_setup needs to be called first") @@ -227,7 +245,9 @@ class EntityComponent: platform_type, platform, scan_interval, entity_namespace ) - await self._platforms[key].async_setup(platform_config, discovery_info) + await self._platforms[key].async_setup( # type: ignore + platform_config, discovery_info + ) async def _async_reset(self) -> None: """Remove entities and reset the entity component to initial values. @@ -285,7 +305,7 @@ class EntityComponent: if scan_interval is None: scan_interval = self.scan_interval - return EntityPlatform( # type: ignore + return EntityPlatform( hass=self.hass, logger=self.logger, domain=self.domain, diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e1e046eaa6d..0c288b0ad21 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -1,18 +1,24 @@ """Class to manage the entities for a single platform.""" import asyncio from contextvars import ContextVar -from datetime import datetime -from typing import Optional +from datetime import datetime, timedelta +from logging import Logger +from types import ModuleType +from typing import TYPE_CHECKING, Dict, Iterable, List, Optional, cast from homeassistant.const import DEVICE_DEFAULT_NAME -from homeassistant.core import callback, split_entity_id, valid_entity_id +from homeassistant.core import CALLBACK_TYPE, callback, split_entity_id, valid_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import config_validation as cv, service +from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.async_ import run_callback_threadsafe from .entity_registry import DISABLED_INTEGRATION from .event import async_call_later, async_track_time_interval +if TYPE_CHECKING: + from .entity import Entity + # mypy: allow-untyped-defs, no-check-untyped-defs SLOW_SETUP_WARNING = 10 @@ -26,23 +32,15 @@ class EntityPlatform: def __init__( self, *, - hass, - logger, - domain, - platform_name, - platform, - scan_interval, - entity_namespace, + hass: HomeAssistantType, + logger: Logger, + domain: str, + platform_name: str, + platform: Optional[ModuleType], + scan_interval: timedelta, + entity_namespace: Optional[str], ): - """Initialize the entity platform. - - hass: HomeAssistant - logger: Logger - domain: str - platform_name: str - scan_interval: timedelta - entity_namespace: str - """ + """Initialize the entity platform.""" self.hass = hass self.logger = logger self.domain = domain @@ -51,13 +49,13 @@ class EntityPlatform: self.scan_interval = scan_interval self.entity_namespace = entity_namespace self.config_entry = None - self.entities = {} - self._tasks = [] + self.entities: Dict[str, Entity] = {} # pylint: disable=used-before-assignment + self._tasks: List[asyncio.Future] = [] # Method to cancel the state change listener - self._async_unsub_polling = None + self._async_unsub_polling: Optional[CALLBACK_TYPE] = None # Method to cancel the retry of setup - self._async_cancel_retry_setup = None - self._process_updates = None + self._async_cancel_retry_setup: Optional[CALLBACK_TYPE] = None + self._process_updates: Optional[asyncio.Lock] = None # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities @@ -224,7 +222,9 @@ class EntityPlatform: finally: warn_task.cancel() - def _schedule_add_entities(self, new_entities, update_before_add=False): + def _schedule_add_entities( + self, new_entities: Iterable["Entity"], update_before_add: bool = False + ) -> None: """Schedule adding entities for a single platform, synchronously.""" run_callback_threadsafe( self.hass.loop, @@ -234,17 +234,24 @@ class EntityPlatform: ).result() @callback - def _async_schedule_add_entities(self, new_entities, update_before_add=False): + def _async_schedule_add_entities( + self, new_entities: Iterable["Entity"], update_before_add: bool = False + ) -> None: """Schedule adding entities for a single platform async.""" self._tasks.append( - self.hass.async_add_job( - self.async_add_entities( - new_entities, update_before_add=update_before_add - ) + cast( + asyncio.Future, + self.hass.async_add_job( + self.async_add_entities( # type: ignore + new_entities, update_before_add=update_before_add + ), + ), ) ) - def add_entities(self, new_entities, update_before_add=False): + def add_entities( + self, new_entities: Iterable["Entity"], update_before_add: bool = False + ) -> None: """Add entities for a single platform.""" # That avoid deadlocks if update_before_add: @@ -258,7 +265,9 @@ class EntityPlatform: self.hass.loop, ).result() - async def async_add_entities(self, new_entities, update_before_add=False): + async def async_add_entities( + self, new_entities: Iterable["Entity"], update_before_add: bool = False + ) -> None: """Add entities for a single platform async. This method must be run in the event loop. @@ -272,7 +281,7 @@ class EntityPlatform: device_registry = await hass.helpers.device_registry.async_get_registry() entity_registry = await hass.helpers.entity_registry.async_get_registry() tasks = [ - self._async_add_entity( + self._async_add_entity( # type: ignore entity, update_before_add, entity_registry, device_registry ) for entity in new_entities @@ -290,7 +299,9 @@ class EntityPlatform: return self._async_unsub_polling = async_track_time_interval( - self.hass, self._update_entity_states, self.scan_interval + self.hass, + self._update_entity_states, # type: ignore + self.scan_interval, ) async def _async_add_entity( @@ -515,7 +526,7 @@ class EntityPlatform: for entity in self.entities.values(): if not entity.should_poll: continue - tasks.append(entity.async_update_ha_state(True)) + tasks.append(entity.async_update_ha_state(True)) # type: ignore if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 6e31301066c..c7859d9d1d9 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -8,6 +8,7 @@ import homeassistant.core GPSType = Tuple[float, float] ConfigType = Dict[str, Any] ContextType = homeassistant.core.Context +DiscoveryInfoType = Dict[str, Any] EventType = homeassistant.core.Event HomeAssistantType = homeassistant.core.HomeAssistant ServiceCallType = homeassistant.core.ServiceCall From 661570dfadc10c85f8bcdac878720ba38b768f59 Mon Sep 17 00:00:00 2001 From: Daniel Shokouhi Date: Wed, 18 Mar 2020 13:19:40 -0700 Subject: [PATCH 116/431] Small tweaks (#32946) --- homeassistant/components/obihai/manifest.json | 2 +- homeassistant/components/obihai/sensor.py | 10 ++++++---- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/obihai/manifest.json b/homeassistant/components/obihai/manifest.json index 3a979922eba..de85a85842a 100644 --- a/homeassistant/components/obihai/manifest.json +++ b/homeassistant/components/obihai/manifest.json @@ -2,7 +2,7 @@ "domain": "obihai", "name": "Obihai", "documentation": "https://www.home-assistant.io/integrations/obihai", - "requirements": ["pyobihai==1.2.0"], + "requirements": ["pyobihai==1.2.1"], "dependencies": [], "codeowners": ["@dshokouhi"] } diff --git a/homeassistant/components/obihai/sensor.py b/homeassistant/components/obihai/sensor.py index 13d09de0542..a81b381f1ed 100644 --- a/homeassistant/components/obihai/sensor.py +++ b/homeassistant/components/obihai/sensor.py @@ -59,8 +59,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for key in services: sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) - for key in line_services: - sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) + if line_services is not None: + for key in line_services: + sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) for key in call_direction: sensors.append(ObihaiServiceSensors(pyobihai, serial, key)) @@ -136,8 +137,9 @@ class ObihaiServiceSensors(Entity): services = self._pyobihai.get_line_state() - if self._service_name in services: - self._state = services.get(self._service_name) + if services is not None: + if self._service_name in services: + self._state = services.get(self._service_name) call_direction = self._pyobihai.get_call_direction() diff --git a/requirements_all.txt b/requirements_all.txt index 235558575e1..b6e90b841dd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1432,7 +1432,7 @@ pynx584==0.4 pynzbgetapi==0.2.0 # homeassistant.components.obihai -pyobihai==1.2.0 +pyobihai==1.2.1 # homeassistant.components.ombi pyombi==0.1.10 From cb450dcebd47835e934661b2dc397478d4eff9dd Mon Sep 17 00:00:00 2001 From: Bendik Brenne Date: Wed, 18 Mar 2020 21:43:43 +0100 Subject: [PATCH 117/431] Resolve unexpected exception caused by sinch error_code being a string (#32929) --- homeassistant/components/sinch/notify.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/sinch/notify.py b/homeassistant/components/sinch/notify.py index c0092f013c4..5e48f7b314d 100644 --- a/homeassistant/components/sinch/notify.py +++ b/homeassistant/components/sinch/notify.py @@ -83,7 +83,7 @@ class SinchNotificationService(BaseNotificationService): ) except ErrorResponseException as ex: _LOGGER.error( - "Caught ErrorResponseException. Response code: %d (%s)", + "Caught ErrorResponseException. Response code: %s (%s)", ex.error_code, ex, ) From 0cb27ff2365e97c83582553d3632645b808f82f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miroslav=20=C5=BDdrale?= Date: Wed, 18 Mar 2020 21:45:23 +0100 Subject: [PATCH 118/431] Bump pyubee to 0.10 to support more Ubee routers (#32934) * Bump pyubee to 0.10 to support more Ubee routers * Update requirements_all.txt --- homeassistant/components/ubee/device_tracker.py | 8 +++++++- homeassistant/components/ubee/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ubee/device_tracker.py b/homeassistant/components/ubee/device_tracker.py index 21d2fd10009..266acc49c09 100644 --- a/homeassistant/components/ubee/device_tracker.py +++ b/homeassistant/components/ubee/device_tracker.py @@ -24,7 +24,13 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_PASSWORD): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Optional(CONF_MODEL, default=DEFAULT_MODEL): vol.Any( - "EVW32C-0N", "EVW320B", "EVW321B", "EVW3200-Wifi", "EVW3226@UPC", "DVW32CB" + "EVW32C-0N", + "EVW320B", + "EVW321B", + "EVW3200-Wifi", + "EVW3226@UPC", + "DVW32CB", + "DDW36C", ), } ) diff --git a/homeassistant/components/ubee/manifest.json b/homeassistant/components/ubee/manifest.json index e853c7490db..446bc2c62d5 100644 --- a/homeassistant/components/ubee/manifest.json +++ b/homeassistant/components/ubee/manifest.json @@ -2,7 +2,7 @@ "domain": "ubee", "name": "Ubee Router", "documentation": "https://www.home-assistant.io/integrations/ubee", - "requirements": ["pyubee==0.9"], + "requirements": ["pyubee==0.10"], "dependencies": [], "codeowners": ["@mzdrale"] } diff --git a/requirements_all.txt b/requirements_all.txt index b6e90b841dd..dd7885af5ba 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1705,7 +1705,7 @@ pytradfri[async]==6.4.0 pytrafikverket==0.1.6.1 # homeassistant.components.ubee -pyubee==0.9 +pyubee==0.10 # homeassistant.components.uptimerobot pyuptimerobot==0.0.5 From 34e44e7f3a5cb5fb22200f05cbefce5e7bc785a5 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Wed, 18 Mar 2020 21:20:40 +0000 Subject: [PATCH 119/431] Add support for homekit valve accessories to homekit_controller (#32937) --- .../components/homekit_controller/const.py | 1 + .../components/homekit_controller/switch.py | 98 +- .../test_rainmachine_pro_8.py | 65 + .../homekit_controller/test_switch.py | 67 +- .../homekit_controller/rainmachine-pro-8.json | 1137 +++++++++++++++++ 5 files changed, 1351 insertions(+), 17 deletions(-) create mode 100644 tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py create mode 100644 tests/fixtures/homekit_controller/rainmachine-pro-8.json diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index f5ae6cbd644..a13ad22df51 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -32,4 +32,5 @@ HOMEKIT_ACCESSORY_DISPATCH = { "air-quality": "air_quality", "occupancy": "binary_sensor", "television": "media_player", + "valve": "switch", } diff --git a/homeassistant/components/homekit_controller/switch.py b/homeassistant/components/homekit_controller/switch.py index 5897bbb7b3f..61595b504ca 100644 --- a/homeassistant/components/homekit_controller/switch.py +++ b/homeassistant/components/homekit_controller/switch.py @@ -1,7 +1,11 @@ """Support for Homekit switches.""" import logging -from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics import ( + CharacteristicsTypes, + InUseValues, + IsConfiguredValues, +) from homeassistant.components.switch import SwitchDevice from homeassistant.core import callback @@ -12,21 +16,9 @@ OUTLET_IN_USE = "outlet_in_use" _LOGGER = logging.getLogger(__name__) - -async def async_setup_entry(hass, config_entry, async_add_entities): - """Set up Homekit lock.""" - hkid = config_entry.data["AccessoryPairingID"] - conn = hass.data[KNOWN_DEVICES][hkid] - - @callback - def async_add_service(aid, service): - if service["stype"] not in ("switch", "outlet"): - return False - info = {"aid": aid, "iid": service["iid"]} - async_add_entities([HomeKitSwitch(conn, info)], True) - return True - - conn.add_listener(async_add_service) +ATTR_IN_USE = "in_use" +ATTR_IS_CONFIGURED = "is_configured" +ATTR_REMAINING_DURATION = "remaining_duration" class HomeKitSwitch(HomeKitEntity, SwitchDevice): @@ -55,3 +47,77 @@ class HomeKitSwitch(HomeKitEntity, SwitchDevice): outlet_in_use = self.service.value(CharacteristicsTypes.OUTLET_IN_USE) if outlet_in_use is not None: return {OUTLET_IN_USE: outlet_in_use} + + +class HomeKitValve(HomeKitEntity, SwitchDevice): + """Represents a valve in an irrigation system.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity cares about.""" + return [ + CharacteristicsTypes.ACTIVE, + CharacteristicsTypes.IN_USE, + CharacteristicsTypes.IS_CONFIGURED, + CharacteristicsTypes.REMAINING_DURATION, + ] + + async def async_turn_on(self, **kwargs): + """Turn the specified valve on.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: True}) + + async def async_turn_off(self, **kwargs): + """Turn the specified valve off.""" + await self.async_put_characteristics({CharacteristicsTypes.ACTIVE: False}) + + @property + def icon(self) -> str: + """Return the icon.""" + return "mdi:water" + + @property + def is_on(self): + """Return true if device is on.""" + return self.service.value(CharacteristicsTypes.ACTIVE) + + @property + def device_state_attributes(self): + """Return the optional state attributes.""" + attrs = {} + + in_use = self.service.value(CharacteristicsTypes.IN_USE) + if in_use is not None: + attrs[ATTR_IN_USE] = in_use == InUseValues.IN_USE + + is_configured = self.service.value(CharacteristicsTypes.IS_CONFIGURED) + if is_configured is not None: + attrs[ATTR_IS_CONFIGURED] = is_configured == IsConfiguredValues.CONFIGURED + + remaining = self.service.value(CharacteristicsTypes.REMAINING_DURATION) + if remaining is not None: + attrs[ATTR_REMAINING_DURATION] = remaining + + return attrs + + +ENTITY_TYPES = { + "switch": HomeKitSwitch, + "outlet": HomeKitSwitch, + "valve": HomeKitValve, +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Homekit switches.""" + hkid = config_entry.data["AccessoryPairingID"] + conn = hass.data[KNOWN_DEVICES][hkid] + + @callback + def async_add_service(aid, service): + entity_class = ENTITY_TYPES.get(service["stype"]) + if not entity_class: + return False + info = {"aid": aid, "iid": service["iid"]} + async_add_entities([entity_class(conn, info)], True) + return True + + conn.add_listener(async_add_service) diff --git a/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py new file mode 100644 index 00000000000..fd95ef98c09 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_rainmachine_pro_8.py @@ -0,0 +1,65 @@ +""" +Make sure that existing RainMachine support isn't broken. + +https://github.com/home-assistant/core/issues/31745 +""" + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_rainmachine_pro_8_setup(hass): + """Test that a RainMachine can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "rainmachine-pro-8.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + # Assert that the entity is correctly added to the entity registry + entry = entity_registry.async_get("switch.rainmachine_00ce4a") + assert entry.unique_id == "homekit-00aa0000aa0a-512" + + helper = Helper( + hass, "switch.rainmachine_00ce4a", pairing, accessories[0], config_entry + ) + state = await helper.poll_and_get_state() + + # Assert that the friendly name is detected correctly + assert state.attributes["friendly_name"] == "RainMachine-00ce4a" + + device_registry = await hass.helpers.device_registry.async_get_registry() + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "Green Electronics LLC" + assert device.name == "RainMachine-00ce4a" + assert device.model == "SPK5 Pro" + assert device.sw_version == "1.0.4" + assert device.via_device_id is None + + # The device is made up of multiple valves - make sure we have enumerated them all + entry = entity_registry.async_get("switch.rainmachine_00ce4a_2") + assert entry.unique_id == "homekit-00aa0000aa0a-768" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_3") + assert entry.unique_id == "homekit-00aa0000aa0a-1024" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_4") + assert entry.unique_id == "homekit-00aa0000aa0a-1280" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_5") + assert entry.unique_id == "homekit-00aa0000aa0a-1536" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_6") + assert entry.unique_id == "homekit-00aa0000aa0a-1792" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_7") + assert entry.unique_id == "homekit-00aa0000aa0a-2048" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_8") + assert entry.unique_id == "homekit-00aa0000aa0a-2304" + + entry = entity_registry.async_get("switch.rainmachine_00ce4a_9") + assert entry is None diff --git a/tests/components/homekit_controller/test_switch.py b/tests/components/homekit_controller/test_switch.py index eb10d42e208..c53d20891b1 100644 --- a/tests/components/homekit_controller/test_switch.py +++ b/tests/components/homekit_controller/test_switch.py @@ -1,6 +1,10 @@ """Basic checks for HomeKitSwitch.""" -from aiohomekit.model.characteristics import CharacteristicsTypes +from aiohomekit.model.characteristics import ( + CharacteristicsTypes, + InUseValues, + IsConfiguredValues, +) from aiohomekit.model.services import ServicesTypes from tests.components.homekit_controller.common import setup_test_component @@ -17,6 +21,23 @@ def create_switch_service(accessory): outlet_in_use.value = False +def create_valve_service(accessory): + """Define valve characteristics.""" + service = accessory.add_service(ServicesTypes.VALVE) + + on_char = service.add_char(CharacteristicsTypes.ACTIVE) + on_char.value = False + + in_use = service.add_char(CharacteristicsTypes.IN_USE) + in_use.value = InUseValues.IN_USE + + configured = service.add_char(CharacteristicsTypes.IS_CONFIGURED) + configured.value = IsConfiguredValues.CONFIGURED + + remaining = service.add_char(CharacteristicsTypes.REMAINING_DURATION) + remaining.value = 99 + + async def test_switch_change_outlet_state(hass, utcnow): """Test that we can turn a HomeKit outlet on and off again.""" helper = await setup_test_component(hass, create_switch_service) @@ -57,3 +78,47 @@ async def test_switch_read_outlet_state(hass, utcnow): switch_1 = await helper.poll_and_get_state() assert switch_1.state == "off" assert switch_1.attributes["outlet_in_use"] is True + + +async def test_valve_change_active_state(hass, utcnow): + """Test that we can turn a valve on and off again.""" + helper = await setup_test_component(hass, create_valve_service) + + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.testdevice"}, blocking=True + ) + assert helper.characteristics[("valve", "active")].value == 1 + + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.testdevice"}, blocking=True + ) + assert helper.characteristics[("valve", "active")].value == 0 + + +async def test_valve_read_state(hass, utcnow): + """Test that we can read the state of a valve accessory.""" + helper = await setup_test_component(hass, create_valve_service) + + # Initial state is that the switch is off and the outlet isn't in use + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == "off" + assert switch_1.attributes["in_use"] is True + assert switch_1.attributes["is_configured"] is True + assert switch_1.attributes["remaining_duration"] == 99 + + # Simulate that someone switched on the device in the real world not via HA + helper.characteristics[("valve", "active")].set_value(True) + switch_1 = await helper.poll_and_get_state() + assert switch_1.state == "on" + + # Simulate that someone configured the device in the real world not via HA + helper.characteristics[ + ("valve", "is-configured") + ].value = IsConfiguredValues.NOT_CONFIGURED + switch_1 = await helper.poll_and_get_state() + assert switch_1.attributes["is_configured"] is False + + # Simulate that someone using the device in the real world not via HA + helper.characteristics[("valve", "in-use")].value = InUseValues.NOT_IN_USE + switch_1 = await helper.poll_and_get_state() + assert switch_1.attributes["in_use"] is False diff --git a/tests/fixtures/homekit_controller/rainmachine-pro-8.json b/tests/fixtures/homekit_controller/rainmachine-pro-8.json new file mode 100644 index 00000000000..1b50063006e --- /dev/null +++ b/tests/fixtures/homekit_controller/rainmachine-pro-8.json @@ -0,0 +1,1137 @@ +[ + { + "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": "Green Electronics LLC", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 4, + "type": "00000021-0000-1000-8000-0026BB765291", + "format": "string", + "value": "SPK5 Pro", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 5, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "RainMachine-00ce4a", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 6, + "type": "00000030-0000-1000-8000-0026BB765291", + "format": "string", + "value": "00aa0000aa0a", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 7, + "type": "00000052-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1.0.4", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 8, + "type": "00000053-0000-1000-8000-0026BB765291", + "format": "string", + "value": "1", + "perms": [ + "pr" + ], + "ev": false + }, + { + "iid": 9, + "type": "34AB8811-AC7F-4340-BAC3-FD6A85F9943B", + "format": "string", + "value": "2.0;16A62", + "perms": [ + "pr", + "hd" + ], + "ev": false + } + ] + }, + { + "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 + } + ] + }, + { + "iid": 64, + "type": "000000CF-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "linked": [ + 512, + 768, + 1024, + 1280, + 1536, + 1792, + 2048, + 2304 + ], + "characteristics": [ + { + "iid": 67, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 65, + "type": "000000D1-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 2, + "minStep": 1 + }, + { + "iid": 68, + "type": "000000D2-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": "RainMachine", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 512, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 544, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 560, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 576, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 592, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 608, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 624, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 640, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 528, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 768, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 800, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 816, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 832, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 848, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 864, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 880, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 896, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 2, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 784, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 1024, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1056, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1072, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1088, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1104, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1120, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1136, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1152, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 3, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1040, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 1280, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1312, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1328, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1344, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1360, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1376, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1392, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1408, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 4, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1296, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 1536, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1568, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1584, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1600, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1616, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1632, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1648, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1664, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 5, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1552, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 1792, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 1824, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1840, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1856, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 1872, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 1888, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1904, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 1920, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 6, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 1808, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 2048, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2080, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2096, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2112, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 2128, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2144, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2160, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2176, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 7, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 2064, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + }, + { + "iid": 2304, + "type": "000000D0-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "linked": [], + "characteristics": [ + { + "iid": 2336, + "type": "000000B0-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2352, + "type": "000000D2-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2368, + "type": "000000D5-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 3, + "minStep": 1 + }, + { + "iid": 2384, + "type": "000000D6-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 1, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 1, + "minStep": 1 + }, + { + "iid": 2400, + "type": "000000D4-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 0, + "perms": [ + "pr", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2416, + "type": "000000D3-0000-1000-8000-0026BB765291", + "format": "uint32", + "value": 300, + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": false, + "minValue": 0, + "maxValue": 86400, + "minStep": 1 + }, + { + "iid": 2432, + "type": "000000CB-0000-1000-8000-0026BB765291", + "format": "uint8", + "value": 8, + "perms": [ + "pr" + ], + "ev": false, + "minValue": 1, + "maxValue": 255, + "minStep": 1 + }, + { + "iid": 2320, + "type": "00000023-0000-1000-8000-0026BB765291", + "format": "string", + "value": "", + "perms": [ + "pr" + ], + "ev": false + } + ] + } + ] + } +] \ No newline at end of file From ede0cfaeb842e7776e40633c1df3f2d94d7fbf3c Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 19 Mar 2020 01:13:19 +0100 Subject: [PATCH 120/431] Updated frontend to 20200318.1 (#32957) --- 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 fc9cd188565..df484edf137 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==20200318.0" + "home-assistant-frontend==20200318.1" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index cc1486944b4..4b16757a361 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200318.0 +home-assistant-frontend==20200318.1 importlib-metadata==1.5.0 jinja2>=2.10.3 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index dd7885af5ba..34511565d70 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -696,7 +696,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200318.0 +home-assistant-frontend==20200318.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e71929dadbd..bcf77f1a4fd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -266,7 +266,7 @@ hole==0.5.0 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200318.0 +home-assistant-frontend==20200318.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.9 From 5123baba3ff4ac29d45a7d477a24977099998a32 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Mar 2020 18:14:25 -0700 Subject: [PATCH 121/431] Fix zone config (#32963) * Fix zone config * Add zone as dependency again to device tracker * Fix tests --- .../components/device_tracker/manifest.json | 4 +-- homeassistant/components/zone/__init__.py | 24 ++++++++++++--- tests/components/unifi/test_device_tracker.py | 24 +++++++-------- tests/components/unifi/test_sensor.py | 4 +-- tests/components/unifi/test_switch.py | 30 +++++++++---------- tests/components/zone/test_init.py | 15 ++++++++++ 6 files changed, 66 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/device_tracker/manifest.json b/homeassistant/components/device_tracker/manifest.json index 2d0e9a82a53..4bd9846f76d 100644 --- a/homeassistant/components/device_tracker/manifest.json +++ b/homeassistant/components/device_tracker/manifest.json @@ -3,8 +3,8 @@ "name": "Device Tracker", "documentation": "https://www.home-assistant.io/integrations/device_tracker", "requirements": [], - "dependencies": [], - "after_dependencies": ["zone"], + "dependencies": ["zone"], + "after_dependencies": [], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index 08304df8137..f81bd814651 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_RADIUS, EVENT_CORE_CONFIG_UPDATE, SERVICE_RELOAD, + STATE_UNAVAILABLE, ) from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback from homeassistant.helpers import ( @@ -65,8 +66,20 @@ UPDATE_FIELDS = { } +def empty_value(value): + """Test if the user has the default config value from adding "zone:".""" + if isinstance(value, dict) and len(value) == 0: + return [] + + raise vol.Invalid("Not a default value") + + CONFIG_SCHEMA = vol.Schema( - {vol.Optional(DOMAIN): vol.All(cv.ensure_list, [vol.Schema(CREATE_FIELDS)])}, + { + vol.Optional(DOMAIN, default=[]): vol.Any( + vol.All(cv.ensure_list, [vol.Schema(CREATE_FIELDS)]), empty_value, + ) + }, extra=vol.ALLOW_EXTRA, ) @@ -93,7 +106,7 @@ def async_active_zone( closest = None for zone in zones: - if zone.attributes.get(ATTR_PASSIVE): + if zone.state == STATE_UNAVAILABLE or zone.attributes.get(ATTR_PASSIVE): continue zone_dist = distance( @@ -126,6 +139,9 @@ def in_zone(zone: State, latitude: float, longitude: float, radius: float = 0) - Async friendly. """ + if zone.state == STATE_UNAVAILABLE: + return False + zone_dist = distance( latitude, longitude, @@ -180,7 +196,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: component, storage_collection, lambda conf: Zone(conf, True) ) - if DOMAIN in config: + if config[DOMAIN]: await yaml_collection.async_load(config[DOMAIN]) await storage_collection.async_load() @@ -206,7 +222,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: conf = await component.async_prepare_reload(skip_reset=True) if conf is None: return - await yaml_collection.async_load(conf.get(DOMAIN, [])) + await yaml_collection.async_load(conf[DOMAIN]) service.async_register_admin_service( hass, diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index e9ffad5a8b4..cfb4637a6c4 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -109,7 +109,7 @@ async def test_no_clients(hass): """Test the update_clients function when no clients are found.""" await setup_unifi_integration(hass) - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("device_tracker")) == 0 async def test_tracked_devices(hass): @@ -124,7 +124,7 @@ async def test_tracked_devices(hass): devices_response=[DEVICE_1, DEVICE_2], known_wireless_clients=(CLIENT_4["mac"],), ) - assert len(hass.states.async_all()) == 5 + assert len(hass.states.async_entity_ids("device_tracker")) == 5 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -185,7 +185,7 @@ async def test_controller_state_change(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids("device_tracker")) == 2 # Controller unavailable controller.async_unifi_signalling_callback( @@ -215,7 +215,7 @@ async def test_option_track_clients(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_entity_ids("device_tracker")) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -260,7 +260,7 @@ async def test_option_track_wired_clients(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_entity_ids("device_tracker")) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -305,7 +305,7 @@ async def test_option_track_devices(hass): controller = await setup_unifi_integration( hass, clients_response=[CLIENT_1, CLIENT_2], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_entity_ids("device_tracker")) == 3 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -350,7 +350,7 @@ async def test_option_ssid_filter(hass): controller = await setup_unifi_integration( hass, options={CONF_SSID_FILTER: ["ssid"]}, clients_response=[CLIENT_3], ) - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("device_tracker")) == 0 # SSID filter active client_3 = hass.states.get("device_tracker.client_3") @@ -388,7 +388,7 @@ async def test_wireless_client_go_wired_issue(hass): client_1_client["last_seen"] = dt_util.as_timestamp(dt_util.utcnow()) controller = await setup_unifi_integration(hass, clients_response=[client_1_client]) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("device_tracker")) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -461,7 +461,7 @@ async def test_restoring_client(hass): clients_response=[CLIENT_2], clients_all_response=[CLIENT_1], ) - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids("device_tracker")) == 2 device_1 = hass.states.get("device_tracker.client_1") assert device_1 is not None @@ -475,7 +475,7 @@ async def test_dont_track_clients(hass): clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("device_tracker")) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is None @@ -493,7 +493,7 @@ async def test_dont_track_devices(hass): clients_response=[CLIENT_1], devices_response=[DEVICE_1], ) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("device_tracker")) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None @@ -510,7 +510,7 @@ async def test_dont_track_wired_clients(hass): options={unifi.controller.CONF_TRACK_WIRED_CLIENTS: False}, clients_response=[CLIENT_1, CLIENT_2], ) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("device_tracker")) == 1 client_1 = hass.states.get("device_tracker.client_1") assert client_1 is not None diff --git a/tests/components/unifi/test_sensor.py b/tests/components/unifi/test_sensor.py index a858bc9a649..91531a9ee38 100644 --- a/tests/components/unifi/test_sensor.py +++ b/tests/components/unifi/test_sensor.py @@ -55,7 +55,7 @@ async def test_no_clients(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("sensor")) == 0 async def test_sensors(hass): @@ -71,7 +71,7 @@ async def test_sensors(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 4 + assert len(hass.states.async_entity_ids("sensor")) == 4 wired_client_rx = hass.states.get("sensor.wired_client_name_rx") assert wired_client_rx.state == "1234.0" diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index a06be14024b..a6b33c2aa34 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -209,7 +209,7 @@ async def test_no_clients(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("switch")) == 0 async def test_controller_not_client(hass): @@ -222,7 +222,7 @@ async def test_controller_not_client(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("switch")) == 0 cloudkey = hass.states.get("switch.cloud_key") assert cloudkey is None @@ -240,7 +240,7 @@ async def test_not_admin(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("switch")) == 0 async def test_switches(hass): @@ -258,7 +258,7 @@ async def test_switches(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_entity_ids("switch")) == 3 switch_1 = hass.states.get("switch.poe_client_1") assert switch_1 is not None @@ -312,7 +312,7 @@ async def test_new_client_discovered_on_block_control(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("switch")) == 0 blocked = hass.states.get("switch.block_client_1") assert blocked is None @@ -324,7 +324,7 @@ async def test_new_client_discovered_on_block_control(hass): controller.api.session_handler("data") await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("switch")) == 1 blocked = hass.states.get("switch.block_client_1") assert blocked is not None @@ -336,7 +336,7 @@ async def test_option_block_clients(hass): options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, clients_all_response=[BLOCKED, UNBLOCKED], ) - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("switch")) == 1 # Add a second switch hass.config_entries.async_update_entry( @@ -344,28 +344,28 @@ async def test_option_block_clients(hass): options={CONF_BLOCK_CLIENT: [BLOCKED["mac"], UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids("switch")) == 2 # Remove the second switch again hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: [BLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("switch")) == 1 # Enable one and remove another one hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: [UNBLOCKED["mac"]]}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("switch")) == 1 # Remove one hass.config_entries.async_update_entry( controller.config_entry, options={CONF_BLOCK_CLIENT: []}, ) await hass.async_block_till_done() - assert len(hass.states.async_all()) == 0 + assert len(hass.states.async_entity_ids("switch")) == 0 async def test_new_client_discovered_on_poe_control(hass): @@ -378,7 +378,7 @@ async def test_new_client_discovered_on_poe_control(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 1 + assert len(hass.states.async_entity_ids("switch")) == 1 controller.api.websocket._data = { "meta": {"message": "sta:sync"}, @@ -391,7 +391,7 @@ async def test_new_client_discovered_on_poe_control(hass): "switch", "turn_off", {"entity_id": "switch.poe_client_1"}, blocking=True ) assert len(controller.mock_requests) == 5 - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids("switch")) == 2 assert controller.mock_requests[4] == { "json": { "port_overrides": [{"port_idx": 1, "portconf_id": "1a1", "poe_mode": "off"}] @@ -430,7 +430,7 @@ async def test_ignore_multiple_poe_clients_on_same_port(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 3 + assert len(hass.states.async_entity_ids("device_tracker")) == 3 switch_1 = hass.states.get("switch.poe_client_1") switch_2 = hass.states.get("switch.poe_client_2") @@ -481,7 +481,7 @@ async def test_restoring_client(hass): ) assert len(controller.mock_requests) == 4 - assert len(hass.states.async_all()) == 2 + assert len(hass.states.async_entity_ids("switch")) == 2 device_1 = hass.states.get("switch.client_1") assert device_1 is not None diff --git a/tests/components/zone/test_init.py b/tests/components/zone/test_init.py index 0835b77579a..bf92a6aa12a 100644 --- a/tests/components/zone/test_init.py +++ b/tests/components/zone/test_init.py @@ -487,3 +487,18 @@ async def test_import_config_entry(hass): assert state.attributes[zone.ATTR_RADIUS] == 3 assert state.attributes[zone.ATTR_PASSIVE] is False assert state.attributes[ATTR_ICON] == "mdi:from-config-entry" + + +async def test_zone_empty_setup(hass): + """Set up zone with empty config.""" + assert await setup.async_setup_component(hass, DOMAIN, {"zone": {}}) + + +async def test_unavailable_zone(hass): + """Test active zone with unavailable zones.""" + assert await setup.async_setup_component(hass, DOMAIN, {"zone": {}}) + hass.states.async_set("zone.bla", "unavailable", {"restored": True}) + + assert zone.async_active_zone(hass, 0.0, 0.01) is None + + assert zone.in_zone(hass.states.get("zone.bla"), 0, 0) is False From 08aa4b098c6da2ae3e72ebde070fa292945f6346 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Mar 2020 18:15:23 -0700 Subject: [PATCH 122/431] Add device automation as frontend dependency (#32962) --- homeassistant/components/frontend/manifest.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index df484edf137..e5f6c3e2a26 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -8,6 +8,7 @@ "dependencies": [ "api", "auth", + "device_automation", "http", "lovelace", "onboarding", @@ -19,4 +20,4 @@ "@home-assistant/frontend" ], "quality_scale": "internal" -} \ No newline at end of file +} From f6ce5f2d051c91b688d8fe815ad77b62664334b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Mar 2020 18:20:04 -0700 Subject: [PATCH 123/431] Fix type --- homeassistant/components/zone/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index f81bd814651..b1d784a7acb 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -1,6 +1,6 @@ """Support for the definition of zones.""" import logging -from typing import Dict, Optional, cast +from typing import Any, Dict, Optional, cast import voluptuous as vol @@ -66,7 +66,7 @@ UPDATE_FIELDS = { } -def empty_value(value): +def empty_value(value: Any) -> Any: """Test if the user has the default config value from adding "zone:".""" if isinstance(value, dict) and len(value) == 0: return [] @@ -237,7 +237,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: home_zone = Zone(_home_conf(hass), True,) home_zone.entity_id = ENTITY_ID_HOME - await component.async_add_entities([home_zone]) + await component.async_add_entities([home_zone]) # type: ignore async def core_config_updated(_: Event) -> None: """Handle core config updated.""" From 5c1dc60505ce537e1910589120690ac0b37950c1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 18 Mar 2020 22:13:37 -0700 Subject: [PATCH 124/431] Fix mobile app test --- tests/components/mobile_app/test_webhook.py | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/tests/components/mobile_app/test_webhook.py b/tests/components/mobile_app/test_webhook.py index 974fb577606..1e8441290bb 100644 --- a/tests/components/mobile_app/test_webhook.py +++ b/tests/components/mobile_app/test_webhook.py @@ -140,16 +140,7 @@ async def test_webhook_update_registration(webhook_client, authed_api_client): async def test_webhook_handle_get_zones(hass, create_registrations, webhook_client): """Test that we can get zones properly.""" await async_setup_component( - hass, - ZONE_DOMAIN, - { - ZONE_DOMAIN: { - "name": "test", - "latitude": 32.880837, - "longitude": -117.237561, - "radius": 250, - } - }, + hass, ZONE_DOMAIN, {ZONE_DOMAIN: {}}, ) resp = await webhook_client.post( @@ -160,10 +151,9 @@ async def test_webhook_handle_get_zones(hass, create_registrations, webhook_clie assert resp.status == 200 json = await resp.json() - assert len(json) == 2 + assert len(json) == 1 zones = sorted(json, key=lambda entry: entry["entity_id"]) assert zones[0]["entity_id"] == "zone.home" - assert zones[1]["entity_id"] == "zone.test" async def test_webhook_handle_get_config(hass, create_registrations, webhook_client): From e0d2e5dcb044696fa02f581a26d088cee18ea2da Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 19 Mar 2020 06:39:15 +0100 Subject: [PATCH 125/431] Remove deprecated features from MQTT platforms (#32909) * Remove deprecated features * Lint --- homeassistant/components/mqtt/discovery.py | 42 +------ homeassistant/components/mqtt/sensor.py | 31 ----- tests/components/mqtt/test_discovery.py | 121 ++++--------------- tests/components/mqtt/test_light_json.py | 21 +--- tests/components/mqtt/test_light_template.py | 28 +---- tests/components/mqtt/test_sensor.py | 92 -------------- 6 files changed, 30 insertions(+), 305 deletions(-) diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index e6350179571..ac20ba7a4a8 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS -from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC, CONF_STATE_TOPIC +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC _LOGGER = logging.getLogger(__name__) @@ -50,15 +50,6 @@ CONFIG_ENTRY_COMPONENTS = [ "vacuum", ] -DEPRECATED_PLATFORM_TO_SCHEMA = { - "light": {"mqtt_json": "json", "mqtt_template": "template"} -} - -# These components require state_topic to be set. -# If not specified, infer state_topic from discovery topic. -IMPLICIT_STATE_TOPIC_COMPONENTS = ["alarm_control_panel", "binary_sensor", "sensor"] - - ALREADY_DISCOVERED = "mqtt_discovered_components" DATA_CONFIG_ENTRY_LOCK = "mqtt_config_entry_lock" CONFIG_ENTRY_IS_SETUP = "mqtt_config_entry_is_setup" @@ -143,39 +134,8 @@ async def async_start( } setattr(payload, "discovery_data", discovery_data) - if CONF_PLATFORM in payload and "schema" not in payload: - platform = payload[CONF_PLATFORM] - if ( - component in DEPRECATED_PLATFORM_TO_SCHEMA - and platform in DEPRECATED_PLATFORM_TO_SCHEMA[component] - ): - schema = DEPRECATED_PLATFORM_TO_SCHEMA[component][platform] - payload["schema"] = schema - _LOGGER.warning( - '"platform": "%s" is deprecated, ' 'replace with "schema":"%s"', - platform, - schema, - ) payload[CONF_PLATFORM] = "mqtt" - if ( - CONF_STATE_TOPIC not in payload - and component in IMPLICIT_STATE_TOPIC_COMPONENTS - ): - # state_topic not specified, infer from discovery topic - fmt_node_id = f"{node_id}/" if node_id else "" - payload[ - CONF_STATE_TOPIC - ] = f"{discovery_topic}/{component}/{fmt_node_id}{object_id}/state" - _LOGGER.warning( - 'implicit %s is deprecated, add "%s":"%s" to ' - "%s discovery message", - CONF_STATE_TOPIC, - CONF_STATE_TOPIC, - payload[CONF_STATE_TOPIC], - topic, - ) - if ALREADY_DISCOVERED not in hass.data: hass.data[ALREADY_DISCOVERED] = {} if discovery_hash in hass.data[ALREADY_DISCOVERED]: diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 07910697d21..7be923927ca 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -1,6 +1,5 @@ """Support for MQTT sensors.""" from datetime import timedelta -import json import logging from typing import Optional @@ -41,7 +40,6 @@ from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) CONF_EXPIRE_AFTER = "expire_after" -CONF_JSON_ATTRS = "json_attributes" DEFAULT_NAME = "MQTT Sensor" DEFAULT_FORCE_UPDATE = False @@ -53,7 +51,6 @@ PLATFORM_SCHEMA = ( vol.Optional(CONF_EXPIRE_AFTER): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_ICON): cv.icon, - vol.Optional(CONF_JSON_ATTRS, default=[]): cv.ensure_list_csv, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, vol.Optional(CONF_UNIQUE_ID): cv.string, vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string, @@ -110,16 +107,9 @@ class MqttSensor( self._state = None self._sub_state = None self._expiration_trigger = None - self._attributes = None device_config = config.get(CONF_DEVICE) - if config.get(CONF_JSON_ATTRS): - _LOGGER.warning( - 'configuration variable "json_attributes" is ' - 'deprecated, replace with "json_attributes_topic"' - ) - MqttAttributes.__init__(self, config) MqttAvailability.__init__(self, config) MqttDiscoveryUpdate.__init__(self, discovery_data, self.discovery_update) @@ -165,22 +155,6 @@ class MqttSensor( self.hass, self.value_is_expired, expiration_at ) - json_attributes = set(self._config[CONF_JSON_ATTRS]) - if json_attributes: - self._attributes = {} - try: - json_dict = json.loads(payload) - if isinstance(json_dict, dict): - attrs = { - k: json_dict[k] for k in json_attributes & json_dict.keys() - } - self._attributes = attrs - else: - _LOGGER.warning("JSON result was not a dictionary") - except ValueError: - _LOGGER.warning("MQTT payload could not be parsed as JSON") - _LOGGER.debug("Erroneous JSON: %s", payload) - if template is not None: payload = template.async_render_with_possible_json_value( payload, self._state @@ -241,11 +215,6 @@ class MqttSensor( """Return the state of the entity.""" return self._state - @property - def device_state_attributes(self): - """Return the state attributes.""" - return self._attributes - @property def unique_id(self): """Return a unique ID.""" diff --git a/tests/components/mqtt/test_discovery.py b/tests/components/mqtt/test_discovery.py index 4a28b95e32c..8c925bdf315 100644 --- a/tests/components/mqtt/test_discovery.py +++ b/tests/components/mqtt/test_discovery.py @@ -117,7 +117,9 @@ async def test_correct_config_discovery(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() @@ -199,7 +201,9 @@ async def test_discovery_incl_nodeid(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/my_node_id/bla/config", '{ "name": "Beer" }', + hass, + "homeassistant/binary_sensor/my_node_id/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() @@ -217,10 +221,14 @@ async def test_non_duplicate_discovery(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() @@ -240,7 +248,9 @@ async def test_removal(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.beer") @@ -259,7 +269,9 @@ async def test_rediscover(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.beer") @@ -271,7 +283,9 @@ async def test_rediscover(hass, mqtt_mock, caplog): assert state is None async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() state = hass.states.get("binary_sensor.beer") @@ -285,7 +299,9 @@ async def test_duplicate_removal(hass, mqtt_mock, caplog): await async_start(hass, "homeassistant", {}, entry) async_fire_mqtt_message( - hass, "homeassistant/binary_sensor/bla/config", '{ "name": "Beer" }' + hass, + "homeassistant/binary_sensor/bla/config", + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", "") @@ -431,93 +447,6 @@ async def test_missing_discover_abbreviations(hass, mqtt_mock, caplog): assert not missing -async def test_implicit_state_topic_alarm(hass, mqtt_mock, caplog): - """Test implicit state topic for alarm_control_panel.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - - await async_start(hass, "homeassistant", {}, entry) - - data = ( - '{ "name": "Test1",' - ' "command_topic": "homeassistant/alarm_control_panel/bla/cmnd"' - "}" - ) - - async_fire_mqtt_message(hass, "homeassistant/alarm_control_panel/bla/config", data) - await hass.async_block_till_done() - assert ( - "implicit state_topic is deprecated, add " - '"state_topic":"homeassistant/alarm_control_panel/bla/state"' in caplog.text - ) - - state = hass.states.get("alarm_control_panel.Test1") - assert state is not None - assert state.name == "Test1" - assert ("alarm_control_panel", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == "unknown" - - async_fire_mqtt_message( - hass, "homeassistant/alarm_control_panel/bla/state", "armed_away" - ) - - state = hass.states.get("alarm_control_panel.Test1") - assert state.state == "armed_away" - - -async def test_implicit_state_topic_binary_sensor(hass, mqtt_mock, caplog): - """Test implicit state topic for binary_sensor.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - - await async_start(hass, "homeassistant", {}, entry) - - data = '{ "name": "Test1"' "}" - - async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/config", data) - await hass.async_block_till_done() - assert ( - "implicit state_topic is deprecated, add " - '"state_topic":"homeassistant/binary_sensor/bla/state"' in caplog.text - ) - - state = hass.states.get("binary_sensor.Test1") - assert state is not None - assert state.name == "Test1" - assert ("binary_sensor", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == "off" - - async_fire_mqtt_message(hass, "homeassistant/binary_sensor/bla/state", "ON") - - state = hass.states.get("binary_sensor.Test1") - assert state.state == "on" - - -async def test_implicit_state_topic_sensor(hass, mqtt_mock, caplog): - """Test implicit state topic for sensor.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - - await async_start(hass, "homeassistant", {}, entry) - - data = '{ "name": "Test1"' "}" - - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) - await hass.async_block_till_done() - assert ( - "implicit state_topic is deprecated, add " - '"state_topic":"homeassistant/sensor/bla/state"' in caplog.text - ) - - state = hass.states.get("sensor.Test1") - assert state is not None - assert state.name == "Test1" - assert ("sensor", "bla") in hass.data[ALREADY_DISCOVERED] - assert state.state == "unknown" - - async_fire_mqtt_message(hass, "homeassistant/sensor/bla/state", "1234") - - state = hass.states.get("sensor.Test1") - assert state.state == "1234" - - async def test_no_implicit_state_topic_switch(hass, mqtt_mock, caplog): """Test no implicit state topic for switch.""" entry = MockConfigEntry(domain=mqtt.DOMAIN) @@ -552,7 +481,7 @@ async def test_complex_discovery_topic_prefix(hass, mqtt_mock, caplog): async_fire_mqtt_message( hass, ("my_home/homeassistant/register/binary_sensor/node1/object1/config"), - '{ "name": "Beer" }', + '{ "name": "Beer", "state_topic": "test-topic" }', ) await hass.async_block_till_done() diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index 6a8bf10dc3d..c07cec47ecc 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -91,8 +91,7 @@ import json from unittest import mock from unittest.mock import patch -from homeassistant.components import light, mqtt -from homeassistant.components.mqtt.discovery import async_start +from homeassistant.components import light from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, @@ -122,7 +121,7 @@ from .common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import MockConfigEntry, async_fire_mqtt_message, mock_coro +from tests.common import async_fire_mqtt_message, mock_coro from tests.components.light import common DEFAULT_CONFIG = { @@ -1061,22 +1060,6 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) -async def test_discovery_deprecated(hass, mqtt_mock, caplog): - """Test discovery of mqtt_json light with deprecated platform option.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - await async_start(hass, "homeassistant", {"mqtt": {}}, entry) - data = ( - '{ "name": "Beer",' - ' "platform": "mqtt_json",' - ' "command_topic": "test_topic"}' - ) - async_fire_mqtt_message(hass, "homeassistant/light/bla/config", data) - await hass.async_block_till_done() - state = hass.states.get("light.beer") - assert state is not None - assert state.name == "Beer" - - async def test_discovery_update_light(hass, mqtt_mock, caplog): """Test update of discovered light.""" data1 = ( diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index f3965479c14..4f89ca77847 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -28,8 +28,7 @@ If your light doesn't support RGB feature, omit `(red|green|blue)_template`. """ from unittest.mock import patch -from homeassistant.components import light, mqtt -from homeassistant.components.mqtt.discovery import async_start +from homeassistant.components import light from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_SUPPORTED_FEATURES, @@ -59,12 +58,7 @@ from .common import ( help_test_update_with_json_attrs_not_dict, ) -from tests.common import ( - MockConfigEntry, - assert_setup_component, - async_fire_mqtt_message, - mock_coro, -) +from tests.common import assert_setup_component, async_fire_mqtt_message, mock_coro from tests.components.light import common DEFAULT_CONFIG = { @@ -883,24 +877,6 @@ async def test_discovery_removal(hass, mqtt_mock, caplog): await help_test_discovery_removal(hass, mqtt_mock, caplog, light.DOMAIN, data) -async def test_discovery_deprecated(hass, mqtt_mock, caplog): - """Test discovery of mqtt template light with deprecated option.""" - entry = MockConfigEntry(domain=mqtt.DOMAIN) - await async_start(hass, "homeassistant", {"mqtt": {}}, entry) - data = ( - '{ "name": "Beer",' - ' "platform": "mqtt_template",' - ' "command_topic": "test_topic",' - ' "command_on_template": "on",' - ' "command_off_template": "off"}' - ) - async_fire_mqtt_message(hass, "homeassistant/light/bla/config", data) - await hass.async_block_till_done() - state = hass.states.get("light.beer") - assert state is not None - assert state.name == "Beer" - - async def test_discovery_update_light(hass, mqtt_mock, caplog): """Test update of discovered light.""" data1 = ( diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 5e472efcc89..0455c5f9c7c 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -240,98 +240,6 @@ async def test_custom_availability_payload(hass, mqtt_mock): ) -async def test_setting_sensor_attribute_via_legacy_mqtt_json_message(hass, mqtt_mock): - """Test the setting of attribute via MQTT with JSON payload.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "json_attributes_topic": "test-attributes-topic", - } - }, - ) - - async_fire_mqtt_message(hass, "test-attributes-topic", '{ "val": "100" }') - state = hass.states.get("sensor.test") - - assert state.attributes.get("val") == "100" - - -async def test_update_with_legacy_json_attrs_not_dict(hass, mqtt_mock, caplog): - """Test attributes get extracted from a JSON result.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "json_attributes_topic": "test-attributes-topic", - } - }, - ) - - async_fire_mqtt_message(hass, "test-attributes-topic", '[ "list", "of", "things"]') - state = hass.states.get("sensor.test") - - assert state.attributes.get("val") is None - assert "JSON result was not a dictionary" in caplog.text - - -async def test_update_with_legacy_json_attrs_bad_JSON(hass, mqtt_mock, caplog): - """Test attributes get extracted from a JSON result.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "json_attributes_topic": "test-attributes-topic", - } - }, - ) - - async_fire_mqtt_message(hass, "test-attributes-topic", "This is not JSON") - - state = hass.states.get("sensor.test") - assert state.attributes.get("val") is None - assert "Erroneous JSON: This is not JSON" in caplog.text - - -async def test_update_with_legacy_json_attrs_and_template(hass, mqtt_mock): - """Test attributes get extracted from a JSON result.""" - assert await async_setup_component( - hass, - sensor.DOMAIN, - { - sensor.DOMAIN: { - "platform": "mqtt", - "name": "test", - "state_topic": "test-topic", - "unit_of_measurement": "fav unit", - "value_template": "{{ value_json.val }}", - "json_attributes": "val", - } - }, - ) - - async_fire_mqtt_message(hass, "test-topic", '{ "val": "100" }') - state = hass.states.get("sensor.test") - - assert state.attributes.get("val") == "100" - assert state.state == "100" - - async def test_invalid_device_class(hass, mqtt_mock): """Test device_class option with invalid value.""" assert await async_setup_component( From 03c906a2f196c4fcb0cf89e32f65eff442d44cea Mon Sep 17 00:00:00 2001 From: Maikel Punie Date: Thu, 19 Mar 2020 07:56:32 +0100 Subject: [PATCH 126/431] Fix velbus in the 107 release (#32936) velbus 2.0.41 introduced some data files in the python module. They are not copied when installing the velbus module. 2.0.43 fixes this problem --- 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 4179c3e89ba..fe3aee9a4cd 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": ["python-velbus==2.0.42"], + "requirements": ["python-velbus==2.0.43"], "config_flow": true, "dependencies": [], "codeowners": ["@Cereal2nd", "@brefra"] diff --git a/requirements_all.txt b/requirements_all.txt index 34511565d70..a6925149f53 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1665,7 +1665,7 @@ python-telnet-vlc==1.0.4 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.42 +python-velbus==2.0.43 # homeassistant.components.vlc python-vlc==1.1.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcf77f1a4fd..f27b756f3bd 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -590,7 +590,7 @@ python-nest==4.1.0 python-twitch-client==0.6.0 # homeassistant.components.velbus -python-velbus==2.0.42 +python-velbus==2.0.43 # homeassistant.components.awair python_awair==0.0.4 From 445ef861c0dd36cbb8b85afc0bd7d47ed982ea96 Mon Sep 17 00:00:00 2001 From: MatthewFlamm <39341281+MatthewFlamm@users.noreply.github.com> Date: Thu, 19 Mar 2020 03:38:24 -0400 Subject: [PATCH 127/431] Add pending to template alarm (#31614) --- .../template/alarm_control_panel.py | 6 ++++-- .../template/test_alarm_control_panel.py | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/template/alarm_control_panel.py b/homeassistant/components/template/alarm_control_panel.py index 019c9cd8787..45cccef9766 100644 --- a/homeassistant/components/template/alarm_control_panel.py +++ b/homeassistant/components/template/alarm_control_panel.py @@ -24,6 +24,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ) @@ -38,9 +39,10 @@ _LOGGER = logging.getLogger(__name__) _VALID_STATES = [ STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, - STATE_ALARM_TRIGGERED, STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, STATE_UNAVAILABLE, ] diff --git a/tests/components/template/test_alarm_control_panel.py b/tests/components/template/test_alarm_control_panel.py index 36c639bc95b..6d2e4e48279 100644 --- a/tests/components/template/test_alarm_control_panel.py +++ b/tests/components/template/test_alarm_control_panel.py @@ -7,6 +7,8 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, STATE_ALARM_DISARMED, + STATE_ALARM_PENDING, + STATE_ALARM_TRIGGERED, ) from tests.common import async_mock_service @@ -79,6 +81,24 @@ async def test_template_state_text(hass): state = hass.states.get("alarm_control_panel.test_template_panel") assert state.state == STATE_ALARM_DISARMED + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_PENDING) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == STATE_ALARM_PENDING + + hass.states.async_set("alarm_control_panel.test", STATE_ALARM_TRIGGERED) + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == STATE_ALARM_TRIGGERED + + hass.states.async_set("alarm_control_panel.test", "invalid_state") + await hass.async_block_till_done() + + state = hass.states.get("alarm_control_panel.test_template_panel") + assert state.state == "unknown" + async def test_optimistic_states(hass): """Test the optimistic state.""" From 242aff9269f66e53af088e5b6bb20bf4e18d371f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 19 Mar 2020 11:44:52 -0400 Subject: [PATCH 128/431] Fix mypy in ci by removing unneeded ignore in zone init (#32997) --- homeassistant/components/zone/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index b1d784a7acb..74c145e19d9 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -237,7 +237,7 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: home_zone = Zone(_home_conf(hass), True,) home_zone.entity_id = ENTITY_ID_HOME - await component.async_add_entities([home_zone]) # type: ignore + await component.async_add_entities([home_zone]) async def core_config_updated(_: Event) -> None: """Handle core config updated.""" From 5b4d2aed64c08ad9d0142f12bfd4519c83e69cab Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2020 10:50:17 -0500 Subject: [PATCH 129/431] Add Powerwalls integration (#32851) * Create an integration for Powerwalls * Self review :: remove unused code * Remove debug * Update homeassistant/components/powerwall/__init__.py Co-Authored-By: Martin Hjelmare * _call_site_info to module level * Update homeassistant/components/powerwall/binary_sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/binary_sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/binary_sensor.py Co-Authored-By: Martin Hjelmare * Update tests/components/powerwall/test_binary_sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/binary_sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/binary_sensor.py Co-Authored-By: Martin Hjelmare * remove sensors that I added tests for from the comment * Update homeassistant/components/powerwall/config_flow.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/sensor.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/powerwall/sensor.py Co-Authored-By: Martin Hjelmare * Switch to UNIT_PERCENTAGE * reduce code * Add test for import * Adjust tests * Add missing file Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + .../components/powerwall/__init__.py | 130 +++++++++++++++++ .../components/powerwall/binary_sensor.py | 131 ++++++++++++++++++ .../components/powerwall/config_flow.py | 72 ++++++++++ homeassistant/components/powerwall/const.py | 40 ++++++ homeassistant/components/powerwall/entity.py | 67 +++++++++ .../components/powerwall/manifest.json | 16 +++ homeassistant/components/powerwall/sensor.py | 116 ++++++++++++++++ .../components/powerwall/strings.json | 20 +++ homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/powerwall/__init__.py | 1 + tests/components/powerwall/mocks.py | 56 ++++++++ .../powerwall/test_binary_sensor.py | 54 ++++++++ .../components/powerwall/test_config_flow.py | 93 +++++++++++++ tests/components/powerwall/test_sensor.py | 103 ++++++++++++++ tests/fixtures/powerwall/meters.json | 62 +++++++++ tests/fixtures/powerwall/site_info.json | 20 +++ tests/fixtures/powerwall/sitemaster.json | 1 + 20 files changed, 990 insertions(+) create mode 100644 homeassistant/components/powerwall/__init__.py create mode 100644 homeassistant/components/powerwall/binary_sensor.py create mode 100644 homeassistant/components/powerwall/config_flow.py create mode 100644 homeassistant/components/powerwall/const.py create mode 100644 homeassistant/components/powerwall/entity.py create mode 100644 homeassistant/components/powerwall/manifest.json create mode 100644 homeassistant/components/powerwall/sensor.py create mode 100644 homeassistant/components/powerwall/strings.json create mode 100644 tests/components/powerwall/__init__.py create mode 100644 tests/components/powerwall/mocks.py create mode 100644 tests/components/powerwall/test_binary_sensor.py create mode 100644 tests/components/powerwall/test_config_flow.py create mode 100644 tests/components/powerwall/test_sensor.py create mode 100644 tests/fixtures/powerwall/meters.json create mode 100644 tests/fixtures/powerwall/site_info.json create mode 100644 tests/fixtures/powerwall/sitemaster.json diff --git a/CODEOWNERS b/CODEOWNERS index 9730c96a573..31db27145e4 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -277,6 +277,7 @@ homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew homeassistant/components/point/* @fredrike +homeassistant/components/powerwall/* @bdraco homeassistant/components/proxmoxve/* @k4ds3 homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py new file mode 100644 index 00000000000..a5401206379 --- /dev/null +++ b/homeassistant/components/powerwall/__init__.py @@ -0,0 +1,130 @@ +"""The Tesla Powerwall integration.""" +import asyncio +from datetime import timedelta +import logging + +from tesla_powerwall import ( + ApiError, + MetersResponse, + PowerWall, + PowerWallUnreachableError, +) +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_IP_ADDRESS +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import ( + DOMAIN, + POWERWALL_API_CHARGE, + POWERWALL_API_GRID_STATUS, + POWERWALL_API_METERS, + POWERWALL_API_SITEMASTER, + POWERWALL_COORDINATOR, + POWERWALL_OBJECT, + POWERWALL_SITE_INFO, + UPDATE_INTERVAL, +) + +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: vol.Schema({vol.Required(CONF_IP_ADDRESS): cv.string})}, + extra=vol.ALLOW_EXTRA, +) + +PLATFORMS = ["binary_sensor", "sensor"] + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Tesla Powerwall component.""" + hass.data.setdefault(DOMAIN, {}) + conf = config.get(DOMAIN) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Tesla Powerwall from a config entry.""" + + entry_id = entry.entry_id + + hass.data[DOMAIN].setdefault(entry_id, {}) + power_wall = PowerWall(entry.data[CONF_IP_ADDRESS]) + try: + site_info = await hass.async_add_executor_job(call_site_info, power_wall) + except (PowerWallUnreachableError, ApiError, ConnectionError): + raise ConfigEntryNotReady + + async def async_update_data(): + """Fetch data from API endpoint.""" + return await hass.async_add_executor_job(_fetch_powerwall_data, power_wall) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="Powerwall site", + update_method=async_update_data, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + hass.data[DOMAIN][entry.entry_id] = { + POWERWALL_OBJECT: power_wall, + POWERWALL_COORDINATOR: coordinator, + POWERWALL_SITE_INFO: site_info, + } + + await coordinator.async_refresh() + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +def call_site_info(power_wall): + """Wrap site_info to be a callable.""" + return power_wall.site_info + + +def _fetch_powerwall_data(power_wall): + """Process and update powerwall data.""" + meters = power_wall.meters + return { + POWERWALL_API_CHARGE: power_wall.charge, + POWERWALL_API_SITEMASTER: power_wall.sitemaster, + POWERWALL_API_METERS: { + meter: MetersResponse(meters[meter]) for meter in meters + }, + POWERWALL_API_GRID_STATUS: power_wall.grid_status, + } + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py new file mode 100644 index 00000000000..52b82531472 --- /dev/null +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -0,0 +1,131 @@ +"""Support for August sensors.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, +) +from homeassistant.const import DEVICE_CLASS_POWER + +from .const import ( + ATTR_GRID_CODE, + ATTR_NOMINAL_SYSTEM_POWER, + ATTR_REGION, + DOMAIN, + POWERWALL_API_GRID_STATUS, + POWERWALL_API_SITEMASTER, + POWERWALL_CONNECTED_KEY, + POWERWALL_COORDINATOR, + POWERWALL_GRID_ONLINE, + POWERWALL_RUNNING_KEY, + POWERWALL_SITE_INFO, + SITE_INFO_GRID_CODE, + SITE_INFO_NOMINAL_SYSTEM_POWER_KW, + SITE_INFO_REGION, +) +from .entity import PowerWallEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the August sensors.""" + powerwall_data = hass.data[DOMAIN][config_entry.entry_id] + + coordinator = powerwall_data[POWERWALL_COORDINATOR] + site_info = powerwall_data[POWERWALL_SITE_INFO] + + entities = [] + for sensor_class in ( + PowerWallRunningSensor, + PowerWallGridStatusSensor, + PowerWallConnectedSensor, + ): + entities.append(sensor_class(coordinator, site_info)) + + async_add_entities(entities, True) + + +class PowerWallRunningSensor(PowerWallEntity, BinarySensorDevice): + """Representation of an Powerwall running sensor.""" + + @property + def name(self): + """Device Name.""" + return "Powerwall Status" + + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_POWER + + @property + def unique_id(self): + """Device Uniqueid.""" + return f"{self.base_unique_id}_running" + + @property + def is_on(self): + """Get the powerwall running state.""" + return self._coordinator.data[POWERWALL_API_SITEMASTER][POWERWALL_RUNNING_KEY] + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return { + ATTR_REGION: self._site_info[SITE_INFO_REGION], + ATTR_GRID_CODE: self._site_info[SITE_INFO_GRID_CODE], + ATTR_NOMINAL_SYSTEM_POWER: self._site_info[ + SITE_INFO_NOMINAL_SYSTEM_POWER_KW + ], + } + + +class PowerWallConnectedSensor(PowerWallEntity, BinarySensorDevice): + """Representation of an Powerwall connected sensor.""" + + @property + def name(self): + """Device Name.""" + return "Powerwall Connected to Tesla" + + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_CONNECTIVITY + + @property + def unique_id(self): + """Device Uniqueid.""" + return f"{self.base_unique_id}_connected_to_tesla" + + @property + def is_on(self): + """Get the powerwall connected to tesla state.""" + return self._coordinator.data[POWERWALL_API_SITEMASTER][POWERWALL_CONNECTED_KEY] + + +class PowerWallGridStatusSensor(PowerWallEntity, BinarySensorDevice): + """Representation of an Powerwall grid status sensor.""" + + @property + def name(self): + """Device Name.""" + return "Grid Status" + + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_POWER + + @property + def unique_id(self): + """Device Uniqueid.""" + return f"{self.base_unique_id}_grid_status" + + @property + def is_on(self): + """Get the current value in kWh.""" + return ( + self._coordinator.data[POWERWALL_API_GRID_STATUS] == POWERWALL_GRID_ONLINE + ) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py new file mode 100644 index 00000000000..e94b0cd4056 --- /dev/null +++ b/homeassistant/components/powerwall/config_flow.py @@ -0,0 +1,72 @@ +"""Config flow for Tesla Powerwall integration.""" +import logging + +from tesla_powerwall import ApiError, PowerWall, PowerWallUnreachableError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_IP_ADDRESS + +from . import call_site_info +from .const import DOMAIN # pylint:disable=unused-import +from .const import POWERWALL_SITE_NAME + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_IP_ADDRESS): str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + power_wall = PowerWall(data[CONF_IP_ADDRESS]) + + try: + site_info = await hass.async_add_executor_job(call_site_info, power_wall) + except (PowerWallUnreachableError, ApiError, ConnectionError): + raise CannotConnect + + # Return info that you want to store in the config entry. + return {"title": site_info[POWERWALL_SITE_NAME]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tesla Powerwall.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + 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" + + if "base" not in errors: + await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) + 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 + ) + + async def async_step_import(self, user_input): + """Handle import.""" + await self.async_set_unique_id(user_input[CONF_IP_ADDRESS]) + self._abort_if_unique_id_configured() + + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py new file mode 100644 index 00000000000..59accc9e9a3 --- /dev/null +++ b/homeassistant/components/powerwall/const.py @@ -0,0 +1,40 @@ +"""Constants for the Tesla Powerwall integration.""" + +DOMAIN = "powerwall" + +POWERWALL_SITE_NAME = "site_name" + +POWERWALL_OBJECT = "powerwall" +POWERWALL_COORDINATOR = "coordinator" +POWERWALL_SITE_INFO = "site_info" + +UPDATE_INTERVAL = 60 + +ATTR_REGION = "region" +ATTR_GRID_CODE = "grid_code" +ATTR_FREQUENCY = "frequency" +ATTR_ENERGY_EXPORTED = "energy_exported" +ATTR_ENERGY_IMPORTED = "energy_imported" +ATTR_INSTANT_AVERAGE_VOLTAGE = "instant_average_voltage" +ATTR_NOMINAL_SYSTEM_POWER = "nominal_system_power_kW" + +SITE_INFO_UTILITY = "utility" +SITE_INFO_GRID_CODE = "grid_code" +SITE_INFO_NOMINAL_SYSTEM_POWER_KW = "nominal_system_power_kW" +SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH = "nominal_system_energy_kWh" +SITE_INFO_REGION = "region" + +POWERWALL_SITE_NAME = "site_name" + +POWERWALL_API_METERS = "meters" +POWERWALL_API_CHARGE = "charge" +POWERWALL_API_GRID_STATUS = "grid_status" +POWERWALL_API_SITEMASTER = "sitemaster" + +POWERWALL_GRID_ONLINE = "SystemGridConnected" +POWERWALL_CONNECTED_KEY = "connected_to_tesla" +POWERWALL_RUNNING_KEY = "running" + + +MODEL = "PowerWall 2" +MANUFACTURER = "Tesla" diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py new file mode 100644 index 00000000000..0411f956bdc --- /dev/null +++ b/homeassistant/components/powerwall/entity.py @@ -0,0 +1,67 @@ +"""The Tesla Powerwall integration base entity.""" + +from homeassistant.helpers.entity import Entity + +from .const import ( + DOMAIN, + MANUFACTURER, + MODEL, + POWERWALL_SITE_NAME, + SITE_INFO_GRID_CODE, + SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH, + SITE_INFO_NOMINAL_SYSTEM_POWER_KW, + SITE_INFO_UTILITY, +) + + +class PowerWallEntity(Entity): + """Base class for powerwall entities.""" + + def __init__(self, coordinator, site_info): + """Initialize the sensor.""" + super().__init__() + self._coordinator = coordinator + self._site_info = site_info + # This group of properties will be unique to to the site + unique_group = ( + site_info[SITE_INFO_UTILITY], + site_info[SITE_INFO_GRID_CODE], + str(site_info[SITE_INFO_NOMINAL_SYSTEM_POWER_KW]), + str(site_info[SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH]), + ) + self.base_unique_id = "_".join(unique_group) + + @property + def device_info(self): + """Powerwall device info.""" + return { + "identifiers": {(DOMAIN, self.base_unique_id)}, + "name": self._site_info[POWERWALL_SITE_NAME], + "manufacturer": MANUFACTURER, + "model": MODEL, + } + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Undo subscription.""" + self._coordinator.async_remove_listener(self.async_write_ha_state) diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json new file mode 100644 index 00000000000..ed90bc339fc --- /dev/null +++ b/homeassistant/components/powerwall/manifest.json @@ -0,0 +1,16 @@ +{ + "domain": "powerwall", + "name": "Tesla Powerwall", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/powerwall", + "requirements": [ + "tesla-powerwall==0.1.1" + ], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": [ + "@bdraco" + ] +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py new file mode 100644 index 00000000000..3ebb467d4fc --- /dev/null +++ b/homeassistant/components/powerwall/sensor.py @@ -0,0 +1,116 @@ +"""Support for August sensors.""" +import logging + +from homeassistant.const import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_POWER, + ENERGY_KILO_WATT_HOUR, + UNIT_PERCENTAGE, +) + +from .const import ( + ATTR_ENERGY_EXPORTED, + ATTR_ENERGY_IMPORTED, + ATTR_FREQUENCY, + ATTR_INSTANT_AVERAGE_VOLTAGE, + DOMAIN, + POWERWALL_API_CHARGE, + POWERWALL_API_METERS, + POWERWALL_COORDINATOR, + POWERWALL_SITE_INFO, +) +from .entity import PowerWallEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the August sensors.""" + powerwall_data = hass.data[DOMAIN][config_entry.entry_id] + _LOGGER.debug("Powerwall_data: %s", powerwall_data) + + coordinator = powerwall_data[POWERWALL_COORDINATOR] + site_info = powerwall_data[POWERWALL_SITE_INFO] + + entities = [] + for meter in coordinator.data[POWERWALL_API_METERS]: + entities.append(PowerWallEnergySensor(meter, coordinator, site_info)) + + entities.append(PowerWallChargeSensor(coordinator, site_info)) + + async_add_entities(entities, True) + + +class PowerWallChargeSensor(PowerWallEntity): + """Representation of an Powerwall charge sensor.""" + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return UNIT_PERCENTAGE + + @property + def name(self): + """Device Name.""" + return "Powerwall Charge" + + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_BATTERY + + @property + def unique_id(self): + """Device Uniqueid.""" + return f"{self.base_unique_id}_charge" + + @property + def state(self): + """Get the current value in percentage.""" + return round(self._coordinator.data[POWERWALL_API_CHARGE], 3) + + +class PowerWallEnergySensor(PowerWallEntity): + """Representation of an Powerwall Energy sensor.""" + + def __init__(self, meter, coordinator, site_info): + """Initialize the sensor.""" + super().__init__(coordinator, site_info) + self._meter = meter + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return ENERGY_KILO_WATT_HOUR + + @property + def name(self): + """Device Name.""" + return f"Powerwall {self._meter.title()} Now" + + @property + def device_class(self): + """Device Class.""" + return DEVICE_CLASS_POWER + + @property + def unique_id(self): + """Device Uniqueid.""" + return f"{self.base_unique_id}_{self._meter}_instant_power" + + @property + def state(self): + """Get the current value in kWh.""" + meter = self._coordinator.data[POWERWALL_API_METERS][self._meter] + return round(float(meter.instant_power / 1000), 3) + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + meter = self._coordinator.data[POWERWALL_API_METERS][self._meter] + return { + ATTR_FREQUENCY: meter.frequency, + ATTR_ENERGY_EXPORTED: meter.energy_exported, + ATTR_ENERGY_IMPORTED: meter.energy_imported, + ATTR_INSTANT_AVERAGE_VOLTAGE: meter.instant_average_voltage, + } diff --git a/homeassistant/components/powerwall/strings.json b/homeassistant/components/powerwall/strings.json new file mode 100644 index 00000000000..92f0fd19464 --- /dev/null +++ b/homeassistant/components/powerwall/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "title": "Tesla Powerwall", + "step": { + "user": { + "title": "Connect to the powerwall", + "data": { + "ip_address": "IP Address" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "The powerwall is already configured" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0ca18cec442..fb2f2bdb7a9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -79,6 +79,7 @@ FLOWS = [ "plaato", "plex", "point", + "powerwall", "ps4", "rachio", "rainmachine", diff --git a/requirements_all.txt b/requirements_all.txt index a6925149f53..df442551bb2 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1995,6 +1995,9 @@ temperusb==1.5.3 # homeassistant.components.tensorflow # tensorflow==1.13.2 +# homeassistant.components.powerwall +tesla-powerwall==0.1.1 + # homeassistant.components.tesla teslajsonpy==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f27b756f3bd..614ad22605e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -692,6 +692,9 @@ sunwatcher==0.2.1 # homeassistant.components.tellduslive tellduslive==0.10.10 +# homeassistant.components.powerwall +tesla-powerwall==0.1.1 + # homeassistant.components.tesla teslajsonpy==0.5.1 diff --git a/tests/components/powerwall/__init__.py b/tests/components/powerwall/__init__.py new file mode 100644 index 00000000000..0e43ec085eb --- /dev/null +++ b/tests/components/powerwall/__init__.py @@ -0,0 +1 @@ +"""Tests for the Tesla Powerwall integration.""" diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py new file mode 100644 index 00000000000..330f1123b8f --- /dev/null +++ b/tests/components/powerwall/mocks.py @@ -0,0 +1,56 @@ +"""Mocks for powerwall.""" + +import json +import os + +from asynctest import MagicMock, PropertyMock + +from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.const import CONF_IP_ADDRESS + +from tests.common import load_fixture + + +async def _mock_powerwall_with_fixtures(hass): + """Mock data used to build powerwall state.""" + meters = await _async_load_json_fixture(hass, "meters.json") + sitemaster = await _async_load_json_fixture(hass, "sitemaster.json") + site_info = await _async_load_json_fixture(hass, "site_info.json") + return _mock_powerwall_return_value( + site_info=site_info, + charge=47.31993232, + sitemaster=sitemaster, + meters=meters, + grid_status="SystemGridConnected", + ) + + +def _mock_powerwall_return_value( + site_info=None, charge=None, sitemaster=None, meters=None, grid_status=None +): + powerwall_mock = MagicMock() + type(powerwall_mock).site_info = PropertyMock(return_value=site_info) + type(powerwall_mock).charge = PropertyMock(return_value=charge) + type(powerwall_mock).sitemaster = PropertyMock(return_value=sitemaster) + type(powerwall_mock).meters = PropertyMock(return_value=meters) + type(powerwall_mock).grid_status = PropertyMock(return_value=grid_status) + + return powerwall_mock + + +def _mock_powerwall_side_effect(site_info=None): + powerwall_mock = MagicMock() + type(powerwall_mock).site_info = PropertyMock(side_effect=site_info) + return powerwall_mock + + +async def _async_load_json_fixture(hass, path): + fixture = await hass.async_add_executor_job( + load_fixture, os.path.join("powerwall", path) + ) + return json.loads(fixture) + + +def _mock_get_config(): + """Return a default powerwall config.""" + return {DOMAIN: {CONF_IP_ADDRESS: "1.2.3.4"}} diff --git a/tests/components/powerwall/test_binary_sensor.py b/tests/components/powerwall/test_binary_sensor.py new file mode 100644 index 00000000000..621304793ab --- /dev/null +++ b/tests/components/powerwall/test_binary_sensor.py @@ -0,0 +1,54 @@ +"""The binary sensor tests for the powerwall platform.""" + +from asynctest import patch + +from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.const import STATE_ON +from homeassistant.setup import async_setup_component + +from .mocks import _mock_get_config, _mock_powerwall_with_fixtures + + +async def test_sensors(hass): + """Test creation of the binary sensors.""" + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + with patch( + "homeassistant.components.powerwall.config_flow.PowerWall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.PowerWall", return_value=mock_powerwall, + ): + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.grid_status") + assert state.state == STATE_ON + expected_attributes = {"friendly_name": "Grid Status", "device_class": "power"} + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("binary_sensor.powerwall_status") + assert state.state == STATE_ON + expected_attributes = { + "region": "IEEE1547a:2014", + "grid_code": "60Hz_240V_s_IEEE1547a_2014", + "nominal_system_power_kW": 25, + "friendly_name": "Powerwall Status", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("binary_sensor.powerwall_connected_to_tesla") + assert state.state == STATE_ON + expected_attributes = { + "friendly_name": "Powerwall Connected to Tesla", + "device_class": "connectivity", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/powerwall/test_config_flow.py b/tests/components/powerwall/test_config_flow.py new file mode 100644 index 00000000000..f27d7e1f41b --- /dev/null +++ b/tests/components/powerwall/test_config_flow.py @@ -0,0 +1,93 @@ +"""Test the Powerwall config flow.""" + +from asynctest import patch +from tesla_powerwall import PowerWallUnreachableError + +from homeassistant import config_entries, setup +from homeassistant.components.powerwall.const import DOMAIN, POWERWALL_SITE_NAME +from homeassistant.const import CONF_IP_ADDRESS + +from .mocks import _mock_powerwall_return_value, _mock_powerwall_side_effect + + +async def test_form_source_user(hass): + """Test we get config flow setup form as a user.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_powerwall = _mock_powerwall_return_value( + site_info={POWERWALL_SITE_NAME: "My site"} + ) + + with patch( + "homeassistant.components.powerwall.config_flow.PowerWall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.powerwall.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: "1.2.3.4"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "My site" + assert result2["data"] == {CONF_IP_ADDRESS: "1.2.3.4"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_source_import(hass): + """Test we setup the config entry via import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_powerwall = _mock_powerwall_return_value( + site_info={POWERWALL_SITE_NAME: "Imported site"} + ) + + with patch( + "homeassistant.components.powerwall.config_flow.PowerWall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.powerwall.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_IP_ADDRESS: "1.2.3.4"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Imported site" + assert result["data"] == {CONF_IP_ADDRESS: "1.2.3.4"} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_powerwall = _mock_powerwall_side_effect(site_info=PowerWallUnreachableError) + + with patch( + "homeassistant.components.powerwall.config_flow.PowerWall", + return_value=mock_powerwall, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_IP_ADDRESS: "1.2.3.4"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py new file mode 100644 index 00000000000..ea74f33671f --- /dev/null +++ b/tests/components/powerwall/test_sensor.py @@ -0,0 +1,103 @@ +"""The sensor tests for the powerwall platform.""" + +from asynctest import patch + +from homeassistant.components.powerwall.const import DOMAIN +from homeassistant.setup import async_setup_component + +from .mocks import _mock_get_config, _mock_powerwall_with_fixtures + + +async def test_sensors(hass): + """Test creation of the sensors.""" + + mock_powerwall = await _mock_powerwall_with_fixtures(hass) + + with patch( + "homeassistant.components.powerwall.config_flow.PowerWall", + return_value=mock_powerwall, + ), patch( + "homeassistant.components.powerwall.PowerWall", return_value=mock_powerwall, + ): + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() + + device_registry = await hass.helpers.device_registry.async_get_registry() + reg_device = device_registry.async_get_device( + identifiers={("powerwall", "Wom Energy_60Hz_240V_s_IEEE1547a_2014_25_13.5")}, + connections=set(), + ) + assert reg_device.model == "PowerWall 2" + assert reg_device.manufacturer == "Tesla" + assert reg_device.name == "MySite" + + state = hass.states.get("sensor.powerwall_site_now") + assert state.state == "0.032" + expected_attributes = { + "frequency": 60, + "energy_exported": 10429451.9916853, + "energy_imported": 4824191.60668611, + "instant_average_voltage": 120.650001525879, + "unit_of_measurement": "kWh", + "friendly_name": "Powerwall Site Now", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("sensor.powerwall_load_now") + assert state.state == "1.971" + expected_attributes = { + "frequency": 60, + "energy_exported": 1056797.48917483, + "energy_imported": 4692987.91889705, + "instant_average_voltage": 120.650001525879, + "unit_of_measurement": "kWh", + "friendly_name": "Powerwall Load Now", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("sensor.powerwall_battery_now") + assert state.state == "-8.55" + expected_attributes = { + "frequency": 60.014, + "energy_exported": 3620010, + "energy_imported": 4216170, + "instant_average_voltage": 240.56, + "unit_of_measurement": "kWh", + "friendly_name": "Powerwall Battery Now", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("sensor.powerwall_solar_now") + assert state.state == "10.49" + expected_attributes = { + "frequency": 60, + "energy_exported": 9864205.82222448, + "energy_imported": 28177.5358355867, + "instant_average_voltage": 120.685001373291, + "unit_of_measurement": "kWh", + "friendly_name": "Powerwall Solar Now", + "device_class": "power", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("sensor.powerwall_charge") + assert state.state == "47.32" + expected_attributes = { + "unit_of_measurement": "%", + "friendly_name": "Powerwall Charge", + "device_class": "battery", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/fixtures/powerwall/meters.json b/tests/fixtures/powerwall/meters.json new file mode 100644 index 00000000000..8eb69593d59 --- /dev/null +++ b/tests/fixtures/powerwall/meters.json @@ -0,0 +1,62 @@ +{ + "battery" : { + "instant_power" : -8550, + "i_b_current" : 0, + "instant_average_voltage" : 240.56, + "i_a_current" : 0, + "frequency" : 60.014, + "instant_reactive_power" : 50, + "energy_imported" : 4216170, + "instant_total_current" : 185.5, + "timeout" : 1500000000, + "energy_exported" : 3620010, + "instant_apparent_power" : 8550.14619758048, + "last_communication_time" : "2020-03-15T15:58:53.855997624-05:00", + "i_c_current" : 0 + }, + "load" : { + "i_b_current" : 0, + "instant_average_voltage" : 120.650001525879, + "instant_power" : 1971.46005249023, + "instant_reactive_power" : -2119.58996582031, + "i_a_current" : 0, + "frequency" : 60, + "last_communication_time" : "2020-03-15T15:58:53.853784964-05:00", + "instant_apparent_power" : 2894.70488336392, + "i_c_current" : 0, + "instant_total_current" : 0, + "energy_imported" : 4692987.91889705, + "timeout" : 1500000000, + "energy_exported" : 1056797.48917483 + }, + "solar" : { + "i_a_current" : 0, + "frequency" : 60, + "instant_reactive_power" : -15.2600002288818, + "instant_power" : 10489.6596679688, + "i_b_current" : 0, + "instant_average_voltage" : 120.685001373291, + "timeout" : 1500000000, + "energy_exported" : 9864205.82222448, + "energy_imported" : 28177.5358355867, + "instant_total_current" : 0, + "i_c_current" : 0, + "instant_apparent_power" : 10489.6707678276, + "last_communication_time" : "2020-03-15T15:58:53.853898963-05:00" + }, + "site" : { + "instant_total_current" : 0.263575500367178, + "energy_imported" : 4824191.60668611, + "energy_exported" : 10429451.9916853, + "timeout" : 1500000000, + "last_communication_time" : "2020-03-15T15:58:53.853784964-05:00", + "instant_apparent_power" : 2154.56465790676, + "i_c_current" : 0, + "instant_average_voltage" : 120.650001525879, + "i_b_current" : 0, + "instant_power" : 31.8003845214844, + "instant_reactive_power" : -2154.32996559143, + "i_a_current" : 0, + "frequency" : 60 + } +} diff --git a/tests/fixtures/powerwall/site_info.json b/tests/fixtures/powerwall/site_info.json new file mode 100644 index 00000000000..3bd6ee59e40 --- /dev/null +++ b/tests/fixtures/powerwall/site_info.json @@ -0,0 +1,20 @@ +{ + "state" : "Somewhere", + "utility" : "Wom Energy", + "distributor" : "*", + "max_system_energy_kWh" : 0, + "nominal_system_power_kW" : 25, + "grid_voltage_setting" : 240, + "retailer" : "*", + "grid_code" : "60Hz_240V_s_IEEE1547a_2014", + "timezone" : "America/Chicago", + "nominal_system_energy_kWh" : 13.5, + "region" : "IEEE1547a:2014", + "min_site_meter_power_kW" : -1000000000, + "site_name" : "MySite", + "country" : "United States", + "max_site_meter_power_kW" : 1000000000, + "grid_phase_setting" : "Split", + "max_system_power_kW" : 0, + "grid_freq_setting" : 60 +} diff --git a/tests/fixtures/powerwall/sitemaster.json b/tests/fixtures/powerwall/sitemaster.json new file mode 100644 index 00000000000..a2d6c0dd965 --- /dev/null +++ b/tests/fixtures/powerwall/sitemaster.json @@ -0,0 +1 @@ +{"connected_to_tesla": true, "running": true, "status": "StatusUp"} From d33a3ca90f82f3c721214f61c90aaed6dc78795c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2020 11:29:51 -0500 Subject: [PATCH 130/431] Config flow for harmony (#32919) * Config flow for harmony * Fixes unique ids when using XMPP Co-authored-by: Bram Kragten * Find the unique id for the config flow * move shutdown to init * Add test for ssdp (still failing) * Fix ssdp test * Add harmony to MIGRATED_SERVICE_HANDLERS (this is a breaking change) * more cleanups * use unique id for the config file Co-authored-by: Bram Kragten --- CODEOWNERS | 2 +- .../components/discovery/__init__.py | 2 +- .../components/harmony/.translations/en.json | 38 ++++ homeassistant/components/harmony/__init__.py | 92 +++++++- .../components/harmony/config_flow.py | 211 ++++++++++++++++++ homeassistant/components/harmony/const.py | 2 + .../components/harmony/manifest.json | 9 +- homeassistant/components/harmony/remote.py | 168 +++++++------- homeassistant/components/harmony/strings.json | 38 ++++ homeassistant/components/harmony/util.py | 15 ++ homeassistant/components/remote/__init__.py | 2 +- homeassistant/generated/config_flows.py | 1 + homeassistant/generated/ssdp.py | 6 + requirements_test_all.txt | 3 + tests/components/harmony/__init__.py | 1 + tests/components/harmony/test_config_flow.py | 148 ++++++++++++ 16 files changed, 656 insertions(+), 82 deletions(-) create mode 100644 homeassistant/components/harmony/.translations/en.json create mode 100644 homeassistant/components/harmony/config_flow.py create mode 100644 homeassistant/components/harmony/strings.json create mode 100644 homeassistant/components/harmony/util.py create mode 100644 tests/components/harmony/__init__.py create mode 100644 tests/components/harmony/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 31db27145e4..ccb0cac17ea 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -147,7 +147,7 @@ homeassistant/components/griddy/* @bdraco homeassistant/components/group/* @home-assistant/core homeassistant/components/growatt_server/* @indykoning homeassistant/components/gtfs/* @robbiet480 -homeassistant/components/harmony/* @ehendrix23 +homeassistant/components/harmony/* @ehendrix23 @bramkragten @bdraco homeassistant/components/hassio/* @home-assistant/hass-io homeassistant/components/heatmiser/* @andylockran homeassistant/components/heos/* @andrewsayre diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index d12e9d2c54b..64816acaaf3 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -75,7 +75,6 @@ SERVICE_HANDLERS = { "denonavr": ("media_player", "denonavr"), "frontier_silicon": ("media_player", "frontier_silicon"), "openhome": ("media_player", "openhome"), - "harmony": ("remote", "harmony"), "bose_soundtouch": ("media_player", "soundtouch"), "bluesound": ("media_player", "bluesound"), "songpal": ("media_player", "songpal"), @@ -93,6 +92,7 @@ MIGRATED_SERVICE_HANDLERS = [ "esphome", "google_cast", SERVICE_HEOS, + "harmony", "homekit", "ikea_tradfri", "philips_hue", diff --git a/homeassistant/components/harmony/.translations/en.json b/homeassistant/components/harmony/.translations/en.json new file mode 100644 index 00000000000..b183e067101 --- /dev/null +++ b/homeassistant/components/harmony/.translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Logitech Harmony Hub", + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "user": { + "title": "Setup Logitech Harmony Hub", + "data": { + "host": "Hostname or IP Address", + "name": "Hub Name" + } + }, + "link": { + "title": "Setup Logitech Harmony Hub", + "description": "Do you want to setup {name} ({host})?" + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "description": "Adjust Harmony Hub Options", + "data": { + "activity": "The default activity to execute when none is specified.", + "delay_secs": "The delay between sending commands." + } + } + } + } +} diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 12ccc78077e..0f9824231ea 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -1 +1,91 @@ -"""Support for Harmony devices.""" +"""The Logitech Harmony Hub integration.""" +import asyncio +import logging + +from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN, PLATFORMS +from .remote import DEVICES, HarmonyRemote + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Logitech Harmony Hub component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Logitech Harmony Hub from a config entry.""" + + conf = entry.data + address = conf[CONF_HOST] + name = conf.get(CONF_NAME) + activity = conf.get(ATTR_ACTIVITY) + delay_secs = conf.get(ATTR_DELAY_SECS) + + _LOGGER.info( + "Loading Harmony Platform: %s at %s, startup activity: %s", + name, + address, + activity, + ) + + harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") + try: + device = HarmonyRemote(name, address, activity, harmony_conf_file, delay_secs) + await device.connect() + except (asyncio.TimeoutError, ValueError, AttributeError): + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = device + DEVICES.append(device) + + entry.add_update_listener(_update_listener) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def _update_listener(hass, entry): + """Handle options update.""" + + device = hass.data[DOMAIN][entry.entry_id] + + if ATTR_DELAY_SECS in entry.options: + device.delay_seconds = entry.options[ATTR_DELAY_SECS] + + if ATTR_ACTIVITY in entry.options: + device.default_activity = entry.options[ATTR_ACTIVITY] + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + # Shutdown a harmony remote for removal + device = hass.data[DOMAIN][entry.entry_id] + await device.shutdown() + + if unload_ok: + DEVICES.remove(hass.data[DOMAIN][entry.entry_id]) + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py new file mode 100644 index 00000000000..ddd52dfd008 --- /dev/null +++ b/homeassistant/components/harmony/config_flow.py @@ -0,0 +1,211 @@ +"""Config flow for Logitech Harmony Hub integration.""" +import logging +from urllib.parse import urlparse + +import aioharmony.exceptions as harmony_exceptions +from aioharmony.harmonyapi import HarmonyAPI +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.components import ssdp +from homeassistant.components.remote import ( + ATTR_ACTIVITY, + ATTR_DELAY_SECS, + DEFAULT_DELAY_SECS, +) +from homeassistant.const import CONF_HOST, CONF_NAME +from homeassistant.core import callback + +from .const import DOMAIN, UNIQUE_ID +from .util import find_unique_id_for_remote + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_HOST): str, vol.Required(CONF_NAME): str}, extra=vol.ALLOW_EXTRA +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + harmony = HarmonyAPI(ip_address=data[CONF_HOST]) + + _LOGGER.debug("harmony:%s", harmony) + + try: + if not await harmony.connect(): + await harmony.close() + raise CannotConnect + except harmony_exceptions.TimeOut: + raise CannotConnect + + unique_id = find_unique_id_for_remote(harmony) + await harmony.close() + + # As a last resort we get the name from the harmony client + # in the event a name was not provided. harmony.name is + # usually the ip address but it can be an empty string. + if CONF_NAME not in data or data[CONF_NAME] is None or data[CONF_NAME] == "": + data[CONF_NAME] = harmony.name + + return { + CONF_NAME: data[CONF_NAME], + CONF_HOST: data[CONF_HOST], + UNIQUE_ID: unique_id, + } + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Logitech Harmony Hub.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the Harmony config flow.""" + self.harmony_config = {} + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + + 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" + + if "base" not in errors: + return await self._async_create_entry_from_valid_input(info, user_input) + + # Return form + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_ssdp(self, discovery_info): + """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] + + # pylint: disable=no-member + self.context["title_placeholders"] = {"name": friendly_name} + + self.harmony_config = { + CONF_HOST: parsed_url.hostname, + CONF_NAME: friendly_name, + } + + if self._host_already_configured(self.harmony_config): + return self.async_abort(reason="already_configured") + + return await self.async_step_link() + + async def async_step_link(self, user_input=None): + """Attempt to link with the Harmony.""" + errors = {} + + if user_input is not None: + try: + info = await validate_input(self.hass, self.harmony_config) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + return await self._async_create_entry_from_valid_input(info, user_input) + + return self.async_show_form( + step_id="link", + errors=errors, + description_placeholders={ + CONF_HOST: self.harmony_config[CONF_NAME], + CONF_NAME: self.harmony_config[CONF_HOST], + }, + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @staticmethod + @callback + def async_get_options_flow(config_entry): + """Get the options flow for this handler.""" + return OptionsFlowHandler(config_entry) + + async def _async_create_entry_from_valid_input(self, validated, user_input): + """Single path to create the config entry from validated input.""" + await self.async_set_unique_id(validated[UNIQUE_ID]) + if self._host_already_configured(validated): + return self.async_abort(reason="already_configured") + self._abort_if_unique_id_configured() + config_entry = self.async_create_entry( + title=validated[CONF_NAME], + data={CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]}, + ) + # Options from yaml are preserved + options = _options_from_user_input(user_input) + if options: + config_entry["options"] = options + return config_entry + + def _host_already_configured(self, user_input): + """See if we already have a harmony matching user input configured.""" + existing_hosts = { + entry.data[CONF_HOST] for entry in self._async_current_entries() + } + return user_input[CONF_HOST] in existing_hosts + + +def _options_from_user_input(user_input): + options = {} + if ATTR_ACTIVITY in user_input: + options[ATTR_ACTIVITY] = user_input[ATTR_ACTIVITY] + if ATTR_DELAY_SECS in user_input: + options[ATTR_DELAY_SECS] = user_input[ATTR_DELAY_SECS] + return options + + +class OptionsFlowHandler(config_entries.OptionsFlow): + """Handle a option flow for Harmony.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """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) + + remote = self.hass.data[DOMAIN][self.config_entry.entry_id] + + data_schema = vol.Schema( + { + vol.Optional( + ATTR_DELAY_SECS, + default=self.config_entry.options.get( + ATTR_DELAY_SECS, DEFAULT_DELAY_SECS + ), + ): vol.Coerce(float), + vol.Optional( + ATTR_ACTIVITY, default=self.config_entry.options.get(ATTR_ACTIVITY), + ): vol.In(remote.activity_names), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index 12e71050665..60542845bd0 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -2,3 +2,5 @@ DOMAIN = "harmony" SERVICE_SYNC = "sync" SERVICE_CHANGE_CHANNEL = "change_channel" +PLATFORMS = ["remote"] +UNIQUE_ID = "unique_id" diff --git a/homeassistant/components/harmony/manifest.json b/homeassistant/components/harmony/manifest.json index a0e8baa0b58..870e3f15044 100644 --- a/homeassistant/components/harmony/manifest.json +++ b/homeassistant/components/harmony/manifest.json @@ -4,5 +4,12 @@ "documentation": "https://www.home-assistant.io/integrations/harmony", "requirements": ["aioharmony==0.1.13"], "dependencies": [], - "codeowners": ["@ehendrix23"] + "codeowners": ["@ehendrix23","@bramkragten","@bdraco"], + "ssdp": [ + { + "manufacturer": "Logitech", + "deviceType": "urn:myharmony-com:device:harmony:1" + } + ], + "config_flow": true } diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 126ce0ff992..a5e70b4d807 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -21,36 +21,29 @@ from homeassistant.components.remote import ( DEFAULT_DELAY_SECS, PLATFORM_SCHEMA, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_HOST, - CONF_NAME, - CONF_PORT, - EVENT_HOMEASSISTANT_STOP, -) -from homeassistant.exceptions import PlatformNotReady +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from .const import DOMAIN, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC +from .util import find_unique_id_for_remote _LOGGER = logging.getLogger(__name__) ATTR_CHANNEL = "channel" ATTR_CURRENT_ACTIVITY = "current_activity" -DEFAULT_PORT = 8088 DEVICES = [] -CONF_DEVICE_CACHE = "harmony_device_cache" - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(ATTR_ACTIVITY): cv.string, vol.Required(CONF_NAME): cv.string, vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - } + # The client ignores port so lets not confuse the user by pretenting we do anything with this + }, + extra=vol.ALLOW_EXTRA, ) HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) @@ -65,65 +58,36 @@ HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Harmony platform.""" - activity = None - - if CONF_DEVICE_CACHE not in hass.data: - hass.data[CONF_DEVICE_CACHE] = [] if discovery_info: - # Find the discovered device in the list of user configurations - override = next( - ( - c - for c in hass.data[CONF_DEVICE_CACHE] - if c.get(CONF_NAME) == discovery_info.get(CONF_NAME) - ), - None, - ) - - port = DEFAULT_PORT - delay_secs = DEFAULT_DELAY_SECS - if override is not None: - activity = override.get(ATTR_ACTIVITY) - delay_secs = override.get(ATTR_DELAY_SECS) - port = override.get(CONF_PORT, DEFAULT_PORT) - - host = (discovery_info.get(CONF_NAME), discovery_info.get(CONF_HOST), port) - - # Ignore hub name when checking if this hub is known - ip and port only - if host[1:] in ((h.host, h.port) for h in DEVICES): - _LOGGER.debug("Discovered host already known: %s", host) - return - elif CONF_HOST in config: - host = (config.get(CONF_NAME), config.get(CONF_HOST), config.get(CONF_PORT)) - activity = config.get(ATTR_ACTIVITY) - delay_secs = config.get(ATTR_DELAY_SECS) - else: - hass.data[CONF_DEVICE_CACHE].append(config) + # Now handled by ssdp in the config flow return - name, address, port = host - _LOGGER.info( - "Loading Harmony Platform: %s at %s:%s, startup activity: %s", - name, - address, - port, - activity, + if CONF_HOST not in config: + _LOGGER.error( + "The harmony remote '%s' cannot be setup because configuration now requires a host when configured manually.", + config[CONF_NAME], + ) + return + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) ) - harmony_conf_file = hass.config.path(f"harmony_{slugify(name)}.conf") - try: - device = HarmonyRemote( - name, address, port, activity, harmony_conf_file, delay_secs - ) - if not await device.connect(): - raise PlatformNotReady - DEVICES.append(device) - async_add_entities([device]) - register_services(hass) - except (ValueError, AttributeError): - raise PlatformNotReady +async def async_setup_entry( + hass: HomeAssistant, entry: ConfigEntry, async_add_entities +): + """Set up the Harmony config entry.""" + + device = hass.data[DOMAIN][entry.entry_id] + + _LOGGER.info("Harmony Remote: %s", device) + + async_add_entities([device]) + register_services(hass) def register_services(hass): @@ -165,11 +129,10 @@ async def _change_channel_service(service): class HarmonyRemote(remote.RemoteDevice): """Remote representation used to control a Harmony device.""" - def __init__(self, name, host, port, activity, out_path, delay_secs): + def __init__(self, name, host, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" self._name = name self.host = host - self.port = port self._state = None self._current_activity = None self._default_activity = activity @@ -178,6 +141,39 @@ class HarmonyRemote(remote.RemoteDevice): self._delay_secs = delay_secs self._available = False + @property + def delay_secs(self): + """Delay seconds between sending commands.""" + return self._delay_secs + + @delay_secs.setter + def delay_secs(self, delay_secs): + """Update the delay seconds (from options flow).""" + self._delay_secs = delay_secs + + @property + def default_activity(self): + """Activity used when non specified.""" + return self._default_activity + + @property + def activity_names(self): + """Names of all the remotes activities.""" + activities = [activity["label"] for activity in self._client.config["activity"]] + + # Remove both ways of representing PowerOff + if None in activities: + activities.remove(None) + if "PowerOff" in activities: + activities.remove("PowerOff") + + return activities + + @default_activity.setter + def default_activity(self, activity): + """Update the default activity (from options flow).""" + self._default_activity = activity + async def async_added_to_hass(self): """Complete the initialization.""" _LOGGER.debug("%s: Harmony Hub added", self._name) @@ -193,15 +189,34 @@ class HarmonyRemote(remote.RemoteDevice): # activity await self.new_config() - async def shutdown(_): - """Close connection on shutdown.""" - _LOGGER.debug("%s: Closing Harmony Hub", self._name) - try: - await self._client.close() - except aioexc.TimeOut: - _LOGGER.warning("%s: Disconnect timed-out", self._name) + async def shutdown(self): + """Close connection on shutdown.""" + _LOGGER.debug("%s: Closing Harmony Hub", self._name) + try: + await self._client.close() + except aioexc.TimeOut: + _LOGGER.warning("%s: Disconnect timed-out", self._name) - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown) + @property + def device_info(self): + """Return device info.""" + model = "Harmony Hub" + if "ethernetStatus" in self._client.hub_config.info: + model = "Harmony Hub Pro 2400" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "manufacturer": "Logitech", + "sw_version": self._client.hub_config.info.get( + "hubSwVersion", self._client.fw_version + ), + "name": self.name, + "model": model, + } + + @property + def unique_id(self): + """Return the unique id.""" + return find_unique_id_for_remote(self._client) @property def name(self): @@ -239,7 +254,6 @@ class HarmonyRemote(remote.RemoteDevice): except aioexc.TimeOut: _LOGGER.warning("%s: Connection timed-out", self._name) return False - return True def new_activity(self, activity_info: tuple) -> None: diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json new file mode 100644 index 00000000000..b183e067101 --- /dev/null +++ b/homeassistant/components/harmony/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Logitech Harmony Hub", + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "user": { + "title": "Setup Logitech Harmony Hub", + "data": { + "host": "Hostname or IP Address", + "name": "Hub Name" + } + }, + "link": { + "title": "Setup Logitech Harmony Hub", + "description": "Do you want to setup {name} ({host})?" + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "description": "Adjust Harmony Hub Options", + "data": { + "activity": "The default activity to execute when none is specified.", + "delay_secs": "The delay between sending commands." + } + } + } + } +} diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py new file mode 100644 index 00000000000..1aa29548f7c --- /dev/null +++ b/homeassistant/components/harmony/util.py @@ -0,0 +1,15 @@ +"""The Logitech Harmony Hub integration utils.""" +from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient + + +def find_unique_id_for_remote(harmony: HarmonyClient): + """Find the unique id for both websocket and xmpp clients.""" + websocket_unique_id = harmony.hub_config.info.get("activeRemoteId") + if websocket_unique_id is not None: + return websocket_unique_id + + xmpp_unique_id = harmony.config.get("global", {}).get("timeStampHash") + if not xmpp_unique_id: + return None + + return xmpp_unique_id.split(";")[-1] diff --git a/homeassistant/components/remote/__init__.py b/homeassistant/components/remote/__init__.py index 0a598ae345d..580c0a3b152 100644 --- a/homeassistant/components/remote/__init__.py +++ b/homeassistant/components/remote/__init__.py @@ -24,7 +24,7 @@ from homeassistant.helpers.entity_component import EntityComponent from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.loader import bind_hass -# mypy: allow-untyped-calls +# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index fb2f2bdb7a9..ac24ecb9209 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -40,6 +40,7 @@ FLOWS = [ "gpslogger", "griddy", "hangouts", + "harmony", "heos", "hisense_aehw4a1", "homekit_controller", diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 1df265bffe5..c9832ea2d86 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -17,6 +17,12 @@ SSDP = { "manufacturer": "DIRECTV" } ], + "harmony": [ + { + "deviceType": "urn:myharmony-com:device:harmony:1", + "manufacturer": "Logitech" + } + ], "heos": [ { "st": "urn:schemas-denon-com:device:ACT-Denon:1" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 614ad22605e..d08e44cc579 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -64,6 +64,9 @@ aioesphomeapi==2.6.1 # homeassistant.components.freebox aiofreepybox==0.0.8 +# homeassistant.components.harmony +aioharmony==0.1.13 + # homeassistant.components.homekit_controller aiohomekit[IP]==0.2.34 diff --git a/tests/components/harmony/__init__.py b/tests/components/harmony/__init__.py new file mode 100644 index 00000000000..f427677b40a --- /dev/null +++ b/tests/components/harmony/__init__.py @@ -0,0 +1 @@ +"""Tests for the Logitech Harmony Hub integration.""" diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py new file mode 100644 index 00000000000..39e11d30afe --- /dev/null +++ b/tests/components/harmony/test_config_flow.py @@ -0,0 +1,148 @@ +"""Test the Logitech Harmony Hub config flow.""" +from asynctest import CoroutineMock, MagicMock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.harmony.config_flow import CannotConnect +from homeassistant.components.harmony.const import DOMAIN + + +def _get_mock_harmonyapi(connect=None, close=None): + harmonyapi_mock = MagicMock() + type(harmonyapi_mock).connect = CoroutineMock(return_value=connect) + type(harmonyapi_mock).close = CoroutineMock(return_value=close) + + return harmonyapi_mock + + +async def test_user_form(hass): + """Test we get the user form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + harmonyapi = _get_mock_harmonyapi(connect=True) + with patch( + "homeassistant.components.harmony.config_flow.HarmonyAPI", + return_value=harmonyapi, + ), patch( + "homeassistant.components.harmony.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.harmony.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {"host": "1.2.3.4", "name": "friend"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "friend" + assert result2["data"] == { + "host": "1.2.3.4", + "name": "friend", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + harmonyapi = _get_mock_harmonyapi(connect=True) + with patch( + "homeassistant.components.harmony.config_flow.HarmonyAPI", + return_value=harmonyapi, + ), patch( + "homeassistant.components.harmony.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.harmony.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={ + "host": "1.2.3.4", + "name": "friend", + "activity": "Watch TV", + "delay_secs": 0.9, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "friend" + assert result["data"] == { + "host": "1.2.3.4", + "name": "friend", + } + assert result["options"] == { + "activity": "Watch TV", + "delay_secs": 0.9, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_ssdp(hass): + """Test we get the form with ssdp source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + "friendlyName": "Harmony Hub", + "ssdp_location": "http://192.168.209.238:8088/description", + }, + ) + assert result["type"] == "form" + assert result["step_id"] == "link" + assert result["errors"] == {} + + harmonyapi = _get_mock_harmonyapi(connect=True) + with patch( + "homeassistant.components.harmony.config_flow.HarmonyAPI", + return_value=harmonyapi, + ), patch( + "homeassistant.components.harmony.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.harmony.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {},) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Harmony Hub" + assert result2["data"] == { + "host": "192.168.209.238", + "name": "Harmony Hub", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.harmony.config_flow.HarmonyAPI", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.2.3.4", + "name": "friend", + "activity": "Watch TV", + "delay_secs": 0.2, + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From c8c81d493d10a7c5b4442239169b1f4a64f42b49 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 19 Mar 2020 12:37:47 -0400 Subject: [PATCH 131/431] Refactor ZHA setup (#32959) * Refactor ZHA setup. Catch errors and raise if needed. * Cleanup. --- homeassistant/components/zha/__init__.py | 32 ++++++++++---------- homeassistant/components/zha/core/gateway.py | 26 ++++++++++++++-- 2 files changed, 39 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 0d4ceed829b..ac5648e097b 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -89,9 +89,19 @@ async def async_setup_entry(hass, config_entry): Will automatically load components to support devices found on the network. """ - hass.data[DATA_ZHA] = hass.data.get(DATA_ZHA, {}) - hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS] = [] - hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] = asyncio.Event() + zha_data = hass.data.setdefault(DATA_ZHA, {}) + config = zha_data.get(DATA_ZHA_CONFIG, {}) + + if config.get(CONF_ENABLE_QUIRKS, True): + # needs to be done here so that the ZHA module is finished loading + # before zhaquirks is imported + import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error + + zha_gateway = ZHAGateway(hass, config, config_entry) + await zha_gateway.async_initialize() + + zha_data[DATA_ZHA_DISPATCHERS] = [] + zha_data[DATA_ZHA_PLATFORM_LOADED] = asyncio.Event() platforms = [] for component in COMPONENTS: platforms.append( @@ -102,20 +112,10 @@ async def async_setup_entry(hass, config_entry): async def _platforms_loaded(): await asyncio.gather(*platforms) - hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED].set() + zha_data[DATA_ZHA_PLATFORM_LOADED].set() hass.async_create_task(_platforms_loaded()) - config = hass.data[DATA_ZHA].get(DATA_ZHA_CONFIG, {}) - - if config.get(CONF_ENABLE_QUIRKS, True): - # needs to be done here so that the ZHA module is finished loading - # before zhaquirks is imported - import zhaquirks # noqa: F401 pylint: disable=unused-import, import-outside-toplevel, import-error - - zha_gateway = ZHAGateway(hass, config, config_entry) - await zha_gateway.async_initialize() - device_registry = await hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -130,8 +130,8 @@ async def async_setup_entry(hass, config_entry): async def async_zha_shutdown(event): """Handle shutdown tasks.""" - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].shutdown() - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_update_device_storage() + await zha_data[DATA_ZHA_GATEWAY].shutdown() + await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown) hass.async_create_task(zha_gateway.async_load_devices()) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 6c0681d9eca..1ad10710c60 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -7,10 +7,12 @@ import logging import os import traceback +from serial import SerialException import zigpy.device as zigpy_dev from homeassistant.components.system_log import LogEntry, _figure_out_source from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.device_registry import ( CONNECTION_ZIGBEE, async_get_registry as get_dev_reg, @@ -98,7 +100,6 @@ class ZHAGateway: self.ha_entity_registry = None self.application_controller = None self.radio_description = None - hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self self._log_levels = { DEBUG_LEVEL_ORIGINAL: async_capture_log_levels(), DEBUG_LEVEL_CURRENT: async_capture_log_levels(), @@ -122,7 +123,11 @@ class ZHAGateway: radio_details = RADIO_TYPES[radio_type] radio = radio_details[ZHA_GW_RADIO]() self.radio_description = radio_details[ZHA_GW_RADIO_DESCRIPTION] - await radio.connect(usb_path, baudrate) + try: + await radio.connect(usb_path, baudrate) + except (SerialException, OSError) as exception: + _LOGGER.error("Couldn't open serial port for ZHA: %s", str(exception)) + raise ConfigEntryNotReady if CONF_DATABASE in self._config: database = self._config[CONF_DATABASE] @@ -133,7 +138,22 @@ class ZHAGateway: apply_application_controller_patch(self) self.application_controller.add_listener(self) self.application_controller.groups.add_listener(self) - await self.application_controller.startup(auto_form=True) + + try: + res = await self.application_controller.startup(auto_form=True) + if res is False: + await self.application_controller.shutdown() + raise ConfigEntryNotReady + except asyncio.TimeoutError as exception: + _LOGGER.error( + "Couldn't start %s coordinator", + radio_details[ZHA_GW_RADIO_DESCRIPTION], + exc_info=exception, + ) + radio.close() + raise ConfigEntryNotReady from exception + + self._hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] = self self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( self.application_controller.ieee ) From 62f2ee5f60586402e90ac868354550571e96cb2a Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Thu, 19 Mar 2020 17:42:49 +0100 Subject: [PATCH 132/431] Bump version of zigpy-cc to 0.2.3 (#32951) --- 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 19940eaea00..da437e5f4d4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows-homeassistant==0.14.0", "zha-quirks==0.0.37", - "zigpy-cc==0.1.0", + "zigpy-cc==0.2.3", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.16.0", "zigpy-xbee-homeassistant==0.10.0", diff --git a/requirements_all.txt b/requirements_all.txt index df442551bb2..f22d52b377d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2170,7 +2170,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.1.0 +zigpy-cc==0.2.3 # homeassistant.components.zha zigpy-deconz==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d08e44cc579..b186e196303 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -765,7 +765,7 @@ zeroconf==0.24.5 zha-quirks==0.0.37 # homeassistant.components.zha -zigpy-cc==0.1.0 +zigpy-cc==0.2.3 # homeassistant.components.zha zigpy-deconz==0.7.0 From 9451920ab5b2111c85eaab58da908af726356a09 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 19 Mar 2020 19:34:15 +0000 Subject: [PATCH 133/431] Fix sighthound dependency issue (#33010) --- homeassistant/components/sighthound/manifest.json | 1 + requirements_all.txt | 1 + requirements_test_all.txt | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/homeassistant/components/sighthound/manifest.json b/homeassistant/components/sighthound/manifest.json index 737aa01c21f..a891d807f57 100644 --- a/homeassistant/components/sighthound/manifest.json +++ b/homeassistant/components/sighthound/manifest.json @@ -3,6 +3,7 @@ "name": "Sighthound", "documentation": "https://www.home-assistant.io/integrations/sighthound", "requirements": [ + "pillow==7.0.0", "simplehound==0.3" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index f22d52b377d..ab07956c167 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1017,6 +1017,7 @@ pilight==0.1.1 # homeassistant.components.proxy # homeassistant.components.qrcode # homeassistant.components.seven_segments +# homeassistant.components.sighthound # homeassistant.components.tensorflow pillow==7.0.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b186e196303..c7d910223be 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -371,6 +371,14 @@ pexpect==4.6.0 # homeassistant.components.pilight pilight==0.1.1 +# homeassistant.components.doods +# homeassistant.components.proxy +# homeassistant.components.qrcode +# homeassistant.components.seven_segments +# homeassistant.components.sighthound +# homeassistant.components.tensorflow +pillow==7.0.0 + # homeassistant.components.plex plexapi==3.3.0 From 23045af4a75e7933322de4c2a7a8e98ebc7a24b3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 19 Mar 2020 16:39:24 -0600 Subject: [PATCH 134/431] Bump simplisafe-python to 9.0.3 (#33013) --- 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 b5f89a65fea..0fdcdcfcf5e 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==9.0.2"], + "requirements": ["simplisafe-python==9.0.3"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index ab07956c167..f2a5b7e3d0f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1856,7 +1856,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.0.2 +simplisafe-python==9.0.3 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c7d910223be..7bb25f84d85 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -661,7 +661,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.0.2 +simplisafe-python==9.0.3 # homeassistant.components.sleepiq sleepyq==0.7 From 5a2aabea9c2f09c4776cee76191167d5e46f03f5 Mon Sep 17 00:00:00 2001 From: tetienne Date: Fri, 20 Mar 2020 00:09:14 +0100 Subject: [PATCH 135/431] Fix somfy optimistic mode when missing in conf (#32995) * Fix optimistic mode when missing in conf #32971 * Ease code using a default value * Client id and secret are now inclusive --- homeassistant/components/somfy/__init__.py | 34 ++++++++++------------ 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/somfy/__init__.py b/homeassistant/components/somfy/__init__.py index 1b2722882e6..619e5a72602 100644 --- a/homeassistant/components/somfy/__init__.py +++ b/homeassistant/components/somfy/__init__.py @@ -27,7 +27,7 @@ DOMAIN = "somfy" CONF_CLIENT_ID = "client_id" CONF_CLIENT_SECRET = "client_secret" -CONF_OPTIMISTIC = "optimisitic" +CONF_OPTIMISTIC = "optimistic" SOMFY_AUTH_CALLBACK_PATH = "/auth/somfy/callback" SOMFY_AUTH_START = "/auth/somfy" @@ -36,8 +36,8 @@ CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_CLIENT_SECRET): cv.string, + vol.Inclusive(CONF_CLIENT_ID, "oauth"): cv.string, + vol.Inclusive(CONF_CLIENT_SECRET, "oauth"): cv.string, vol.Optional(CONF_OPTIMISTIC, default=False): cv.boolean, } ) @@ -51,23 +51,21 @@ SOMFY_COMPONENTS = ["cover", "switch"] async def async_setup(hass, config): """Set up the Somfy component.""" hass.data[DOMAIN] = {} + domain_config = config.get(DOMAIN, {}) + hass.data[DOMAIN][CONF_OPTIMISTIC] = domain_config.get(CONF_OPTIMISTIC, False) - if DOMAIN not in config: - return True - - hass.data[DOMAIN][CONF_OPTIMISTIC] = config[DOMAIN][CONF_OPTIMISTIC] - - config_flow.SomfyFlowHandler.async_register_implementation( - hass, - config_entry_oauth2_flow.LocalOAuth2Implementation( + if CONF_CLIENT_ID in domain_config: + config_flow.SomfyFlowHandler.async_register_implementation( hass, - DOMAIN, - config[DOMAIN][CONF_CLIENT_ID], - config[DOMAIN][CONF_CLIENT_SECRET], - "https://accounts.somfy.com/oauth/oauth/v2/auth", - "https://accounts.somfy.com/oauth/oauth/v2/token", - ), - ) + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + "https://accounts.somfy.com/oauth/oauth/v2/auth", + "https://accounts.somfy.com/oauth/oauth/v2/token", + ), + ) return True From 84712d2f40d7ef0c98fa2964956a9f1648dab48b Mon Sep 17 00:00:00 2001 From: Jc2k Date: Thu, 19 Mar 2020 23:25:18 +0000 Subject: [PATCH 136/431] Bump aiohomekit to fix Insignia NS-CH1XGO8 and Lennox S30 (#33014) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 8d669174c24..392495e34ea 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[IP]==0.2.34"], + "requirements": ["aiohomekit[IP]==0.2.35"], "dependencies": [], "zeroconf": ["_hap._tcp.local."], "codeowners": ["@Jc2k"] diff --git a/requirements_all.txt b/requirements_all.txt index f2a5b7e3d0f..4eaffe245fc 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioftp==0.12.0 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.34 +aiohomekit[IP]==0.2.35 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bb25f84d85..72acaf4dd1d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -68,7 +68,7 @@ aiofreepybox==0.0.8 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.34 +aiohomekit[IP]==0.2.35 # homeassistant.components.emulated_hue # homeassistant.components.http From 4c9303bbd5250f00edcd769693f43efbe7f4ee7c Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 20 Mar 2020 00:30:38 +0100 Subject: [PATCH 137/431] Axis - Fix char in stream url (#33004) * An unwanted character had found its way into a stream string, reverting f-string work to remove duplication of code and improve readability * Fix failing tests --- homeassistant/components/axis/camera.py | 28 ++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/axis/camera.py b/homeassistant/components/axis/camera.py index 3cf84ce2288..c914319aa42 100644 --- a/homeassistant/components/axis/camera.py +++ b/homeassistant/components/axis/camera.py @@ -21,6 +21,10 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from .axis_base import AxisEntityBase from .const import DOMAIN as AXIS_DOMAIN +AXIS_IMAGE = "http://{host}:{port}/axis-cgi/jpg/image.cgi" +AXIS_VIDEO = "http://{host}:{port}/axis-cgi/mjpg/video.cgi" +AXIS_STREAM = "rtsp://{user}:{password}@{host}/axis-media/media.amp?videocodec=h264" + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Axis camera video stream.""" @@ -32,13 +36,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): CONF_NAME: config_entry.data[CONF_NAME], CONF_USERNAME: config_entry.data[CONF_USERNAME], CONF_PASSWORD: config_entry.data[CONF_PASSWORD], - CONF_MJPEG_URL: ( - f"http://{config_entry.data[CONF_HOST]}" - f":{config_entry.data[CONF_PORT]}/axis-cgi/mjpg/video.cgi" + CONF_MJPEG_URL: AXIS_VIDEO.format( + host=config_entry.data[CONF_HOST], port=config_entry.data[CONF_PORT], ), - CONF_STILL_IMAGE_URL: ( - f"http://{config_entry.data[CONF_HOST]}" - f":{config_entry.data[CONF_PORT]}/axis-cgi/jpg/image.cgi" + CONF_STILL_IMAGE_URL: AXIS_IMAGE.format( + host=config_entry.data[CONF_HOST], port=config_entry.data[CONF_PORT], ), CONF_AUTHENTICATION: HTTP_DIGEST_AUTHENTICATION, } @@ -70,19 +72,17 @@ class AxisCamera(AxisEntityBase, MjpegCamera): async def stream_source(self): """Return the stream source.""" - return ( - f"rtsp://{self.device.config_entry.data[CONF_USERNAME]}´" - f":{self.device.config_entry.data[CONF_PASSWORD]}" - f"@{self.device.host}/axis-media/media.amp?videocodec=h264" + return AXIS_STREAM.format( + user=self.device.config_entry.data[CONF_USERNAME], + password=self.device.config_entry.data[CONF_PASSWORD], + host=self.device.host, ) def _new_address(self): """Set new device address for video stream.""" port = self.device.config_entry.data[CONF_PORT] - self._mjpeg_url = (f"http://{self.device.host}:{port}/axis-cgi/mjpg/video.cgi",) - self._still_image_url = ( - f"http://{self.device.host}:{port}/axis-cgi/jpg/image.cgi" - ) + self._mjpeg_url = AXIS_VIDEO.format(host=self.device.host, port=port) + self._still_image_url = AXIS_IMAGE.format(host=self.device.host, port=port) @property def unique_id(self): From ff2367fffb3329fb7bd5f00412ffd7daa4e77fdd Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 20 Mar 2020 00:40:50 +0100 Subject: [PATCH 138/431] =?UTF-8?q?deCONZ=20-=20Add=20support=20for=20Seni?= =?UTF-8?q?c=20and=20Gira=20Friends=20of=20Hue=20remote=E2=80=A6=20(#33022?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/deconz/device_trigger.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/homeassistant/components/deconz/device_trigger.py b/homeassistant/components/deconz/device_trigger.py index 8ae0394f935..654bcfd43db 100644 --- a/homeassistant/components/deconz/device_trigger.py +++ b/homeassistant/components/deconz/device_trigger.py @@ -56,6 +56,8 @@ CONF_RIGHT = "right" CONF_OPEN = "open" CONF_CLOSE = "close" CONF_BOTH_BUTTONS = "both_buttons" +CONF_TOP_BUTTONS = "top_buttons" +CONF_BOTTOM_BUTTONS = "bottom_buttons" CONF_BUTTON_1 = "button_1" CONF_BUTTON_2 = "button_2" CONF_BUTTON_3 = "button_3" @@ -97,6 +99,34 @@ HUE_TAP_REMOTE = { (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 18}, } +SENIC_FRIENDS_OF_HUE_MODEL = "FOHSWITCH" +SENIC_FRIENDS_OF_HUE = { + (CONF_SHORT_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1000}, + (CONF_SHORT_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1002}, + (CONF_LONG_PRESS, CONF_BUTTON_1): {CONF_EVENT: 1001}, + (CONF_LONG_RELEASE, CONF_BUTTON_1): {CONF_EVENT: 1003}, + (CONF_SHORT_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2000}, + (CONF_SHORT_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2002}, + (CONF_LONG_PRESS, CONF_BUTTON_2): {CONF_EVENT: 2001}, + (CONF_LONG_RELEASE, CONF_BUTTON_2): {CONF_EVENT: 2003}, + (CONF_SHORT_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3000}, + (CONF_SHORT_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3002}, + (CONF_LONG_PRESS, CONF_BUTTON_3): {CONF_EVENT: 3001}, + (CONF_LONG_RELEASE, CONF_BUTTON_3): {CONF_EVENT: 3003}, + (CONF_SHORT_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4000}, + (CONF_SHORT_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4002}, + (CONF_LONG_PRESS, CONF_BUTTON_4): {CONF_EVENT: 4001}, + (CONF_LONG_RELEASE, CONF_BUTTON_4): {CONF_EVENT: 4003}, + (CONF_SHORT_PRESS, CONF_TOP_BUTTONS): {CONF_EVENT: 5000}, + (CONF_SHORT_RELEASE, CONF_TOP_BUTTONS): {CONF_EVENT: 5002}, + (CONF_LONG_PRESS, CONF_TOP_BUTTONS): {CONF_EVENT: 5001}, + (CONF_LONG_RELEASE, CONF_TOP_BUTTONS): {CONF_EVENT: 5003}, + (CONF_SHORT_PRESS, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6000}, + (CONF_SHORT_RELEASE, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6002}, + (CONF_LONG_PRESS, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6001}, + (CONF_LONG_RELEASE, CONF_BOTTOM_BUTTONS): {CONF_EVENT: 6003}, +} + SYMFONISK_SOUND_CONTROLLER_MODEL = "SYMFONISK Sound Controller" SYMFONISK_SOUND_CONTROLLER = { (CONF_SHORT_PRESS, CONF_TURN_ON): {CONF_EVENT: 1002}, @@ -274,6 +304,7 @@ REMOTES = { HUE_DIMMER_REMOTE_MODEL_GEN1: HUE_DIMMER_REMOTE, HUE_DIMMER_REMOTE_MODEL_GEN2: HUE_DIMMER_REMOTE, HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, + SENIC_FRIENDS_OF_HUE_MODEL: SENIC_FRIENDS_OF_HUE, SYMFONISK_SOUND_CONTROLLER_MODEL: SYMFONISK_SOUND_CONTROLLER, TRADFRI_ON_OFF_SWITCH_MODEL: TRADFRI_ON_OFF_SWITCH, TRADFRI_OPEN_CLOSE_REMOTE_MODEL: TRADFRI_OPEN_CLOSE_REMOTE, From c9592c1447254d67f6b07fc10395885f3c16fddf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2020 20:43:44 -0500 Subject: [PATCH 139/431] Harmony config flow improvements (#33018) * Harmony config flow improvements * Address followup review comments from #32919 * pylint -- catching my naming error * remove leftovers from refactor --- .../components/harmony/.translations/en.json | 1 - homeassistant/components/harmony/__init__.py | 56 +++++---- .../components/harmony/config_flow.py | 16 +-- homeassistant/components/harmony/const.py | 2 + homeassistant/components/harmony/remote.py | 117 +++++++++--------- homeassistant/components/harmony/strings.json | 1 - homeassistant/components/harmony/util.py | 7 +- tests/components/harmony/test_config_flow.py | 5 +- 8 files changed, 101 insertions(+), 104 deletions(-) diff --git a/homeassistant/components/harmony/.translations/en.json b/homeassistant/components/harmony/.translations/en.json index b183e067101..8af5a5ada1a 100644 --- a/homeassistant/components/harmony/.translations/en.json +++ b/homeassistant/components/harmony/.translations/en.json @@ -17,7 +17,6 @@ }, "error": { "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "abort": { diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index 0f9824231ea..c0fddec09cc 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -5,11 +5,12 @@ import logging from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import HomeAssistant +from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.dispatcher import async_dispatcher_send -from .const import DOMAIN, PLATFORMS -from .remote import DEVICES, HarmonyRemote +from .const import DOMAIN, HARMONY_OPTIONS_UPDATE, PLATFORMS +from .remote import HarmonyRemote _LOGGER = logging.getLogger(__name__) @@ -23,19 +24,16 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Logitech Harmony Hub from a config entry.""" + # As there currently is no way to import options from yaml + # when setting up a config entry, we fallback to adding + # the options to the config entry and pull them out here if + # they are missing from the options + _async_import_options_from_data_if_missing(hass, entry) - conf = entry.data - address = conf[CONF_HOST] - name = conf.get(CONF_NAME) - activity = conf.get(ATTR_ACTIVITY) - delay_secs = conf.get(ATTR_DELAY_SECS) - - _LOGGER.info( - "Loading Harmony Platform: %s at %s, startup activity: %s", - name, - address, - activity, - ) + address = entry.data[CONF_HOST] + name = entry.data[CONF_NAME] + activity = entry.options.get(ATTR_ACTIVITY) + delay_secs = entry.options.get(ATTR_DELAY_SECS) harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") try: @@ -45,7 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady hass.data[DOMAIN][entry.entry_id] = device - DEVICES.append(device) entry.add_update_listener(_update_listener) @@ -57,16 +54,24 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -async def _update_listener(hass, entry): +@callback +def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): + options = dict(entry.options) + modified = 0 + for importable_option in [ATTR_ACTIVITY, ATTR_DELAY_SECS]: + if importable_option not in entry.options and importable_option in entry.data: + options[importable_option] = entry.data[importable_option] + modified = 1 + + if modified: + hass.config_entries.async_update_entry(entry, options=options) + + +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): """Handle options update.""" - - device = hass.data[DOMAIN][entry.entry_id] - - if ATTR_DELAY_SECS in entry.options: - device.delay_seconds = entry.options[ATTR_DELAY_SECS] - - if ATTR_ACTIVITY in entry.options: - device.default_activity = entry.options[ATTR_ACTIVITY] + async_dispatcher_send( + hass, f"{HARMONY_OPTIONS_UPDATE}-{entry.unique_id}", entry.options + ) async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): @@ -85,7 +90,6 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): await device.shutdown() if unload_ok: - DEVICES.remove(hass.data[DOMAIN][entry.entry_id]) hass.data[DOMAIN].pop(entry.entry_id) return unload_ok diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index ddd52dfd008..8f9e672c9d9 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -147,18 +147,12 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_valid_input(self, validated, user_input): """Single path to create the config entry from validated input.""" await self.async_set_unique_id(validated[UNIQUE_ID]) - if self._host_already_configured(validated): - return self.async_abort(reason="already_configured") self._abort_if_unique_id_configured() - config_entry = self.async_create_entry( - title=validated[CONF_NAME], - data={CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]}, - ) - # Options from yaml are preserved - options = _options_from_user_input(user_input) - if options: - config_entry["options"] = options - return config_entry + data = {CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]} + # Options from yaml are preserved, we will pull them out when + # we setup the config entry + data.update(_options_from_user_input(user_input)) + return self.async_create_entry(title=validated[CONF_NAME], data=data) def _host_already_configured(self, user_input): """See if we already have a harmony matching user input configured.""" diff --git a/homeassistant/components/harmony/const.py b/homeassistant/components/harmony/const.py index 60542845bd0..4cd5dce0af5 100644 --- a/homeassistant/components/harmony/const.py +++ b/homeassistant/components/harmony/const.py @@ -4,3 +4,5 @@ SERVICE_SYNC = "sync" SERVICE_CHANGE_CHANNEL = "change_channel" PLATFORMS = ["remote"] UNIQUE_ID = "unique_id" +ACTIVITY_POWER_OFF = "PowerOff" +HARMONY_OPTIONS_UPDATE = "harmony_options_update" diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index a5e70b4d807..47f7c7f974e 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -25,8 +25,15 @@ from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import async_dispatcher_connect -from .const import DOMAIN, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC +from .const import ( + ACTIVITY_POWER_OFF, + DOMAIN, + HARMONY_OPTIONS_UPDATE, + SERVICE_CHANGE_CHANNEL, + SERVICE_SYNC, +) from .util import find_unique_id_for_remote _LOGGER = logging.getLogger(__name__) @@ -34,13 +41,12 @@ _LOGGER = logging.getLogger(__name__) ATTR_CHANNEL = "channel" ATTR_CURRENT_ACTIVITY = "current_activity" -DEVICES = [] PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Optional(ATTR_ACTIVITY): cv.string, vol.Required(CONF_NAME): cv.string, vol.Optional(ATTR_DELAY_SECS, default=DEFAULT_DELAY_SECS): vol.Coerce(float), - vol.Optional(CONF_HOST): cv.string, + vol.Required(CONF_HOST): cv.string, # The client ignores port so lets not confuse the user by pretenting we do anything with this }, extra=vol.ALLOW_EXTRA, @@ -63,13 +69,6 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Now handled by ssdp in the config flow return - if CONF_HOST not in config: - _LOGGER.error( - "The harmony remote '%s' cannot be setup because configuration now requires a host when configured manually.", - config[CONF_NAME], - ) - return - hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, data=config @@ -84,7 +83,7 @@ async def async_setup_entry( device = hass.data[DOMAIN][entry.entry_id] - _LOGGER.info("Harmony Remote: %s", device) + _LOGGER.debug("Harmony Remote: %s", device) async_add_entities([device]) register_services(hass) @@ -92,6 +91,30 @@ async def async_setup_entry( def register_services(hass): """Register all services for harmony devices.""" + + async def _apply_service(service, service_func, *service_func_args): + """Handle services to apply.""" + entity_ids = service.data.get("entity_id") + + want_devices = [ + hass.data[DOMAIN][config_entry_id] for config_entry_id in hass.data[DOMAIN] + ] + + if entity_ids: + want_devices = [ + device for device in want_devices if device.entity_id in entity_ids + ] + + for device in want_devices: + await service_func(device, *service_func_args) + + async def _sync_service(service): + await _apply_service(service, HarmonyRemote.sync) + + async def _change_channel_service(service): + channel = service.data.get(ATTR_CHANNEL) + await _apply_service(service, HarmonyRemote.change_channel, channel) + hass.services.async_register( DOMAIN, SERVICE_SYNC, _sync_service, schema=HARMONY_SYNC_SCHEMA ) @@ -104,28 +127,6 @@ def register_services(hass): ) -async def _apply_service(service, service_func, *service_func_args): - """Handle services to apply.""" - entity_ids = service.data.get("entity_id") - - if entity_ids: - _devices = [device for device in DEVICES if device.entity_id in entity_ids] - else: - _devices = DEVICES - - for device in _devices: - await service_func(device, *service_func_args) - - -async def _sync_service(service): - await _apply_service(service, HarmonyRemote.sync) - - -async def _change_channel_service(service): - channel = service.data.get(ATTR_CHANNEL) - await _apply_service(service, HarmonyRemote.change_channel, channel) - - class HarmonyRemote(remote.RemoteDevice): """Remote representation used to control a Harmony device.""" @@ -135,26 +136,12 @@ class HarmonyRemote(remote.RemoteDevice): self.host = host self._state = None self._current_activity = None - self._default_activity = activity + self.default_activity = activity self._client = HarmonyClient(ip_address=host) self._config_path = out_path - self._delay_secs = delay_secs + self.delay_secs = delay_secs self._available = False - - @property - def delay_secs(self): - """Delay seconds between sending commands.""" - return self._delay_secs - - @delay_secs.setter - def delay_secs(self, delay_secs): - """Update the delay seconds (from options flow).""" - self._delay_secs = delay_secs - - @property - def default_activity(self): - """Activity used when non specified.""" - return self._default_activity + self._undo_dispatch_subscription = None @property def activity_names(self): @@ -164,15 +151,23 @@ class HarmonyRemote(remote.RemoteDevice): # Remove both ways of representing PowerOff if None in activities: activities.remove(None) - if "PowerOff" in activities: - activities.remove("PowerOff") + if ACTIVITY_POWER_OFF in activities: + activities.remove(ACTIVITY_POWER_OFF) return activities - @default_activity.setter - def default_activity(self, activity): - """Update the default activity (from options flow).""" - self._default_activity = activity + async def async_will_remove_from_hass(self): + """Undo subscription.""" + if self._undo_dispatch_subscription: + self._undo_dispatch_subscription() + + async def _async_update_options(self, data): + """Change options when the options flow does.""" + if ATTR_DELAY_SECS in data: + self.delay_secs = data[ATTR_DELAY_SECS] + + if ATTR_ACTIVITY in data: + self.default_activity = data[ATTR_ACTIVITY] async def async_added_to_hass(self): """Complete the initialization.""" @@ -185,6 +180,12 @@ class HarmonyRemote(remote.RemoteDevice): disconnect=self.got_disconnected, ) + self._undo_dispatch_subscription = async_dispatcher_connect( + self.hass, + f"{HARMONY_OPTIONS_UPDATE}-{self.unique_id}", + self._async_update_options, + ) + # Store Harmony HUB config, this will also update our current # activity await self.new_config() @@ -294,7 +295,7 @@ class HarmonyRemote(remote.RemoteDevice): """Start an activity from the Harmony device.""" _LOGGER.debug("%s: Turn On", self.name) - activity = kwargs.get(ATTR_ACTIVITY, self._default_activity) + activity = kwargs.get(ATTR_ACTIVITY, self.default_activity) if activity: activity_id = None @@ -351,7 +352,7 @@ class HarmonyRemote(remote.RemoteDevice): return num_repeats = kwargs[ATTR_NUM_REPEATS] - delay_secs = kwargs.get(ATTR_DELAY_SECS, self._delay_secs) + delay_secs = kwargs.get(ATTR_DELAY_SECS, self.delay_secs) hold_secs = kwargs[ATTR_HOLD_SECS] _LOGGER.debug( "Sending commands to device %s holding for %s seconds " diff --git a/homeassistant/components/harmony/strings.json b/homeassistant/components/harmony/strings.json index b183e067101..8af5a5ada1a 100644 --- a/homeassistant/components/harmony/strings.json +++ b/homeassistant/components/harmony/strings.json @@ -17,7 +17,6 @@ }, "error": { "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "abort": { diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index 1aa29548f7c..5f7e46510f9 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -8,8 +8,5 @@ def find_unique_id_for_remote(harmony: HarmonyClient): if websocket_unique_id is not None: return websocket_unique_id - xmpp_unique_id = harmony.config.get("global", {}).get("timeStampHash") - if not xmpp_unique_id: - return None - - return xmpp_unique_id.split(";")[-1] + # fallback to the xmpp unique id if websocket is not available + return harmony.config["global"]["timeStampHash"].split(";")[-1] diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 39e11d30afe..4791b4e8d4c 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -76,11 +76,12 @@ async def test_form_import(hass): assert result["data"] == { "host": "1.2.3.4", "name": "friend", - } - assert result["options"] == { "activity": "Watch TV", "delay_secs": 0.9, } + # It is not possible to import options at this time + # so they end up in the config entry data and are + # used a fallback when they are not in options await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 From f275b7e5ed2a1b73426a7f16365e7bb058776e7b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2020 21:03:51 -0500 Subject: [PATCH 140/431] Add Nexia thermostat support (Trane / American Standard) (#32826) * Merge nexia * Restore original work * Merge cleanups * config flow * Add config flow * Add missing files * Fix import of old yaml config * More cleanups from self review * Additional self review * Update homeassistant/components/nexia/services.yaml Co-Authored-By: Paulus Schoutsen * fix io in event loop * Update homeassistant/components/nexia/climate.py Co-Authored-By: Paulus Schoutsen * avoid using ternary statements if they span multiple * Cleanup strings and remove unneeded attributes * more cleanup * more cleanup of yaml * remove coordinator boiler plate * nuke services for now for the inital pr, add back later * remove copy pasta * this can be reduced more * Update homeassistant/components/nexia/config_flow.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/config_flow.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/__init__.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/__init__.py Co-Authored-By: Martin Hjelmare * review * comments * Update homeassistant/components/nexia/climate.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/climate.py Co-Authored-By: Martin Hjelmare * Update homeassistant/components/nexia/climate.py Co-Authored-By: Martin Hjelmare * more review adjustments * nuke unused constants * Update homeassistant/components/nexia/config_flow.py Co-Authored-By: Martin Hjelmare * map states * add update * zone id is unique * Fix humidfy check * target_humidity should be a property instead of in attributes * remove aux heat as its already there Co-authored-by: Ryan Nazaretian Co-authored-by: Paulus Schoutsen Co-authored-by: Martin Hjelmare --- CODEOWNERS | 1 + .../components/nexia/.translations/en.json | 22 + homeassistant/components/nexia/__init__.py | 119 ++++++ .../components/nexia/binary_sensor.py | 89 ++++ homeassistant/components/nexia/climate.py | 389 ++++++++++++++++++ homeassistant/components/nexia/config_flow.py | 87 ++++ homeassistant/components/nexia/const.py | 26 ++ homeassistant/components/nexia/entity.py | 30 ++ homeassistant/components/nexia/manifest.json | 13 + homeassistant/components/nexia/sensor.py | 276 +++++++++++++ homeassistant/components/nexia/strings.json | 22 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/nexia/__init__.py | 1 + tests/components/nexia/test_config_flow.py | 76 ++++ 16 files changed, 1158 insertions(+) create mode 100644 homeassistant/components/nexia/.translations/en.json create mode 100644 homeassistant/components/nexia/__init__.py create mode 100644 homeassistant/components/nexia/binary_sensor.py create mode 100644 homeassistant/components/nexia/climate.py create mode 100644 homeassistant/components/nexia/config_flow.py create mode 100644 homeassistant/components/nexia/const.py create mode 100644 homeassistant/components/nexia/entity.py create mode 100644 homeassistant/components/nexia/manifest.json create mode 100644 homeassistant/components/nexia/sensor.py create mode 100644 homeassistant/components/nexia/strings.json create mode 100644 tests/components/nexia/__init__.py create mode 100644 tests/components/nexia/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index ccb0cac17ea..4d9ec3a2f0f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -242,6 +242,7 @@ homeassistant/components/ness_alarm/* @nickw444 homeassistant/components/nest/* @awarecan homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff +homeassistant/components/nexia/* @ryannazaretian @bdraco homeassistant/components/nextbus/* @vividboarder homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole diff --git a/homeassistant/components/nexia/.translations/en.json b/homeassistant/components/nexia/.translations/en.json new file mode 100644 index 00000000000..d3fabfb0b4d --- /dev/null +++ b/homeassistant/components/nexia/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Nexia", + "step": { + "user": { + "title": "Connect to mynexia.com", + "data": { + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "This nexia home is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py new file mode 100644 index 00000000000..40ea7b6dcc6 --- /dev/null +++ b/homeassistant/components/nexia/__init__.py @@ -0,0 +1,119 @@ +"""Support for Nexia / Trane XL Thermostats.""" +import asyncio +from datetime import timedelta +from functools import partial +import logging + +from nexia.home import NexiaHome +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import DATA_NEXIA, DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR + +_LOGGER = logging.getLogger(__name__) + + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + }, + extra=vol.ALLOW_EXTRA, + ), + }, + extra=vol.ALLOW_EXTRA, +) + +DEFAULT_UPDATE_RATE = 120 + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up the nexia component from YAML.""" + + conf = config.get(DOMAIN) + hass.data.setdefault(DOMAIN, {}) + + if not conf: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + ) + ) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Configure the base Nexia device for Home Assistant.""" + + conf = entry.data + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + + try: + nexia_home = await hass.async_add_executor_job( + partial(NexiaHome, username=username, password=password) + ) + except ConnectTimeout as ex: + _LOGGER.error("Unable to connect to Nexia service: %s", ex) + raise ConfigEntryNotReady + except HTTPError as http_ex: + if http_ex.response.status_code >= 400 and http_ex.response.status_code < 500: + _LOGGER.error( + "Access error from Nexia service, please check credentials: %s", + http_ex, + ) + return False + _LOGGER.error("HTTP error from Nexia service: %s", http_ex) + raise ConfigEntryNotReady + + async def _async_update_data(): + """Fetch data from API endpoint.""" + return await hass.async_add_job(nexia_home.update) + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="Nexia update", + update_method=_async_update_data, + update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), + ) + + hass.data[DOMAIN][entry.entry_id] = {} + hass.data[DOMAIN][entry.entry_id][DATA_NEXIA] = { + NEXIA_DEVICE: nexia_home, + UPDATE_COORDINATOR: coordinator, + } + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py new file mode 100644 index 00000000000..2802c3d7bd4 --- /dev/null +++ b/homeassistant/components/nexia/binary_sensor.py @@ -0,0 +1,89 @@ +"""Support for Nexia / Trane XL Thermostats.""" + +from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.const import ATTR_ATTRIBUTION + +from .const import ( + ATTRIBUTION, + DATA_NEXIA, + DOMAIN, + MANUFACTURER, + NEXIA_DEVICE, + UPDATE_COORDINATOR, +) +from .entity import NexiaEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for a Nexia device.""" + + nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_home = nexia_data[NEXIA_DEVICE] + coordinator = nexia_data[UPDATE_COORDINATOR] + + entities = [] + for thermostat_id in nexia_home.get_thermostat_ids(): + thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + entities.append( + NexiaBinarySensor( + coordinator, thermostat, "is_blower_active", "Blower Active" + ) + ) + if thermostat.has_emergency_heat(): + entities.append( + NexiaBinarySensor( + coordinator, + thermostat, + "is_emergency_heat_active", + "Emergency Heat Active", + ) + ) + + async_add_entities(entities, True) + + +class NexiaBinarySensor(NexiaEntity, BinarySensorDevice): + """Provices Nexia BinarySensor support.""" + + def __init__(self, coordinator, device, sensor_call, sensor_name): + """Initialize the nexia sensor.""" + super().__init__(coordinator) + self._coordinator = coordinator + self._device = device + self._name = f"{self._device.get_name()} {sensor_name}" + self._call = sensor_call + self._unique_id = f"{self._device.thermostat_id}_{sensor_call}" + self._state = None + + @property + def unique_id(self): + """Return the unique id of the binary sensor.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._device.thermostat_id)}, + "name": self._device.get_name(), + "model": self._device.get_model(), + "sw_version": self._device.get_firmware(), + "manufacturer": MANUFACTURER, + } + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + @property + def is_on(self): + """Return the status of the sensor.""" + return getattr(self._device, self._call)() diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py new file mode 100644 index 00000000000..a1f6bb155f9 --- /dev/null +++ b/homeassistant/components/nexia/climate.py @@ -0,0 +1,389 @@ +"""Support for Nexia / Trane XL thermostats.""" +import logging + +from nexia.const import ( + FAN_MODES, + OPERATION_MODE_AUTO, + OPERATION_MODE_COOL, + OPERATION_MODE_HEAT, + OPERATION_MODE_OFF, + SYSTEM_STATUS_COOL, + SYSTEM_STATUS_HEAT, + SYSTEM_STATUS_IDLE, +) + +from homeassistant.components.climate import ClimateDevice +from homeassistant.components.climate.const import ( + ATTR_MAX_HUMIDITY, + ATTR_MIN_HUMIDITY, + ATTR_TARGET_TEMP_HIGH, + ATTR_TARGET_TEMP_LOW, + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + SUPPORT_AUX_HEAT, + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ( + ATTR_ATTRIBUTION, + ATTR_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, +) + +from .const import ( + ATTR_DEHUMIDIFY_SETPOINT, + ATTR_DEHUMIDIFY_SUPPORTED, + ATTR_HUMIDIFY_SETPOINT, + ATTR_HUMIDIFY_SUPPORTED, + ATTR_ZONE_STATUS, + ATTRIBUTION, + DATA_NEXIA, + DOMAIN, + MANUFACTURER, + NEXIA_DEVICE, + UPDATE_COORDINATOR, +) +from .entity import NexiaEntity + +_LOGGER = logging.getLogger(__name__) + +# +# Nexia has two bits to determine hvac mode +# There are actually eight states so we map to +# the most significant state +# +# 1. Zone Mode : Auto / Cooling / Heating / Off +# 2. Run Mode : Hold / Run Schedule +# +# +HA_TO_NEXIA_HVAC_MODE_MAP = { + HVAC_MODE_HEAT: OPERATION_MODE_HEAT, + HVAC_MODE_COOL: OPERATION_MODE_COOL, + HVAC_MODE_HEAT_COOL: OPERATION_MODE_AUTO, + HVAC_MODE_AUTO: OPERATION_MODE_AUTO, + HVAC_MODE_OFF: OPERATION_MODE_OFF, +} +NEXIA_TO_HA_HVAC_MODE_MAP = { + value: key for key, value in HA_TO_NEXIA_HVAC_MODE_MAP.items() +} + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up climate for a Nexia device.""" + + nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_home = nexia_data[NEXIA_DEVICE] + coordinator = nexia_data[UPDATE_COORDINATOR] + + entities = [] + for thermostat_id in nexia_home.get_thermostat_ids(): + thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + for zone_id in thermostat.get_zone_ids(): + zone = thermostat.get_zone_by_id(zone_id) + entities.append(NexiaZone(coordinator, zone)) + + async_add_entities(entities, True) + + +class NexiaZone(NexiaEntity, ClimateDevice): + """Provides Nexia Climate support.""" + + def __init__(self, coordinator, device): + """Initialize the thermostat.""" + super().__init__(coordinator) + self.thermostat = device.thermostat + self._device = device + self._coordinator = coordinator + # The has_* calls are stable for the life of the device + # and do not do I/O + self._has_relative_humidity = self.thermostat.has_relative_humidity() + self._has_emergency_heat = self.thermostat.has_emergency_heat() + self._has_humidify_support = self.thermostat.has_humidify_support() + self._has_dehumidify_support = self.thermostat.has_dehumidify_support() + + @property + def unique_id(self): + """Device Uniqueid.""" + return self._device.zone_id + + @property + def supported_features(self): + """Return the list of supported features.""" + supported = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE + + if self._has_humidify_support or self._has_dehumidify_support: + supported |= SUPPORT_TARGET_HUMIDITY + + if self._has_emergency_heat: + supported |= SUPPORT_AUX_HEAT + + return supported + + @property + def is_fan_on(self): + """Blower is on.""" + return self.thermostat.is_blower_active() + + @property + def name(self): + """Name of the zone.""" + return self._device.get_name() + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return TEMP_CELSIUS if self.thermostat.get_unit() == "C" else TEMP_FAHRENHEIT + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._device.get_temperature() + + @property + def fan_mode(self): + """Return the fan setting.""" + return self.thermostat.get_fan_mode() + + @property + def fan_modes(self): + """Return the list of available fan modes.""" + return FAN_MODES + + def set_fan_mode(self, fan_mode): + """Set new target fan mode.""" + self.thermostat.set_fan_mode(fan_mode) + self.schedule_update_ha_state() + + @property + def preset_mode(self): + """Preset that is active.""" + return self._device.get_preset() + + @property + def preset_modes(self): + """All presets.""" + return self._device.get_presets() + + def set_humidity(self, humidity): + """Dehumidify target.""" + self.thermostat.set_dehumidify_setpoint(humidity / 100.0) + self.schedule_update_ha_state() + + @property + def target_humidity(self): + """Humidity indoors setpoint.""" + if self._has_dehumidify_support: + return round(self.thermostat.get_dehumidify_setpoint() * 100.0, 1) + if self._has_humidify_support: + return round(self.thermostat.get_humidify_setpoint() * 100.0, 1) + return None + + @property + def current_humidity(self): + """Humidity indoors.""" + if self._has_relative_humidity: + return round(self.thermostat.get_relative_humidity() * 100.0, 1) + return None + + @property + def target_temperature(self): + """Temperature we try to reach.""" + if self._device.get_current_mode() == "COOL": + return self._device.get_cooling_setpoint() + return self._device.get_heating_setpoint() + + @property + def hvac_action(self) -> str: + """Operation ie. heat, cool, idle.""" + system_status = self.thermostat.get_system_status() + zone_called = self._device.is_calling() + + if self._device.get_requested_mode() == OPERATION_MODE_OFF: + return CURRENT_HVAC_OFF + if not zone_called: + return CURRENT_HVAC_IDLE + if system_status == SYSTEM_STATUS_COOL: + return CURRENT_HVAC_COOL + if system_status == SYSTEM_STATUS_HEAT: + return CURRENT_HVAC_HEAT + if system_status == SYSTEM_STATUS_IDLE: + return CURRENT_HVAC_IDLE + return CURRENT_HVAC_IDLE + + @property + def hvac_mode(self): + """Return current mode, as the user-visible name.""" + mode = self._device.get_requested_mode() + hold = self._device.is_in_permanent_hold() + + # If the device is in hold mode with + # OPERATION_MODE_AUTO + # overriding the schedule by still + # heating and cooling to the + # temp range. + if hold and mode == OPERATION_MODE_AUTO: + return HVAC_MODE_HEAT_COOL + + return NEXIA_TO_HA_HVAC_MODE_MAP[mode] + + @property + def hvac_modes(self): + """List of HVAC available modes.""" + return [ + HVAC_MODE_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + ] + + def set_temperature(self, **kwargs): + """Set target temperature.""" + new_heat_temp = kwargs.get(ATTR_TARGET_TEMP_LOW, None) + new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH, None) + set_temp = kwargs.get(ATTR_TEMPERATURE, None) + + deadband = self.thermostat.get_deadband() + cur_cool_temp = self._device.get_cooling_setpoint() + cur_heat_temp = self._device.get_heating_setpoint() + (min_temp, max_temp) = self.thermostat.get_setpoint_limits() + + # Check that we're not going to hit any minimum or maximum values + if new_heat_temp and new_heat_temp + deadband > max_temp: + new_heat_temp = max_temp - deadband + if new_cool_temp and new_cool_temp - deadband < min_temp: + new_cool_temp = min_temp + deadband + + # Check that we're within the deadband range, fix it if we're not + if new_heat_temp and new_heat_temp != cur_heat_temp: + if new_cool_temp - new_heat_temp < deadband: + new_cool_temp = new_heat_temp + deadband + if new_cool_temp and new_cool_temp != cur_cool_temp: + if new_cool_temp - new_heat_temp < deadband: + new_heat_temp = new_cool_temp - deadband + + self._device.set_heat_cool_temp( + heat_temperature=new_heat_temp, + cool_temperature=new_cool_temp, + set_temperature=set_temp, + ) + self.schedule_update_ha_state() + + @property + def is_aux_heat(self): + """Emergency heat state.""" + return self.thermostat.is_emergency_heat_active() + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._device.zone_id)}, + "name": self._device.get_name(), + "model": self.thermostat.get_model(), + "sw_version": self.thermostat.get_firmware(), + "manufacturer": MANUFACTURER, + "via_device": (DOMAIN, self.thermostat.thermostat_id), + } + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + data = { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_ZONE_STATUS: self._device.get_status(), + } + + if self._has_relative_humidity: + data.update( + { + ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support, + ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support, + ATTR_MIN_HUMIDITY: round( + self.thermostat.get_humidity_setpoint_limits()[0] * 100.0, 1, + ), + ATTR_MAX_HUMIDITY: round( + self.thermostat.get_humidity_setpoint_limits()[1] * 100.0, 1, + ), + } + ) + if self._has_dehumidify_support: + data.update( + { + ATTR_DEHUMIDIFY_SETPOINT: round( + self.thermostat.get_dehumidify_setpoint() * 100.0, 1 + ), + } + ) + if self._has_humidify_support: + data.update( + { + ATTR_HUMIDIFY_SETPOINT: round( + self.thermostat.get_humidify_setpoint() * 100.0, 1 + ) + } + ) + return data + + def set_preset_mode(self, preset_mode: str): + """Set the preset mode.""" + self._device.set_preset(preset_mode) + self.schedule_update_ha_state() + + def turn_aux_heat_off(self): + """Turn. Aux Heat off.""" + self.thermostat.set_emergency_heat(False) + self.schedule_update_ha_state() + + def turn_aux_heat_on(self): + """Turn. Aux Heat on.""" + self.thermostat.set_emergency_heat(True) + self.schedule_update_ha_state() + + def turn_off(self): + """Turn. off the zone.""" + self.set_hvac_mode(OPERATION_MODE_OFF) + self.schedule_update_ha_state() + + def turn_on(self): + """Turn. on the zone.""" + self.set_hvac_mode(OPERATION_MODE_AUTO) + self.schedule_update_ha_state() + + def set_hvac_mode(self, hvac_mode: str) -> None: + """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" + if hvac_mode == HVAC_MODE_AUTO: + self._device.call_return_to_schedule() + self._device.set_mode(mode=OPERATION_MODE_AUTO) + else: + self._device.call_permanent_hold() + self._device.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) + + self.schedule_update_ha_state() + + def set_aircleaner_mode(self, aircleaner_mode): + """Set the aircleaner mode.""" + self.thermostat.set_air_cleaner(aircleaner_mode) + self.schedule_update_ha_state() + + def set_humidify_setpoint(self, humidify_setpoint): + """Set the humidify setpoint.""" + self.thermostat.set_humidify_setpoint(humidify_setpoint / 100.0) + self.schedule_update_ha_state() + + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self._coordinator.async_request_refresh() diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py new file mode 100644 index 00000000000..a991b6056c3 --- /dev/null +++ b/homeassistant/components/nexia/config_flow.py @@ -0,0 +1,87 @@ +"""Config flow for Nexia integration.""" +import logging + +from nexia.home import NexiaHome +from requests.exceptions import ConnectTimeout, HTTPError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str}) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + try: + nexia_home = NexiaHome( + username=data[CONF_USERNAME], + password=data[CONF_PASSWORD], + auto_login=False, + auto_update=False, + ) + await hass.async_add_executor_job(nexia_home.login) + except ConnectTimeout as ex: + _LOGGER.error("Unable to connect to Nexia service: %s", ex) + raise CannotConnect + except HTTPError as http_ex: + _LOGGER.error("HTTP error from Nexia service: %s", http_ex) + if http_ex.response.status_code >= 400 and http_ex.response.status_code < 500: + raise InvalidAuth + raise CannotConnect + + if not nexia_home.get_name(): + raise InvalidAuth + + info = {"title": nexia_home.get_name(), "house_id": nexia_home.house_id} + _LOGGER.debug("Setup ok with info: %s", info) + return info + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Nexia.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(info["house_id"]) + 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 + ) + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py new file mode 100644 index 00000000000..7def5f156b4 --- /dev/null +++ b/homeassistant/components/nexia/const.py @@ -0,0 +1,26 @@ +"""Nexia constants.""" + +PLATFORMS = ["sensor", "binary_sensor", "climate"] + +ATTRIBUTION = "Data provided by mynexia.com" + +NOTIFICATION_ID = "nexia_notification" +NOTIFICATION_TITLE = "Nexia Setup" + +DATA_NEXIA = "nexia" +NEXIA_DEVICE = "device" +NEXIA_SCAN_INTERVAL = "scan_interval" + +DOMAIN = "nexia" +DEFAULT_ENTITY_NAMESPACE = "nexia" + +ATTR_ZONE_STATUS = "zone_status" +ATTR_HUMIDIFY_SUPPORTED = "humidify_supported" +ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" +ATTR_HUMIDIFY_SETPOINT = "humidify_setpoint" +ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint" + +UPDATE_COORDINATOR = "update_coordinator" + + +MANUFACTURER = "Trane" diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py new file mode 100644 index 00000000000..ec02a7e5f21 --- /dev/null +++ b/homeassistant/components/nexia/entity.py @@ -0,0 +1,30 @@ +"""The nexia integration base entity.""" + +from homeassistant.helpers.entity import Entity + + +class NexiaEntity(Entity): + """Base class for nexia entities.""" + + def __init__(self, coordinator): + """Initialize the entity.""" + super().__init__() + self._coordinator = coordinator + + @property + def available(self): + """Return True if entity is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Undo subscription.""" + self._coordinator.async_remove_listener(self.async_write_ha_state) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json new file mode 100644 index 00000000000..02804bf0419 --- /dev/null +++ b/homeassistant/components/nexia/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "nexia", + "name": "Nexia", + "requirements": [ + "nexia==0.4.1" + ], + "dependencies": [], + "codeowners": [ + "@ryannazaretian", "@bdraco" + ], + "documentation": "https://www.home-assistant.io/integrations/nexia", + "config_flow": true +} diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py new file mode 100644 index 00000000000..251101ccb1e --- /dev/null +++ b/homeassistant/components/nexia/sensor.py @@ -0,0 +1,276 @@ +"""Support for Nexia / Trane XL Thermostats.""" + +from nexia.const import UNIT_CELSIUS + +from homeassistant.const import ( + ATTR_ATTRIBUTION, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_TEMPERATURE, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + UNIT_PERCENTAGE, +) + +from .const import ( + ATTRIBUTION, + DATA_NEXIA, + DOMAIN, + MANUFACTURER, + NEXIA_DEVICE, + UPDATE_COORDINATOR, +) +from .entity import NexiaEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up sensors for a Nexia device.""" + + nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_home = nexia_data[NEXIA_DEVICE] + coordinator = nexia_data[UPDATE_COORDINATOR] + entities = [] + + # Thermostat / System Sensors + for thermostat_id in nexia_home.get_thermostat_ids(): + thermostat = nexia_home.get_thermostat_by_id(thermostat_id) + + entities.append( + NexiaSensor( + coordinator, + thermostat, + "get_system_status", + "System Status", + None, + None, + ) + ) + # Air cleaner + entities.append( + NexiaSensor( + coordinator, + thermostat, + "get_air_cleaner_mode", + "Air Cleaner Mode", + None, + None, + ) + ) + # Compressor Speed + if thermostat.has_variable_speed_compressor(): + entities.append( + NexiaSensor( + coordinator, + thermostat, + "get_current_compressor_speed", + "Current Compressor Speed", + None, + UNIT_PERCENTAGE, + percent_conv, + ) + ) + entities.append( + NexiaSensor( + coordinator, + thermostat, + "get_requested_compressor_speed", + "Requested Compressor Speed", + None, + UNIT_PERCENTAGE, + percent_conv, + ) + ) + # Outdoor Temperature + if thermostat.has_outdoor_temperature(): + unit = ( + TEMP_CELSIUS + if thermostat.get_unit() == UNIT_CELSIUS + else TEMP_FAHRENHEIT + ) + entities.append( + NexiaSensor( + coordinator, + thermostat, + "get_outdoor_temperature", + "Outdoor Temperature", + DEVICE_CLASS_TEMPERATURE, + unit, + ) + ) + # Relative Humidity + if thermostat.has_relative_humidity(): + entities.append( + NexiaSensor( + coordinator, + thermostat, + "get_relative_humidity", + "Relative Humidity", + DEVICE_CLASS_HUMIDITY, + UNIT_PERCENTAGE, + percent_conv, + ) + ) + + # Zone Sensors + for zone_id in thermostat.get_zone_ids(): + zone = thermostat.get_zone_by_id(zone_id) + unit = ( + TEMP_CELSIUS + if thermostat.get_unit() == UNIT_CELSIUS + else TEMP_FAHRENHEIT + ) + # Temperature + entities.append( + NexiaZoneSensor( + coordinator, + zone, + "get_temperature", + "Temperature", + DEVICE_CLASS_TEMPERATURE, + unit, + None, + ) + ) + # Zone Status + entities.append( + NexiaZoneSensor( + coordinator, zone, "get_status", "Zone Status", None, None, + ) + ) + # Setpoint Status + entities.append( + NexiaZoneSensor( + coordinator, + zone, + "get_setpoint_status", + "Zone Setpoint Status", + None, + None, + ) + ) + + async_add_entities(entities, True) + + +def percent_conv(val): + """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" + return val * 100.0 + + +class NexiaSensor(NexiaEntity): + """Provides Nexia thermostat sensor support.""" + + def __init__( + self, + coordinator, + device, + sensor_call, + sensor_name, + sensor_class, + sensor_unit, + modifier=None, + ): + """Initialize the sensor.""" + super().__init__(coordinator) + self._coordinator = coordinator + self._device = device + self._call = sensor_call + self._sensor_name = sensor_name + self._class = sensor_class + self._state = None + self._name = f"{self._device.get_name()} {self._sensor_name}" + self._unit_of_measurement = sensor_unit + self._modifier = modifier + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + # This is the thermostat unique_id + return f"{self._device.thermostat_id}_{self._call}" + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + } + + @property + def device_class(self): + """Return the device class of the sensor.""" + return self._class + + @property + def state(self): + """Return the state of the sensor.""" + val = getattr(self._device, self._call)() + if self._modifier: + val = self._modifier(val) + if isinstance(val, float): + val = round(val, 1) + return val + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._device.thermostat_id)}, + "name": self._device.get_name(), + "model": self._device.get_model(), + "sw_version": self._device.get_firmware(), + "manufacturer": MANUFACTURER, + } + + +class NexiaZoneSensor(NexiaSensor): + """Nexia Zone Sensor Support.""" + + def __init__( + self, + coordinator, + device, + sensor_call, + sensor_name, + sensor_class, + sensor_unit, + modifier=None, + ): + """Create a zone sensor.""" + + super().__init__( + coordinator, + device, + sensor_call, + sensor_name, + sensor_class, + sensor_unit, + modifier, + ) + self._device = device + + @property + def unique_id(self): + """Return the unique id of the sensor.""" + # This is the zone unique_id + return f"{self._device.zone_id}_{self._call}" + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._device.zone_id)}, + "name": self._device.get_name(), + "model": self._device.thermostat.get_model(), + "sw_version": self._device.thermostat.get_firmware(), + "manufacturer": MANUFACTURER, + "via_device": (DOMAIN, self._device.thermostat.thermostat_id), + } diff --git a/homeassistant/components/nexia/strings.json b/homeassistant/components/nexia/strings.json new file mode 100644 index 00000000000..d3fabfb0b4d --- /dev/null +++ b/homeassistant/components/nexia/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "Nexia", + "step": { + "user": { + "title": "Connect to mynexia.com", + "data": { + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "This nexia home is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index ac24ecb9209..c981a88984e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -73,6 +73,7 @@ FLOWS = [ "neato", "nest", "netatmo", + "nexia", "notion", "opentherm_gw", "openuv", diff --git a/requirements_all.txt b/requirements_all.txt index 4eaffe245fc..2c8bd87d43f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -916,6 +916,9 @@ netdisco==2.6.0 # homeassistant.components.neurio_energy neurio==0.3.1 +# homeassistant.components.nexia +nexia==0.4.1 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 72acaf4dd1d..45bd9b53458 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -343,6 +343,9 @@ nessclient==0.9.15 # homeassistant.components.ssdp netdisco==2.6.0 +# homeassistant.components.nexia +nexia==0.4.1 + # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 diff --git a/tests/components/nexia/__init__.py b/tests/components/nexia/__init__.py new file mode 100644 index 00000000000..27e986cc148 --- /dev/null +++ b/tests/components/nexia/__init__.py @@ -0,0 +1 @@ +"""Tests for the Nexia integration.""" diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py new file mode 100644 index 00000000000..3cb57d77f12 --- /dev/null +++ b/tests/components/nexia/test_config_flow.py @@ -0,0 +1,76 @@ +"""Test the nexia config flow.""" +from asynctest import patch +from asynctest.mock import MagicMock +from requests.exceptions import ConnectTimeout + +from homeassistant import config_entries, setup +from homeassistant.components.nexia.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.nexia.config_flow.NexiaHome.get_name", + return_value="myhouse", + ), patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + side_effect=MagicMock(), + ), patch( + "homeassistant.components.nexia.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nexia.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "myhouse" + assert result2["data"] == { + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch("homeassistant.components.nexia.config_flow.NexiaHome.login"): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nexia.config_flow.NexiaHome.login", + side_effect=ConnectTimeout, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 8cb1d630c896babf693d6a413d69549b75e20e54 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 19 Mar 2020 22:14:07 -0400 Subject: [PATCH 141/431] Poll Hue lights in ZHA (#33017) * Weighted ZHA entity matching. * Poll ZHA Hue lights in 5 min intervals. * Add comment. * Spelling. --- .../components/zha/core/registries.py | 27 ++++++++- homeassistant/components/zha/light.py | 18 +++++- tests/components/zha/test_registries.py | 60 +++++++++++++++++++ 3 files changed, 101 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 3b08d1acd37..9aeb7832f63 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -196,6 +196,30 @@ class MatchRule: factory=frozenset, converter=set_or_callable ) + @property + def weight(self) -> int: + """Return the weight of the matching rule. + + Most specific matches should be preferred over less specific. Model matching + rules have a priority over manufacturer matching rules and rules matching a + single model/manufacturer get a better priority over rules matching multiple + models/manufacturers. And any model or manufacturers matching rules get better + priority over rules matching only channels. + But in case of a channel name/channel id matching, we give rules matching + multiple channels a better priority over rules matching a single channel. + """ + weight = 0 + if self.models: + weight += 401 - len(self.models) + + if self.manufacturers: + weight += 301 - len(self.manufacturers) + + weight += 10 * len(self.channel_names) + weight += 5 * len(self.generic_ids) + weight += 1 * len(self.aux_channels) + return weight + def claim_channels(self, channel_pool: List[ChannelType]) -> List[ChannelType]: """Return a list of channels this rule matches + aux channels.""" claimed = [] @@ -268,7 +292,8 @@ class ZHAEntityRegistry: default: CALLABLE_T = None, ) -> Tuple[CALLABLE_T, List[ChannelType]]: """Match a ZHA Channels to a ZHA Entity class.""" - for match in self._strict_registry[component]: + matches = self._strict_registry[component] + for match in sorted(matches, key=lambda x: x.weight, reverse=True): if match.strict_matched(manufacturer, model, channels): claimed = match.claim_channels(channels) return self._strict_registry[component][match], claimed diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 435f8940032..15ea8c0340b 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -47,7 +47,6 @@ FLASH_EFFECTS = {light.FLASH_SHORT: EFFECT_BLINK, light.FLASH_LONG: EFFECT_BREAT UNSUPPORTED_ATTRIBUTE = 0x86 STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) PARALLEL_UPDATES = 0 -_REFRESH_INTERVAL = (45, 75) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -68,6 +67,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): class Light(ZhaEntity, light.Light): """Representation of a ZHA or ZLL light.""" + _REFRESH_INTERVAL = (45, 75) + def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) @@ -177,9 +178,9 @@ class Light(ZhaEntity, light.Light): await self.async_accept_signal( self._level_channel, SIGNAL_SET_LEVEL, self.set_level ) - refresh_interval = random.randint(*_REFRESH_INTERVAL) + refresh_interval = random.randint(*[x * 60 for x in self._REFRESH_INTERVAL]) self._cancel_refresh_handle = async_track_time_interval( - self.hass, self._refresh, timedelta(minutes=refresh_interval) + self.hass, self._refresh, timedelta(seconds=refresh_interval) ) async def async_will_remove_from_hass(self) -> None: @@ -398,3 +399,14 @@ class Light(ZhaEntity, light.Light): """Call async_get_state at an interval.""" await self.async_get_state(from_cache=False) self.async_write_ha_state() + + +@STRICT_MATCH( + channel_names=CHANNEL_ON_OFF, + aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}, + manufacturers="Philips", +) +class HueLight(Light): + """Representation of a HUE light which does not report attributes.""" + + _REFRESH_INTERVAL = (3, 5) diff --git a/tests/components/zha/test_registries.py b/tests/components/zha/test_registries.py index fc41a409518..2612019f6fe 100644 --- a/tests/components/zha/test_registries.py +++ b/tests/components/zha/test_registries.py @@ -254,3 +254,63 @@ def test_match_rule_claim_channels(rule, match, channel, channels): claimed = rule.claim_channels(channels) assert match == set([ch.name for ch in claimed]) + + +@pytest.fixture +def entity_registry(): + """Registry fixture.""" + return registries.ZHAEntityRegistry() + + +@pytest.mark.parametrize( + "manufacturer, model, match_name", + ( + ("random manufacturer", "random model", "OnOff"), + ("random manufacturer", MODEL, "OnOffModel"), + (MANUFACTURER, "random model", "OnOffManufacturer"), + (MANUFACTURER, MODEL, "OnOffModelManufacturer"), + (MANUFACTURER, "some model", "OnOffMultimodel"), + ), +) +def test_weighted_match(channel, entity_registry, manufacturer, model, match_name): + """Test weightedd match.""" + + s = mock.sentinel + + @entity_registry.strict_match( + s.component, + channel_names="on_off", + models={MODEL, "another model", "some model"}, + ) + class OnOffMultimodel: + pass + + @entity_registry.strict_match(s.component, channel_names="on_off") + class OnOff: + pass + + @entity_registry.strict_match( + s.component, channel_names="on_off", manufacturers=MANUFACTURER + ) + class OnOffManufacturer: + pass + + @entity_registry.strict_match(s.component, channel_names="on_off", models=MODEL) + class OnOffModel: + pass + + @entity_registry.strict_match( + s.component, channel_names="on_off", models=MODEL, manufacturers=MANUFACTURER + ) + class OnOffModelManufacturer: + pass + + ch_on_off = channel("on_off", 6) + ch_level = channel("level", 8) + + match, claimed = entity_registry.get_entity( + s.component, manufacturer, model, [ch_on_off, ch_level] + ) + + assert match.__name__ == match_name + assert claimed == [ch_on_off] From 1a4199c485139bf6c5418a52a6af7c7f03cdb3fe Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Thu, 19 Mar 2020 22:47:08 -0400 Subject: [PATCH 142/431] Handle zigpy clusters without ep_attribute attribute. (#33028) --- homeassistant/components/zha/core/channels/base.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index dca0bbe09f3..dfe564ec2c1 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -85,11 +85,11 @@ class ZigbeeChannel(LogMixin): self, cluster: zha_typing.ZigpyClusterType, ch_pool: zha_typing.ChannelPoolType ) -> None: """Initialize ZigbeeChannel.""" - self._channel_name = cluster.ep_attribute + self._generic_id = f"channel_0x{cluster.cluster_id:04x}" + self._channel_name = getattr(cluster, "ep_attribute", self._generic_id) if self.CHANNEL_NAME: self._channel_name = self.CHANNEL_NAME self._ch_pool = ch_pool - self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._cluster = cluster self._id = f"{ch_pool.id}:0x{cluster.cluster_id:04x}" unique_id = ch_pool.unique_id.replace("-", ":") From 5db1a67c20cdee1fd0e8e436563e98b694e5d6da Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 19 Mar 2020 22:43:09 -0500 Subject: [PATCH 143/431] Make powerwall unique id stable (#33021) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update powerwall unique id * Fix somfy optimistic mode when missing in conf (#32995) * Fix optimistic mode when missing in conf #32971 * Ease code using a default value * Client id and secret are now inclusive * Bump aiohomekit to fix Insignia NS-CH1XGO8 and Lennox S30 (#33014) * Axis - Fix char in stream url (#33004) * An unwanted character had found its way into a stream string, reverting f-string work to remove duplication of code and improve readability * Fix failing tests * deCONZ - Add support for Senic and Gira Friends of Hue remote… (#33022) * Update the test * Harmony config flow improvements (#33018) * Harmony config flow improvements * Address followup review comments from #32919 * pylint -- catching my naming error * remove leftovers from refactor * Update powerwall unique id * Update the test Co-authored-by: tetienne Co-authored-by: Jc2k Co-authored-by: Robert Svensson --- homeassistant/components/powerwall/entity.py | 2 -- tests/components/powerwall/test_sensor.py | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 0411f956bdc..04bb75fd47a 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -9,7 +9,6 @@ from .const import ( POWERWALL_SITE_NAME, SITE_INFO_GRID_CODE, SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH, - SITE_INFO_NOMINAL_SYSTEM_POWER_KW, SITE_INFO_UTILITY, ) @@ -26,7 +25,6 @@ class PowerWallEntity(Entity): unique_group = ( site_info[SITE_INFO_UTILITY], site_info[SITE_INFO_GRID_CODE], - str(site_info[SITE_INFO_NOMINAL_SYSTEM_POWER_KW]), str(site_info[SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH]), ) self.base_unique_id = "_".join(unique_group) diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index ea74f33671f..090e5dac445 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -24,7 +24,7 @@ async def test_sensors(hass): device_registry = await hass.helpers.device_registry.async_get_registry() reg_device = device_registry.async_get_device( - identifiers={("powerwall", "Wom Energy_60Hz_240V_s_IEEE1547a_2014_25_13.5")}, + identifiers={("powerwall", "Wom Energy_60Hz_240V_s_IEEE1547a_2014_13.5")}, connections=set(), ) assert reg_device.model == "PowerWall 2" From c3c5cc9ae7750578613e5eb52967a7bdaa5a5bd9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 19 Mar 2020 20:45:26 -0700 Subject: [PATCH 144/431] Fix zones in packages (#33027) --- homeassistant/config.py | 18 ++++++++++++++++-- tests/test_config.py | 18 ++++++++++++++++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index abb8511cab0..27aff8ca36b 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -565,9 +565,23 @@ def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> def _identify_config_schema(module: ModuleType) -> Tuple[Optional[str], Optional[Dict]]: """Extract the schema and identify list or dict based.""" try: - schema = module.CONFIG_SCHEMA.schema[module.DOMAIN] # type: ignore - except (AttributeError, KeyError): + key = next(k for k in module.CONFIG_SCHEMA.schema if k == module.DOMAIN) # type: ignore + except (AttributeError, StopIteration): return None, None + + schema = module.CONFIG_SCHEMA.schema[key] # type: ignore + + if hasattr(key, "default"): + default_value = schema(key.default()) + + if isinstance(default_value, dict): + return "dict", schema + + if isinstance(default_value, list): + return "list", schema + + return None, None + t_schema = str(schema) if t_schema.startswith("{") or "schema_with_slug_keys" in t_schema: return ("dict", schema) diff --git a/tests/test_config.py b/tests/test_config.py index fc5ec43093b..43f1263e581 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -10,6 +10,7 @@ from unittest.mock import Mock import asynctest from asynctest import CoroutineMock, patch import pytest +import voluptuous as vol from voluptuous import Invalid, MultipleInvalid import yaml @@ -989,3 +990,20 @@ async def test_component_config_exceptions(hass, caplog): "Unknown error validating config for test_platform platform for test_domain component with PLATFORM_SCHEMA" in caplog.text ) + + +@pytest.mark.parametrize( + "domain, schema, expected", + [ + ("zone", vol.Schema({vol.Optional("zone", default=[]): list}), "list"), + ("zone", vol.Schema({vol.Optional("zone", default=dict): dict}), "dict"), + ], +) +def test_identify_config_schema(domain, schema, expected): + """Test identify config schema.""" + assert ( + config_util._identify_config_schema(Mock(DOMAIN=domain, CONFIG_SCHEMA=schema))[ + 0 + ] + == expected + ) From d8e3e9abaa11aa4ba63e1924c2c9daa6ff894468 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Thu, 19 Mar 2020 21:54:41 -0600 Subject: [PATCH 145/431] Fix RainMachine not properly storing data in the config entry (#33002) * Fix bug related to RainMachine's default config flow * A * Fix tests * Code review --- .../components/rainmachine/__init__.py | 20 +++++------ .../components/rainmachine/config_flow.py | 34 ++++++++++++++++--- homeassistant/components/rainmachine/const.py | 6 ++++ .../rainmachine/test_config_flow.py | 4 ++- 4 files changed, 46 insertions(+), 18 deletions(-) diff --git a/homeassistant/components/rainmachine/__init__.py b/homeassistant/components/rainmachine/__init__.py index 4cf32185dc9..92c14d0e0cb 100644 --- a/homeassistant/components/rainmachine/__init__.py +++ b/homeassistant/components/rainmachine/__init__.py @@ -25,6 +25,7 @@ from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.service import verify_domain_control from .const import ( + CONF_ZONE_RUN_TIME, DATA_CLIENT, DATA_PROGRAMS, DATA_PROVISION_SETTINGS, @@ -33,6 +34,8 @@ from .const import ( DATA_ZONES, DATA_ZONES_DETAILS, DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_ZONE_RUN, DOMAIN, PROGRAM_UPDATE_TOPIC, SENSOR_UPDATE_TOPIC, @@ -41,19 +44,14 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -DATA_LISTENER = "listener" - CONF_CONTROLLERS = "controllers" CONF_PROGRAM_ID = "program_id" CONF_SECONDS = "seconds" CONF_ZONE_ID = "zone_id" -CONF_ZONE_RUN_TIME = "zone_run_time" DEFAULT_ATTRIBUTION = "Data provided by Green Electronics LLC" DEFAULT_ICON = "mdi:water" -DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) DEFAULT_SSL = True -DEFAULT_ZONE_RUN = 60 * 10 SERVICE_ALTER_PROGRAM = vol.Schema({vol.Required(CONF_PROGRAM_ID): cv.positive_int}) @@ -109,7 +107,6 @@ async def async_setup(hass, config): """Set up the RainMachine component.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_CLIENT] = {} - hass.data[DOMAIN][DATA_LISTENER] = {} if DOMAIN not in config: return True @@ -143,7 +140,7 @@ async def async_setup_entry(hass, config_entry): config_entry.data[CONF_IP_ADDRESS], config_entry.data[CONF_PASSWORD], port=config_entry.data[CONF_PORT], - ssl=config_entry.data[CONF_SSL], + ssl=config_entry.data.get(CONF_SSL, DEFAULT_SSL), ) except RainMachineError as err: _LOGGER.error("An error occurred: %s", err) @@ -156,8 +153,10 @@ async def async_setup_entry(hass, config_entry): rainmachine = RainMachine( hass, controller, - config_entry.data[CONF_ZONE_RUN_TIME], - config_entry.data[CONF_SCAN_INTERVAL], + config_entry.data.get(CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN), + config_entry.data.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL.total_seconds() + ), ) # Update the data object, which at this point (prior to any sensors registering @@ -260,9 +259,6 @@ async def async_unload_entry(hass, config_entry): """Unload an OpenUV config entry.""" hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) - remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(config_entry.entry_id) - remove_listener() - tasks = [ hass.config_entries.async_forward_entry_unload(config_entry, component) for component in ("binary_sensor", "sensor", "switch") diff --git a/homeassistant/components/rainmachine/config_flow.py b/homeassistant/components/rainmachine/config_flow.py index ffa46cc2c15..dc1ee16d05f 100644 --- a/homeassistant/components/rainmachine/config_flow.py +++ b/homeassistant/components/rainmachine/config_flow.py @@ -4,10 +4,22 @@ from regenmaschine.errors import RainMachineError import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT +from homeassistant.const import ( + CONF_IP_ADDRESS, + CONF_PASSWORD, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SSL, +) from homeassistant.helpers import aiohttp_client -from .const import DEFAULT_PORT, DOMAIN # pylint: disable=unused-import +from .const import ( # pylint: disable=unused-import + CONF_ZONE_RUN_TIME, + DEFAULT_PORT, + DEFAULT_SCAN_INTERVAL, + DEFAULT_ZONE_RUN, + DOMAIN, +) class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -53,8 +65,8 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): user_input[CONF_IP_ADDRESS], user_input[CONF_PASSWORD], websession, - port=user_input.get(CONF_PORT, DEFAULT_PORT), - ssl=True, + port=user_input[CONF_PORT], + ssl=user_input.get(CONF_SSL, True), ) except RainMachineError: return await self._show_form({CONF_PASSWORD: "invalid_credentials"}) @@ -63,5 +75,17 @@ class RainMachineFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): # access token without using the IP address and password, so we have to # store it: return self.async_create_entry( - title=user_input[CONF_IP_ADDRESS], data=user_input + title=user_input[CONF_IP_ADDRESS], + data={ + CONF_IP_ADDRESS: user_input[CONF_IP_ADDRESS], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_PORT: user_input[CONF_PORT], + CONF_SSL: user_input.get(CONF_SSL, True), + CONF_SCAN_INTERVAL: user_input.get( + CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL.total_seconds() + ), + CONF_ZONE_RUN_TIME: user_input.get( + CONF_ZONE_RUN_TIME, DEFAULT_ZONE_RUN + ), + }, ) diff --git a/homeassistant/components/rainmachine/const.py b/homeassistant/components/rainmachine/const.py index 855ff5d5df5..a88573476ba 100644 --- a/homeassistant/components/rainmachine/const.py +++ b/homeassistant/components/rainmachine/const.py @@ -1,6 +1,10 @@ """Define constants for the SimpliSafe component.""" +from datetime import timedelta + DOMAIN = "rainmachine" +CONF_ZONE_RUN_TIME = "zone_run_time" + DATA_CLIENT = "client" DATA_PROGRAMS = "programs" DATA_PROVISION_SETTINGS = "provision.settings" @@ -10,6 +14,8 @@ DATA_ZONES = "zones" DATA_ZONES_DETAILS = "zones_details" DEFAULT_PORT = 8080 +DEFAULT_SCAN_INTERVAL = timedelta(seconds=60) +DEFAULT_ZONE_RUN = 60 * 10 PROGRAM_UPDATE_TOPIC = f"{DOMAIN}_program_update" SENSOR_UPDATE_TOPIC = f"{DOMAIN}_data_update" diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index 38dafdda986..fca0f624a29 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import patch from regenmaschine.errors import RainMachineError from homeassistant import data_entry_flow -from homeassistant.components.rainmachine import DOMAIN, config_flow +from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN, config_flow from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_IP_ADDRESS, @@ -98,6 +98,7 @@ async def test_step_import(hass): CONF_PORT: 8080, CONF_SSL: True, CONF_SCAN_INTERVAL: 60, + CONF_ZONE_RUN_TIME: 600, } @@ -129,4 +130,5 @@ async def test_step_user(hass): CONF_PORT: 8080, CONF_SSL: True, CONF_SCAN_INTERVAL: 60, + CONF_ZONE_RUN_TIME: 600, } From 0ed7bc3b8ef0e2e82b554668cd66b73e764fb8eb Mon Sep 17 00:00:00 2001 From: Phil Bruckner Date: Thu, 19 Mar 2020 23:07:21 -0500 Subject: [PATCH 146/431] Convert amcrest binary sensors from poll to stream (#32818) * Convert amcrest binary sensors from poll to stream - Bump amcrest package to 1.6.0. - For online binary sensor poll camera periodically to test communications in case configuration & usage results in no other communication to camera. - Start a separate thread to call camera's event_stream method since it never returns. - Convert all received events into signals that cause corresponding sensors to update. - Use camera's generic event_channels_happened method to update sensors at startup, and whenever camera comes back online after being unavailable. * Changes per review * Changes per review 2 * Changes per review 3 - Move event stream decoding to amcrest package. - Change name of event processing threads so global counter is no longer required. - Bump amcrest package to 1.7.0. --- homeassistant/components/amcrest/__init__.py | 53 ++++++++++- .../components/amcrest/binary_sensor.py | 89 +++++++++++++++---- homeassistant/components/amcrest/const.py | 7 +- homeassistant/components/amcrest/helpers.py | 9 +- .../components/amcrest/manifest.json | 2 +- requirements_all.txt | 2 +- 6 files changed, 130 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/amcrest/__init__.py b/homeassistant/components/amcrest/__init__.py index b4b3e1866b4..be2a6b78f30 100644 --- a/homeassistant/components/amcrest/__init__.py +++ b/homeassistant/components/amcrest/__init__.py @@ -42,6 +42,8 @@ from .const import ( DATA_AMCREST, DEVICES, DOMAIN, + SENSOR_EVENT_CODE, + SERVICE_EVENT, SERVICE_UPDATE, ) from .helpers import service_signal @@ -96,9 +98,11 @@ AMCREST_SCHEMA = vol.Schema( vol.Optional(CONF_FFMPEG_ARGUMENTS, default=DEFAULT_ARGUMENTS): cv.string, vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, vol.Optional(CONF_BINARY_SENSORS): vol.All( - cv.ensure_list, [vol.In(BINARY_SENSORS)] + cv.ensure_list, [vol.In(BINARY_SENSORS)], vol.Unique() + ), + vol.Optional(CONF_SENSORS): vol.All( + cv.ensure_list, [vol.In(SENSORS)], vol.Unique() ), - vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [vol.In(SENSORS)]), vol.Optional(CONF_CONTROL_LIGHT, default=True): cv.boolean, } ) @@ -119,6 +123,8 @@ class AmcrestChecker(Http): self._wrap_errors = 0 self._wrap_lock = threading.Lock() self._wrap_login_err = False + self._wrap_event_flag = threading.Event() + self._wrap_event_flag.set() self._unsub_recheck = None super().__init__( host, @@ -134,16 +140,22 @@ class AmcrestChecker(Http): """Return if camera's API is responding.""" return self._wrap_errors <= MAX_ERRORS and not self._wrap_login_err + @property + def available_flag(self): + """Return threading event flag that indicates if camera's API is responding.""" + return self._wrap_event_flag + def _start_recovery(self): + self._wrap_event_flag.clear() dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) self._unsub_recheck = track_time_interval( self._hass, self._wrap_test_online, RECHECK_INTERVAL ) - def command(self, cmd, retries=None, timeout_cmd=None, stream=False): + def command(self, *args, **kwargs): """amcrest.Http.command wrapper to catch errors.""" try: - ret = super().command(cmd, retries, timeout_cmd, stream) + ret = super().command(*args, **kwargs) except LoginError as ex: with self._wrap_lock: was_online = self.available @@ -172,6 +184,7 @@ class AmcrestChecker(Http): self._unsub_recheck() self._unsub_recheck = None _LOGGER.error("%s camera back online", self._wrap_name) + self._wrap_event_flag.set() dispatcher_send(self._hass, service_signal(SERVICE_UPDATE, self._wrap_name)) return ret @@ -184,6 +197,31 @@ class AmcrestChecker(Http): pass +def _monitor_events(hass, name, api, event_codes): + event_codes = ",".join(event_codes) + while True: + api.available_flag.wait() + try: + for code, start in api.event_actions(event_codes, retries=5): + signal = service_signal(SERVICE_EVENT, name, code) + _LOGGER.debug("Sending signal: '%s': %s", signal, start) + dispatcher_send(hass, signal, start) + except AmcrestError as error: + _LOGGER.warning( + "Error while processing events from %s camera: %r", name, error + ) + + +def _start_event_monitor(hass, name, api, event_codes): + thread = threading.Thread( + target=_monitor_events, + name=f"Amcrest {name}", + args=(hass, name, api, event_codes), + daemon=True, + ) + thread.start() + + def setup(hass, config): """Set up the Amcrest IP Camera component.""" hass.data.setdefault(DATA_AMCREST, {DEVICES: {}, CAMERAS: []}) @@ -230,6 +268,13 @@ def setup(hass, config): {CONF_NAME: name, CONF_BINARY_SENSORS: binary_sensors}, config, ) + event_codes = [ + BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] + for sensor_type in binary_sensors + if BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] is not None + ] + if event_codes: + _start_event_monitor(hass, name, api, event_codes) if sensors: discovery.load_platform( diff --git a/homeassistant/components/amcrest/binary_sensor.py b/homeassistant/components/amcrest/binary_sensor.py index 809b448876c..40cb755bd98 100644 --- a/homeassistant/components/amcrest/binary_sensor.py +++ b/homeassistant/components/amcrest/binary_sensor.py @@ -10,12 +10,17 @@ from homeassistant.components.binary_sensor import ( BinarySensorDevice, ) from homeassistant.const import CONF_BINARY_SENSORS, CONF_NAME +from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( BINARY_SENSOR_SCAN_INTERVAL_SECS, DATA_AMCREST, DEVICES, + SENSOR_DEVICE_CLASS, + SENSOR_EVENT_CODE, + SENSOR_NAME, + SERVICE_EVENT, SERVICE_UPDATE, ) from .helpers import log_update_error, service_signal @@ -26,11 +31,20 @@ SCAN_INTERVAL = timedelta(seconds=BINARY_SENSOR_SCAN_INTERVAL_SECS) BINARY_SENSOR_MOTION_DETECTED = "motion_detected" BINARY_SENSOR_ONLINE = "online" -# Binary sensor types are defined like: Name, device class BINARY_SENSORS = { - BINARY_SENSOR_MOTION_DETECTED: ("Motion Detected", DEVICE_CLASS_MOTION), - BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY), + BINARY_SENSOR_MOTION_DETECTED: ( + "Motion Detected", + DEVICE_CLASS_MOTION, + "VideoMotion", + ), + BINARY_SENSOR_ONLINE: ("Online", DEVICE_CLASS_CONNECTIVITY, None), } +BINARY_SENSORS = { + k: dict(zip((SENSOR_NAME, SENSOR_DEVICE_CLASS, SENSOR_EVENT_CODE), v)) + for k, v in BINARY_SENSORS.items() +} + +_UPDATE_MSG = "Updating %s binary sensor" async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -54,18 +68,19 @@ class AmcrestBinarySensor(BinarySensorDevice): def __init__(self, name, device, sensor_type): """Initialize entity.""" - self._name = f"{name} {BINARY_SENSORS[sensor_type][0]}" + self._name = f"{name} {BINARY_SENSORS[sensor_type][SENSOR_NAME]}" self._signal_name = name self._api = device.api self._sensor_type = sensor_type self._state = None - self._device_class = BINARY_SENSORS[sensor_type][1] - self._unsub_dispatcher = None + self._device_class = BINARY_SENSORS[sensor_type][SENSOR_DEVICE_CLASS] + self._event_code = BINARY_SENSORS[sensor_type][SENSOR_EVENT_CODE] + self._unsub_dispatcher = [] @property def should_poll(self): """Return True if entity has to be polled for state.""" - return self._sensor_type != BINARY_SENSOR_ONLINE + return self._sensor_type == BINARY_SENSOR_ONLINE @property def name(self): @@ -89,16 +104,34 @@ class AmcrestBinarySensor(BinarySensorDevice): def update(self): """Update entity.""" + if self._sensor_type == BINARY_SENSOR_ONLINE: + self._update_online() + else: + self._update_others() + + def _update_online(self): + if not (self._api.available or self.is_on): + return + _LOGGER.debug(_UPDATE_MSG, self._name) + if self._api.available: + # Send a command to the camera to test if we can still communicate with it. + # Override of Http.command() in __init__.py will set self._api.available + # accordingly. + try: + self._api.current_time + except AmcrestError: + pass + self._state = self._api.available + + def _update_others(self): if not self.available: return - _LOGGER.debug("Updating %s binary sensor", self._name) + _LOGGER.debug(_UPDATE_MSG, self._name) try: - if self._sensor_type == BINARY_SENSOR_MOTION_DETECTED: - self._state = self._api.is_motion_detected - - elif self._sensor_type == BINARY_SENSOR_ONLINE: - self._state = self._api.available + self._state = "channels" in self._api.event_channels_happened( + self._event_code + ) except AmcrestError as error: log_update_error(_LOGGER, "update", self.name, "binary sensor", error) @@ -106,14 +139,32 @@ class AmcrestBinarySensor(BinarySensorDevice): """Update state.""" self.async_schedule_update_ha_state(True) + @callback + def async_event_received(self, start): + """Update state from received event.""" + _LOGGER.debug(_UPDATE_MSG, self._name) + self._state = start + self.async_write_ha_state() + async def async_added_to_hass(self): - """Subscribe to update signal.""" - self._unsub_dispatcher = async_dispatcher_connect( - self.hass, - service_signal(SERVICE_UPDATE, self._signal_name), - self.async_on_demand_update, + """Subscribe to signals.""" + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(SERVICE_UPDATE, self._signal_name), + self.async_on_demand_update, + ) ) + if self._event_code: + self._unsub_dispatcher.append( + async_dispatcher_connect( + self.hass, + service_signal(SERVICE_EVENT, self._signal_name, self._event_code), + self.async_event_received, + ) + ) async def async_will_remove_from_hass(self): """Disconnect from update signal.""" - self._unsub_dispatcher() + for unsub_dispatcher in self._unsub_dispatcher: + unsub_dispatcher() diff --git a/homeassistant/components/amcrest/const.py b/homeassistant/components/amcrest/const.py index 38ff8a8894e..da7e5456786 100644 --- a/homeassistant/components/amcrest/const.py +++ b/homeassistant/components/amcrest/const.py @@ -4,11 +4,16 @@ DATA_AMCREST = DOMAIN CAMERAS = "cameras" DEVICES = "devices" -BINARY_SENSOR_SCAN_INTERVAL_SECS = 5 +BINARY_SENSOR_SCAN_INTERVAL_SECS = 60 CAMERA_WEB_SESSION_TIMEOUT = 10 COMM_RETRIES = 1 COMM_TIMEOUT = 6.05 SENSOR_SCAN_INTERVAL_SECS = 10 SNAPSHOT_TIMEOUT = 20 +SERVICE_EVENT = "event" SERVICE_UPDATE = "update" + +SENSOR_DEVICE_CLASS = "class" +SENSOR_EVENT_CODE = "code" +SENSOR_NAME = "name" diff --git a/homeassistant/components/amcrest/helpers.py b/homeassistant/components/amcrest/helpers.py index 57d1a73c97e..884d39abd70 100644 --- a/homeassistant/components/amcrest/helpers.py +++ b/homeassistant/components/amcrest/helpers.py @@ -2,12 +2,9 @@ from .const import DOMAIN -def service_signal(service, ident=None): - """Encode service and identifier into signal.""" - signal = f"{DOMAIN}_{service}" - if ident: - signal += f"_{ident.replace('.', '_')}" - return signal +def service_signal(service, *args): + """Encode signal.""" + return "_".join([DOMAIN, service, *args]) def log_update_error(logger, action, name, entity_type, error): diff --git a/homeassistant/components/amcrest/manifest.json b/homeassistant/components/amcrest/manifest.json index 38e19e4ec26..0b6fbbdc09a 100644 --- a/homeassistant/components/amcrest/manifest.json +++ b/homeassistant/components/amcrest/manifest.json @@ -2,7 +2,7 @@ "domain": "amcrest", "name": "Amcrest", "documentation": "https://www.home-assistant.io/integrations/amcrest", - "requirements": ["amcrest==1.5.6"], + "requirements": ["amcrest==1.7.0"], "dependencies": ["ffmpeg"], "codeowners": ["@pnbruckner"] } diff --git a/requirements_all.txt b/requirements_all.txt index 2c8bd87d43f..3ce516dc908 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -224,7 +224,7 @@ alpha_vantage==2.1.3 ambiclimate==0.2.1 # homeassistant.components.amcrest -amcrest==1.5.6 +amcrest==1.7.0 # homeassistant.components.androidtv androidtv==0.0.39 From 3461f3a1ed28014dc8d4fe8e3500507733ff38d6 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 20 Mar 2020 10:21:43 +0100 Subject: [PATCH 147/431] Tests improvements to the Brother integration (#32982) * Add tests for states of the sensors * Revert async_update method * Tests improvement * Fix icon name * Tests improvement * Simplify tests * Test improvement * Patch the library instead of the coordinator * Suggested change * Remove return_value --- homeassistant/components/brother/const.py | 2 +- homeassistant/components/brother/sensor.py | 4 + tests/components/brother/__init__.py | 29 ++- tests/components/brother/test_init.py | 110 ++------- tests/components/brother/test_sensor.py | 266 +++++++++++++++++++++ tests/fixtures/brother_printer_data.json | 58 ++++- 6 files changed, 375 insertions(+), 94 deletions(-) create mode 100644 tests/components/brother/test_sensor.py diff --git a/homeassistant/components/brother/const.py b/homeassistant/components/brother/const.py index 94d88162d76..d5bceaa2653 100644 --- a/homeassistant/components/brother/const.py +++ b/homeassistant/components/brother/const.py @@ -48,7 +48,7 @@ PRINTER_TYPES = ["laser", "ink"] SENSOR_TYPES = { ATTR_STATUS: { - ATTR_ICON: "icon:mdi:printer", + ATTR_ICON: "mdi:printer", ATTR_LABEL: ATTR_STATUS.title(), ATTR_UNIT: None, }, diff --git a/homeassistant/components/brother/sensor.py b/homeassistant/components/brother/sensor.py index aa108bf0ac7..b8142ac0c34 100644 --- a/homeassistant/components/brother/sensor.py +++ b/homeassistant/components/brother/sensor.py @@ -147,3 +147,7 @@ class BrotherPrinterSensor(Entity): async def async_will_remove_from_hass(self): """Disconnect from update signal.""" self.coordinator.async_remove_listener(self.async_write_ha_state) + + async def async_update(self): + """Update Brother entity.""" + await self.coordinator.async_request_refresh() diff --git a/tests/components/brother/__init__.py b/tests/components/brother/__init__.py index 91a7b7e92d4..d6c1fedd31d 100644 --- a/tests/components/brother/__init__.py +++ b/tests/components/brother/__init__.py @@ -1 +1,28 @@ -"""Tests for Brother Printer.""" +"""Tests for Brother Printer integration.""" +import json + +from asynctest import patch + +from homeassistant.components.brother.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_TYPE + +from tests.common import MockConfigEntry, load_fixture + + +async def init_integration(hass) -> MockConfigEntry: + """Set up the Brother integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="HL-L2340DW 0123456789", + unique_id="0123456789", + data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, + ) + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index ea9a255f75d..13378e9dbb9 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -1,38 +1,26 @@ """Test init of Brother integration.""" -from datetime import timedelta -import json - from asynctest import patch -import pytest -import homeassistant.components.brother as brother from homeassistant.components.brother.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE -from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.common import MockConfigEntry +from tests.components.brother import init_integration async def test_async_setup_entry(hass): """Test a successful setup entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="HL-L2340DW 0123456789", - data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, - ) - with patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + await init_integration(hass) - state = hass.states.get("sensor.hl_l2340dw_status") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "waiting" + state = hass.states.get("sensor.hl_l2340dw_status") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "waiting" async def test_config_not_ready(hass): @@ -40,73 +28,25 @@ async def test_config_not_ready(hass): entry = MockConfigEntry( domain=DOMAIN, title="HL-L2340DW 0123456789", + unique_id="0123456789", data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, ) - with patch( - "brother.Brother._get_data", side_effect=ConnectionError() - ), pytest.raises(ConfigEntryNotReady): - await brother.async_setup_entry(hass, entry) + + with patch("brother.Brother._get_data", side_effect=ConnectionError()): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == ENTRY_STATE_SETUP_RETRY async def test_unload_entry(hass): """Test successful unload of entry.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="HL-L2340DW 0123456789", - data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, - ) - with patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() + entry = await init_integration(hass) - assert hass.data[DOMAIN][entry.entry_id] + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state == ENTRY_STATE_LOADED - assert await hass.config_entries.async_unload(entry.entry_id) - assert not hass.data[DOMAIN] + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() - -async def test_availability(hass): - """Ensure that we mark the entities unavailable correctly when device is offline.""" - entry = MockConfigEntry( - domain=DOMAIN, - title="HL-L2340DW 0123456789", - data={CONF_HOST: "localhost", CONF_TYPE: "laser"}, - ) - with patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), - ): - entry.add_to_hass(hass) - assert await hass.config_entries.async_setup(entry.entry_id) - await hass.async_block_till_done() - - state = hass.states.get("sensor.hl_l2340dw_status") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "waiting" - - future = utcnow() + timedelta(minutes=5) - with patch("brother.Brother._get_data", side_effect=ConnectionError()): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("sensor.hl_l2340dw_status") - assert state is not None - assert state.state == STATE_UNAVAILABLE - - future = utcnow() + timedelta(minutes=10) - with patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), - ): - async_fire_time_changed(hass, future) - await hass.async_block_till_done() - - state = hass.states.get("sensor.hl_l2340dw_status") - assert state is not None - assert state.state != STATE_UNAVAILABLE - assert state.state == "waiting" + assert entry.state == ENTRY_STATE_NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/brother/test_sensor.py b/tests/components/brother/test_sensor.py new file mode 100644 index 00000000000..e88c22f3f40 --- /dev/null +++ b/tests/components/brother/test_sensor.py @@ -0,0 +1,266 @@ +"""Test sensor of Brother integration.""" +from datetime import timedelta +import json + +from asynctest import patch + +from homeassistant.components.brother.const import UNIT_PAGES +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + STATE_UNAVAILABLE, + TIME_DAYS, + UNIT_PERCENTAGE, +) +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from tests.common import async_fire_time_changed, load_fixture +from tests.components.brother import init_integration + +ATTR_REMAINING_PAGES = "remaining_pages" +ATTR_COUNTER = "counter" + + +async def test_sensors(hass): + """Test states of the sensors.""" + await init_integration(hass) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer" + assert state.state == "waiting" + + entry = registry.async_get("sensor.hl_l2340dw_status") + assert entry + assert entry.unique_id == "0123456789_status" + + state = hass.states.get("sensor.hl_l2340dw_black_toner_remaining") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "75" + + entry = registry.async_get("sensor.hl_l2340dw_black_toner_remaining") + assert entry + assert entry.unique_id == "0123456789_black_toner_remaining" + + state = hass.states.get("sensor.hl_l2340dw_cyan_toner_remaining") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "10" + + entry = registry.async_get("sensor.hl_l2340dw_cyan_toner_remaining") + assert entry + assert entry.unique_id == "0123456789_cyan_toner_remaining" + + state = hass.states.get("sensor.hl_l2340dw_magenta_toner_remaining") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "8" + + entry = registry.async_get("sensor.hl_l2340dw_magenta_toner_remaining") + assert entry + assert entry.unique_id == "0123456789_magenta_toner_remaining" + + state = hass.states.get("sensor.hl_l2340dw_yellow_toner_remaining") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d-nozzle" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "2" + + entry = registry.async_get("sensor.hl_l2340dw_yellow_toner_remaining") + assert entry + assert entry.unique_id == "0123456789_yellow_toner_remaining" + + state = hass.states.get("sensor.hl_l2340dw_drum_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_REMAINING_PAGES) == 11014 + assert state.attributes.get(ATTR_COUNTER) == 986 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "92" + + entry = registry.async_get("sensor.hl_l2340dw_drum_remaining_life") + assert entry + assert entry.unique_id == "0123456789_drum_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_black_drum_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 + assert state.attributes.get(ATTR_COUNTER) == 1611 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "92" + + entry = registry.async_get("sensor.hl_l2340dw_black_drum_remaining_life") + assert entry + assert entry.unique_id == "0123456789_black_drum_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_cyan_drum_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 + assert state.attributes.get(ATTR_COUNTER) == 1611 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "92" + + entry = registry.async_get("sensor.hl_l2340dw_cyan_drum_remaining_life") + assert entry + assert entry.unique_id == "0123456789_cyan_drum_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_magenta_drum_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 + assert state.attributes.get(ATTR_COUNTER) == 1611 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "92" + + entry = registry.async_get("sensor.hl_l2340dw_magenta_drum_remaining_life") + assert entry + assert entry.unique_id == "0123456789_magenta_drum_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_yellow_drum_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:chart-donut" + assert state.attributes.get(ATTR_REMAINING_PAGES) == 16389 + assert state.attributes.get(ATTR_COUNTER) == 1611 + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "92" + + entry = registry.async_get("sensor.hl_l2340dw_yellow_drum_remaining_life") + assert entry + assert entry.unique_id == "0123456789_yellow_drum_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_fuser_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:water-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "97" + + entry = registry.async_get("sensor.hl_l2340dw_fuser_remaining_life") + assert entry + assert entry.unique_id == "0123456789_fuser_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_belt_unit_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:current-ac" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "97" + + entry = registry.async_get("sensor.hl_l2340dw_belt_unit_remaining_life") + assert entry + assert entry.unique_id == "0123456789_belt_unit_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_pf_kit_1_remaining_life") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer-3d" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "98" + + entry = registry.async_get("sensor.hl_l2340dw_pf_kit_1_remaining_life") + assert entry + assert entry.unique_id == "0123456789_pf_kit_1_remaining_life" + + state = hass.states.get("sensor.hl_l2340dw_page_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "986" + + entry = registry.async_get("sensor.hl_l2340dw_page_counter") + assert entry + assert entry.unique_id == "0123456789_page_counter" + + state = hass.states.get("sensor.hl_l2340dw_duplex_unit_pages_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "538" + + entry = registry.async_get("sensor.hl_l2340dw_duplex_unit_pages_counter") + assert entry + assert entry.unique_id == "0123456789_duplex_unit_pages_counter" + + state = hass.states.get("sensor.hl_l2340dw_b_w_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "709" + + entry = registry.async_get("sensor.hl_l2340dw_b_w_counter") + assert entry + assert entry.unique_id == "0123456789_b/w_counter" + + state = hass.states.get("sensor.hl_l2340dw_color_counter") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:file-document-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PAGES + assert state.state == "902" + + entry = registry.async_get("sensor.hl_l2340dw_color_counter") + assert entry + assert entry.unique_id == "0123456789_color_counter" + + state = hass.states.get("sensor.hl_l2340dw_uptime") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:timer" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TIME_DAYS + assert state.state == "48" + + entry = registry.async_get("sensor.hl_l2340dw_uptime") + assert entry + assert entry.unique_id == "0123456789_uptime" + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when device is offline.""" + await init_integration(hass) + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "waiting" + + future = utcnow() + timedelta(minutes=5) + with patch("brother.Brother._get_data", side_effect=ConnectionError()): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=10) + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.hl_l2340dw_status") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "waiting" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + with patch("homeassistant.components.brother.Brother.async_update") as mock_update: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.hl_l2340dw_status"]}, + blocking=True, + ) + + assert len(mock_update.mock_calls) == 1 diff --git a/tests/fixtures/brother_printer_data.json b/tests/fixtures/brother_printer_data.json index 70e7add3c10..f4c36d988b1 100644 --- a/tests/fixtures/brother_printer_data.json +++ b/tests/fixtures/brother_printer_data.json @@ -1,31 +1,75 @@ { - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.10.0": ["000104000003da"], + "1.3.6.1.2.1.1.3.0": "413613515", + "1.3.6.1.2.1.43.10.2.1.4.1.1": "986", + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.10.0": [ + "000104000003da", + "010104000002c5", + "02010400000386", + "0601040000021a", + "0701040000012d", + "080104000000ed" + ], "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.17.0": "1.17", "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.8.0": [ - "63010400000001", "110104000003da", - "410104000023f0", "31010400000001", + "32010400000001", + "33010400000002", + "34010400000002", + "35010400000001", + "410104000023f0", + "54010400000001", + "55010400000001", + "63010400000001", + "68010400000001", + "690104000025e4", + "6a0104000025e4", + "6d010400002648", "6f010400001d4c", - "81010400000050", - "8601040000000a", - "7e01040000064b", + "700104000003e8", + "71010400000320", + "720104000000c8", "7301040000064b", "7401040000064b", "7501040000064b", + "76010400000001", + "77010400000001", + "78010400000001", "790104000023f0", "7a0104000023f0", "7b0104000023f0", - "800104000023f0" + "7e01040000064b", + "800104000023f0", + "81010400000050", + "8201040000000a", + "8301040000000a", + "8401040000000a", + "8601040000000a" ], "1.3.6.1.4.1.2435.2.3.9.1.1.7.0": "MFG:Brother;CMD:PJL,HBP,URF;MDL:HL-L2340DW series;CLS:PRINTER;CID:Brother Laser Type1;URF:W8,CP1,IS4-1,MT1-3-4-5-8,OB10,PQ4,RS300-600,V1.3,DM1;", "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.11.0": [ + "7301040000bd05", + "7701040000be65", "82010400002b06", + "8801040000bd34", "a4010400004005", "a5010400004005", "a6010400004005", "a7010400004005" ], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.21.0": [ + "00002302000025", + "00020016010200", + "00210200022202", + "020000a1040000" + ], + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.20.0": [ + "00a40100a50100", + "0100a301008801", + "01017301007701", + "870100a10100a2", + "a60100a70100a0" + ], "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.1.0": "0123456789", "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING " } \ No newline at end of file From f81464161ac4aa594d809695a70034ea0d08360a Mon Sep 17 00:00:00 2001 From: Jason Swails Date: Fri, 20 Mar 2020 05:42:34 -0400 Subject: [PATCH 148/431] Break dependency on lutron component (#33031) * Break dependency on lutron component * Fix isort complaint --- homeassistant/components/lutron_caseta/light.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index af225d2939d..53de8b66311 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -7,13 +7,22 @@ from homeassistant.components.light import ( SUPPORT_BRIGHTNESS, Light, ) -from homeassistant.components.lutron.light import to_hass_level, to_lutron_level from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice _LOGGER = logging.getLogger(__name__) +def to_lutron_level(level): + """Convert the given Home Assistant light level (0-255) to Lutron (0-100).""" + return int((level * 100) // 255) + + +def to_hass_level(level): + """Convert the given Lutron (0-100) light level to Home Assistant (0-255).""" + return int((level * 255) // 100) + + async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta lights.""" devs = [] From 6ab14a37294def6f084b2206f2873facf069362f Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 20 Mar 2020 15:20:42 +0100 Subject: [PATCH 149/431] Fix discovery issue with netatmo climate devices (#33040) --- homeassistant/components/netatmo/climate.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index aa269cfeb49..1f1b7088b29 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -441,6 +441,11 @@ class ThermostatData: except TypeError: _LOGGER.error("ThermostatData::setup() got error") return False + except pyatmo.exceptions.NoDevice: + _LOGGER.debug( + "No climate devices for %s (%s)", self.home_name, self.home_id + ) + return False return True @Throttle(MIN_TIME_BETWEEN_UPDATES) From eb77b94315c5a9e419aa262db3ac2fd6e8f329de Mon Sep 17 00:00:00 2001 From: cgtobi Date: Fri, 20 Mar 2020 15:22:27 +0100 Subject: [PATCH 150/431] Fix netatmo webhook registration issue (#32994) * Wait for cloud connection * Just wait * Remove redundant entry * Drop webhook before unloading other platforms * Add missing scope * Update homeassistant/components/netatmo/__init__.py Co-Authored-By: Paulus Schoutsen * Fix test Co-authored-by: Paulus Schoutsen --- homeassistant/components/netatmo/__init__.py | 18 ++++++++++++------ homeassistant/components/netatmo/camera.py | 12 ------------ .../components/netatmo/config_flow.py | 1 + homeassistant/components/netatmo/const.py | 3 ++- tests/components/netatmo/test_config_flow.py | 1 + 5 files changed, 16 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/netatmo/__init__.py b/homeassistant/components/netatmo/__init__.py index 776e63bef7d..b7d439b4a74 100644 --- a/homeassistant/components/netatmo/__init__.py +++ b/homeassistant/components/netatmo/__init__.py @@ -28,6 +28,7 @@ from . import api, config_flow from .const import ( AUTH, CONF_CLOUDHOOK_URL, + DATA_DEVICE_IDS, DATA_PERSONS, DOMAIN, OAUTH2_AUTHORIZE, @@ -65,6 +66,7 @@ async def async_setup(hass: HomeAssistant, config: dict): """Set up the Netatmo component.""" hass.data[DOMAIN] = {} hass.data[DOMAIN][DATA_PERSONS] = {} + hass.data[DOMAIN][DATA_DEVICE_IDS] = {} if DOMAIN not in config: return True @@ -104,7 +106,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) async def register_webhook(event): - # Wait for the could integration to be ready + # Wait for the cloud integration to be ready await asyncio.sleep(WAIT_FOR_CLOUD) if CONF_WEBHOOK_ID not in entry.data: @@ -112,6 +114,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.config_entries.async_update_entry(entry, data=data) if hass.components.cloud.async_active_subscription(): + # Wait for cloud connection to be established + await asyncio.sleep(WAIT_FOR_CLOUD) + if CONF_CLOUDHOOK_URL not in entry.data: webhook_url = await hass.components.cloud.async_create_cloudhook( entry.data[CONF_WEBHOOK_ID] @@ -144,6 +149,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" + if CONF_WEBHOOK_ID in entry.data: + await hass.async_add_executor_job( + hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook + ) + unload_ok = all( await asyncio.gather( *[ @@ -152,14 +162,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ] ) ) + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) - if CONF_WEBHOOK_ID in entry.data: - await hass.async_add_executor_job( - hass.data[DOMAIN][entry.entry_id][AUTH].dropwebhook() - ) - return unload_ok diff --git a/homeassistant/components/netatmo/camera.py b/homeassistant/components/netatmo/camera.py index 616d2a620c5..30f209625f6 100644 --- a/homeassistant/components/netatmo/camera.py +++ b/homeassistant/components/netatmo/camera.py @@ -84,21 +84,11 @@ class NetatmoCamera(Camera): self._unique_id = f"{self._camera_id}-{self._camera_type}" self._verify_ssl = verify_ssl self._quality = quality - - # URLs self._vpnurl = None self._localurl = None - - # Monitoring status self._status = None - - # SD Card status self._sd_status = None - - # Power status self._alim_status = None - - # Is local self._is_local = None def camera_image(self): @@ -219,8 +209,6 @@ class NetatmoCamera(Camera): def update(self): """Update entity status.""" - - # Refresh camera data self._data.update() camera = self._data.camera_data.get_camera(cid=self._camera_id) diff --git a/homeassistant/components/netatmo/config_flow.py b/homeassistant/components/netatmo/config_flow.py index dce87fb7931..6d524ab9f29 100644 --- a/homeassistant/components/netatmo/config_flow.py +++ b/homeassistant/components/netatmo/config_flow.py @@ -33,6 +33,7 @@ class NetatmoFlowHandler( "read_station", "read_thermostat", "write_camera", + "write_presence", "write_thermostat", ] diff --git a/homeassistant/components/netatmo/const.py b/homeassistant/components/netatmo/const.py index 0a0c9575600..4e4ff308755 100644 --- a/homeassistant/components/netatmo/const.py +++ b/homeassistant/components/netatmo/const.py @@ -14,12 +14,12 @@ MODELS = { "NOC": "Smart Outdoor Camera", "NSD": "Smart Smoke Alarm", "NACamDoorTag": "Smart Door and Window Sensors", + "NHC": "Smart Indoor Air Quality Monitor", "NAMain": "Smart Home Weather station – indoor module", "NAModule1": "Smart Home Weather station – outdoor module", "NAModule4": "Smart Additional Indoor module", "NAModule3": "Smart Rain Gauge", "NAModule2": "Smart Anemometer", - "NHC": "Home Coach", } AUTH = "netatmo_auth" @@ -32,6 +32,7 @@ CONF_CLOUDHOOK_URL = "cloudhook_url" OAUTH2_AUTHORIZE = "https://api.netatmo.com/oauth2/authorize" OAUTH2_TOKEN = "https://api.netatmo.com/oauth2/token" +DATA_DEVICE_IDS = "netatmo_device_ids" DATA_PERSONS = "netatmo_persons" NETATMO_WEBHOOK_URL = None diff --git a/tests/components/netatmo/test_config_flow.py b/tests/components/netatmo/test_config_flow.py index c9a663991cb..29a1d4f53d5 100644 --- a/tests/components/netatmo/test_config_flow.py +++ b/tests/components/netatmo/test_config_flow.py @@ -65,6 +65,7 @@ async def test_full_flow(hass, aiohttp_client, aioclient_mock): "read_station", "read_thermostat", "write_camera", + "write_presence", "write_thermostat", ] ) From ecbcdee934229514edbeca9287ddb435a68c259c Mon Sep 17 00:00:00 2001 From: Steven Rollason <2099542+gadgetchnnel@users.noreply.github.com> Date: Fri, 20 Mar 2020 14:58:23 +0000 Subject: [PATCH 151/431] Bump pyhaversion to 3.3.0 (#33044) --- 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 37f88d16654..8d79234375c 100644 --- a/homeassistant/components/version/manifest.json +++ b/homeassistant/components/version/manifest.json @@ -2,7 +2,7 @@ "domain": "version", "name": "Version", "documentation": "https://www.home-assistant.io/integrations/version", - "requirements": ["pyhaversion==3.2.0"], + "requirements": ["pyhaversion==3.3.0"], "dependencies": [], "codeowners": ["@fabaff"], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index 3ce516dc908..17cc834e0b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1298,7 +1298,7 @@ pygogogate2==0.1.1 pygtfs==0.1.5 # homeassistant.components.version -pyhaversion==3.2.0 +pyhaversion==3.3.0 # homeassistant.components.heos pyheos==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 45bd9b53458..33d81c03d5b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -491,7 +491,7 @@ pyfttt==0.3 pygatt[GATTTOOL]==4.0.5 # homeassistant.components.version -pyhaversion==3.2.0 +pyhaversion==3.3.0 # homeassistant.components.heos pyheos==0.6.0 From 661101df08e36ab33f4f4418faf4f3395b07be5b Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Mar 2020 16:17:43 +0100 Subject: [PATCH 152/431] Fix packages for schemas without a default (#33045) --- homeassistant/components/person/__init__.py | 6 +++++- homeassistant/config.py | 4 +++- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/person/__init__.py b/homeassistant/components/person/__init__.py index 9cd3e882c48..006929c7345 100644 --- a/homeassistant/components/person/__init__.py +++ b/homeassistant/components/person/__init__.py @@ -77,7 +77,11 @@ PERSON_SCHEMA = vol.Schema( ) CONFIG_SCHEMA = vol.Schema( - {vol.Optional(DOMAIN): vol.All(cv.ensure_list, cv.remove_falsy, [PERSON_SCHEMA])}, + { + vol.Optional(DOMAIN, default=[]): vol.All( + cv.ensure_list, cv.remove_falsy, [PERSON_SCHEMA] + ) + }, extra=vol.ALLOW_EXTRA, ) diff --git a/homeassistant/config.py b/homeassistant/config.py index 27aff8ca36b..b1cd49b0852 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -571,7 +571,9 @@ def _identify_config_schema(module: ModuleType) -> Tuple[Optional[str], Optional schema = module.CONFIG_SCHEMA.schema[key] # type: ignore - if hasattr(key, "default"): + if hasattr(key, "default") and not isinstance( + key.default, vol.schema_builder.Undefined + ): default_value = schema(key.default()) if isinstance(default_value, dict): From 414559f0184f295c30e6f7f5414a03969fedbbc7 Mon Sep 17 00:00:00 2001 From: GaryOkie <37629938+GaryOkie@users.noreply.github.com> Date: Fri, 20 Mar 2020 11:34:42 -0500 Subject: [PATCH 153/431] Enable incremental Pan/Tilt/Zoom capability to Amcrest/Dahua cameras (#32839) Add new "amcrest.ptz_control" service to specify a PTZ camera movement or zoom direction (up, down, right, left, right_up, right_down, left_up, left_down, zoom_in, zoom_out). An optional travel_time attribute specifies the amount of movement between start/stop. --- homeassistant/components/amcrest/camera.py | 57 +++++++++++++++++++ .../components/amcrest/services.yaml | 13 +++++ 2 files changed, 70 insertions(+) diff --git a/homeassistant/components/amcrest/camera.py b/homeassistant/components/amcrest/camera.py index f9515256403..4b3640c1543 100644 --- a/homeassistant/components/amcrest/camera.py +++ b/homeassistant/components/amcrest/camera.py @@ -21,6 +21,7 @@ from homeassistant.helpers.aiohttp_client import ( async_aiohttp_proxy_web, async_get_clientsession, ) +import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect from .const import ( @@ -51,6 +52,28 @@ _SRV_CBW = "set_color_bw" _SRV_TOUR_ON = "start_tour" _SRV_TOUR_OFF = "stop_tour" +_SRV_PTZ_CTRL = "ptz_control" +_ATTR_PTZ_TT = "travel_time" +_ATTR_PTZ_MOV = "movement" +_MOV = [ + "zoom_out", + "zoom_in", + "right", + "left", + "up", + "down", + "right_down", + "right_up", + "left_down", + "left_up", +] +_ZOOM_ACTIONS = ["ZoomWide", "ZoomTele"] +_MOVE_1_ACTIONS = ["Right", "Left", "Up", "Down"] +_MOVE_2_ACTIONS = ["RightDown", "RightUp", "LeftDown", "LeftUp"] +_ACTION = _ZOOM_ACTIONS + _MOVE_1_ACTIONS + _MOVE_2_ACTIONS + +_DEFAULT_TT = 0.2 + _ATTR_PRESET = "preset" _ATTR_COLOR_BW = "color_bw" @@ -65,6 +88,12 @@ _SRV_GOTO_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( _SRV_CBW_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( {vol.Required(_ATTR_COLOR_BW): vol.In(_CBW)} ) +_SRV_PTZ_SCHEMA = CAMERA_SERVICE_SCHEMA.extend( + { + vol.Required(_ATTR_PTZ_MOV): vol.In(_MOV), + vol.Optional(_ATTR_PTZ_TT, default=_DEFAULT_TT): cv.small_float, + } +) CAMERA_SERVICES = { _SRV_EN_REC: (CAMERA_SERVICE_SCHEMA, "async_enable_recording", ()), @@ -77,6 +106,11 @@ CAMERA_SERVICES = { _SRV_CBW: (_SRV_CBW_SCHEMA, "async_set_color_bw", (_ATTR_COLOR_BW,)), _SRV_TOUR_ON: (CAMERA_SERVICE_SCHEMA, "async_start_tour", ()), _SRV_TOUR_OFF: (CAMERA_SERVICE_SCHEMA, "async_stop_tour", ()), + _SRV_PTZ_CTRL: ( + _SRV_PTZ_SCHEMA, + "async_ptz_control", + (_ATTR_PTZ_MOV, _ATTR_PTZ_TT), + ), } _BOOL_TO_STATE = {True: STATE_ON, False: STATE_OFF} @@ -406,6 +440,29 @@ class AmcrestCam(Camera): """Call the job and stop camera tour.""" await self.hass.async_add_executor_job(self._start_tour, False) + async def async_ptz_control(self, movement, travel_time): + """Move or zoom camera in specified direction.""" + code = _ACTION[_MOV.index(movement)] + + kwargs = {"code": code, "arg1": 0, "arg2": 0, "arg3": 0} + if code in _MOVE_1_ACTIONS: + kwargs["arg2"] = 1 + elif code in _MOVE_2_ACTIONS: + kwargs["arg1"] = kwargs["arg2"] = 1 + + try: + await self.hass.async_add_executor_job( + partial(self._api.ptz_control_command, action="start", **kwargs) + ) + await asyncio.sleep(travel_time) + await self.hass.async_add_executor_job( + partial(self._api.ptz_control_command, action="stop", **kwargs) + ) + except AmcrestError as error: + log_update_error( + _LOGGER, "move", self.name, f"camera PTZ {movement}", error + ) + # Methods to send commands to Amcrest camera and handle errors def _enable_video_stream(self, enable): diff --git a/homeassistant/components/amcrest/services.yaml b/homeassistant/components/amcrest/services.yaml index d6e7a02a4f9..820f965c533 100644 --- a/homeassistant/components/amcrest/services.yaml +++ b/homeassistant/components/amcrest/services.yaml @@ -73,3 +73,16 @@ stop_tour: entity_id: description: "Name(s) of the cameras, or 'all' for all cameras." example: 'camera.house_front' + +ptz_control: + description: Move (Pan/Tilt) and/or Zoom a PTZ camera + fields: + entity_id: + description: "Name of the camera, or 'all' for all cameras." + example: 'camera.house_front' + movement: + description: "up, down, right, left, right_up, right_down, left_up, left_down, zoom_in, zoom_out" + example: 'right' + travel_time: + description: "(optional) Travel time in fractional seconds: from 0 to 1. Default: .2" + example: '.5' From c00f04221f9f9eee7673e564c3cc93e6b64e2e74 Mon Sep 17 00:00:00 2001 From: James Nimmo Date: Sat, 21 Mar 2020 06:17:50 +1300 Subject: [PATCH 154/431] Add IntesisHome support for air to water heat pumps (#32250) * Add Intesis support for air to water heat pumps * Bump to pyIntesisHome 1.7.1 * Fix * Re-order HVAC modes --- .../components/intesishome/climate.py | 154 ++++++++++++------ .../components/intesishome/manifest.json | 2 +- requirements_all.txt | 2 +- 3 files changed, 109 insertions(+), 49 deletions(-) diff --git a/homeassistant/components/intesishome/climate.py b/homeassistant/components/intesishome/climate.py index 669d1155d80..a3a06a52c9c 100644 --- a/homeassistant/components/intesishome/climate.py +++ b/homeassistant/components/intesishome/climate.py @@ -14,7 +14,11 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + PRESET_BOOST, + PRESET_COMFORT, + PRESET_ECO, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, SWING_BOTH, @@ -57,9 +61,25 @@ MAP_IH_TO_HVAC_MODE = { "heat": HVAC_MODE_HEAT, "off": HVAC_MODE_OFF, } - MAP_HVAC_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_HVAC_MODE.items()} +MAP_IH_TO_PRESET_MODE = { + "eco": PRESET_ECO, + "comfort": PRESET_COMFORT, + "powerful": PRESET_BOOST, +} +MAP_PRESET_MODE_TO_IH = {v: k for k, v in MAP_IH_TO_PRESET_MODE.items()} + +IH_SWING_STOP = "auto/stop" +IH_SWING_SWING = "swing" +MAP_SWING_TO_IH = { + SWING_OFF: {"vvane": IH_SWING_STOP, "hvane": IH_SWING_STOP}, + SWING_BOTH: {"vvane": IH_SWING_SWING, "hvane": IH_SWING_SWING}, + SWING_HORIZONTAL: {"vvane": IH_SWING_STOP, "hvane": IH_SWING_SWING}, + SWING_VERTICAL: {"vvane": IH_SWING_SWING, "hvane": IH_SWING_STOP}, +} + + MAP_STATE_ICONS = { HVAC_MODE_COOL: "mdi:snowflake", HVAC_MODE_DRY: "mdi:water-off", @@ -68,15 +88,6 @@ MAP_STATE_ICONS = { HVAC_MODE_HEAT_COOL: "mdi:cached", } -IH_HVAC_MODES = [ - HVAC_MODE_HEAT_COOL, - HVAC_MODE_COOL, - HVAC_MODE_HEAT, - HVAC_MODE_DRY, - HVAC_MODE_FAN_ONLY, - HVAC_MODE_OFF, -] - async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Create the IntesisHome climate devices.""" @@ -132,31 +143,53 @@ class IntesisAC(ClimateDevice): self._setpoint_step = 1 self._current_temp = None self._max_temp = None + self._hvac_mode_list = [] self._min_temp = None self._target_temp = None self._outdoor_temp = None + self._hvac_mode = None + self._preset = None + self._preset_list = [PRESET_ECO, PRESET_COMFORT, PRESET_BOOST] self._run_hours = None self._rssi = None - self._swing = None - self._swing_list = None + self._swing_list = [SWING_OFF] self._vvane = None self._hvane = None self._power = False self._fan_speed = None - self._hvac_mode = None - self._fan_modes = controller.get_fan_speed_list(ih_device_id) - self._support = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE - self._swing_list = [SWING_OFF] + self._support = 0 + self._power_consumption_heat = None + self._power_consumption_cool = None - if ih_device.get("config_vertical_vanes"): + # Setpoint support + if controller.has_setpoint_control(ih_device_id): + self._support |= SUPPORT_TARGET_TEMPERATURE + + # Setup swing list + if controller.has_vertical_swing(ih_device_id): self._swing_list.append(SWING_VERTICAL) - if ih_device.get("config_horizontal_vanes"): + if controller.has_horizontal_swing(ih_device_id): self._swing_list.append(SWING_HORIZONTAL) - if len(self._swing_list) == 3: + if SWING_HORIZONTAL in self._swing_list and SWING_VERTICAL in self._swing_list: self._swing_list.append(SWING_BOTH) + if len(self._swing_list) > 1: self._support |= SUPPORT_SWING_MODE - elif len(self._swing_list) == 2: - self._support |= SUPPORT_SWING_MODE + + # Setup fan speeds + self._fan_modes = controller.get_fan_speed_list(ih_device_id) + if self._fan_modes: + self._support |= SUPPORT_FAN_MODE + + # Preset support + if ih_device.get("climate_working_mode"): + self._support |= SUPPORT_PRESET_MODE + + # Setup HVAC modes + modes = controller.get_mode_list(ih_device_id) + if modes: + 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) async def async_added_to_hass(self): """Subscribe to event updates.""" @@ -181,11 +214,17 @@ class IntesisAC(ClimateDevice): def device_state_attributes(self): """Return the device specific state attributes.""" attrs = {} - if len(self._swing_list) > 1: - attrs["vertical_vane"] = self._vvane - attrs["horizontal_vane"] = self._hvane if self._outdoor_temp: attrs["outdoor_temp"] = self._outdoor_temp + if self._power_consumption_heat: + attrs["power_consumption_heat_kw"] = round( + self._power_consumption_heat / 1000, 1 + ) + if self._power_consumption_cool: + attrs["power_consumption_cool_kw"] = round( + self._power_consumption_cool / 1000, 1 + ) + return attrs @property @@ -198,6 +237,16 @@ class IntesisAC(ClimateDevice): """Return whether setpoint should be whole or half degree precision.""" return self._setpoint_step + @property + def preset_modes(self): + """Return a list of HVAC preset modes.""" + return self._preset_list + + @property + def preset_mode(self): + """Return the current preset mode.""" + return self._preset + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" temperature = kwargs.get(ATTR_TEMPERATURE) @@ -248,21 +297,21 @@ class IntesisAC(ClimateDevice): self._fan_speed = fan_mode self.async_write_ha_state() + async def async_set_preset_mode(self, preset_mode): + """Set preset mode.""" + ih_preset_mode = MAP_PRESET_MODE_TO_IH.get(preset_mode) + await self._controller.set_preset_mode(self._device_id, ih_preset_mode) + async def async_set_swing_mode(self, swing_mode): """Set the vertical vane.""" - if swing_mode == SWING_OFF: - await self._controller.set_vertical_vane(self._device_id, "auto/stop") - await self._controller.set_horizontal_vane(self._device_id, "auto/stop") - elif swing_mode == SWING_BOTH: - await self._controller.set_vertical_vane(self._device_id, "swing") - await self._controller.set_horizontal_vane(self._device_id, "swing") - elif swing_mode == SWING_HORIZONTAL: - await self._controller.set_vertical_vane(self._device_id, "auto/stop") - await self._controller.set_horizontal_vane(self._device_id, "swing") - elif swing_mode == SWING_VERTICAL: - await self._controller.set_vertical_vane(self._device_id, "swing") - await self._controller.set_horizontal_vane(self._device_id, "auto/stop") - self._swing = swing_mode + swing_settings = MAP_SWING_TO_IH.get(swing_mode) + if swing_settings: + await self._controller.set_vertical_vane( + self._device_id, swing_settings.get("vvane") + ) + await self._controller.set_horizontal_vane( + self._device_id, swing_settings.get("hvane") + ) async def async_update(self): """Copy values from controller dictionary to climate device.""" @@ -282,19 +331,22 @@ class IntesisAC(ClimateDevice): mode = self._controller.get_mode(self._device_id) self._hvac_mode = MAP_IH_TO_HVAC_MODE.get(mode) + # Preset mode + preset = self._controller.get_preset_mode(self._device_id) + self._preset = MAP_IH_TO_PRESET_MODE.get(preset) + # Swing mode # Climate module only supports one swing setting. self._vvane = self._controller.get_vertical_swing(self._device_id) self._hvane = self._controller.get_horizontal_swing(self._device_id) - if self._vvane == "swing" and self._hvane == "swing": - self._swing = SWING_BOTH - elif self._vvane == "swing": - self._swing = SWING_VERTICAL - elif self._hvane == "swing": - self._swing = SWING_HORIZONTAL - else: - self._swing = SWING_OFF + # Power usage + self._power_consumption_heat = self._controller.get_heat_power_consumption( + self._device_id + ) + self._power_consumption_cool = self._controller.get_cool_power_consumption( + self._device_id + ) async def async_will_remove_from_hass(self): """Shutdown the controller when the device is being removed.""" @@ -357,7 +409,7 @@ class IntesisAC(ClimateDevice): @property def hvac_modes(self): """List of available operation modes.""" - return IH_HVAC_MODES + return self._hvac_mode_list @property def fan_mode(self): @@ -367,7 +419,15 @@ class IntesisAC(ClimateDevice): @property def swing_mode(self): """Return current swing mode.""" - return self._swing + if self._vvane == IH_SWING_SWING and self._hvane == IH_SWING_SWING: + swing = SWING_BOTH + elif self._vvane == IH_SWING_SWING: + swing = SWING_VERTICAL + elif self._hvane == IH_SWING_SWING: + swing = SWING_HORIZONTAL + else: + swing = SWING_OFF + return swing @property def fan_modes(self): diff --git a/homeassistant/components/intesishome/manifest.json b/homeassistant/components/intesishome/manifest.json index f0caf88808a..f1647f5d97e 100644 --- a/homeassistant/components/intesishome/manifest.json +++ b/homeassistant/components/intesishome/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/intesishome", "dependencies": [], "codeowners": ["@jnimmo"], - "requirements": ["pyintesishome==1.6"] + "requirements": ["pyintesishome==1.7.1"] } diff --git a/requirements_all.txt b/requirements_all.txt index 17cc834e0b9..1a27660a482 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1322,7 +1322,7 @@ pyialarm==0.3 pyicloud==0.9.5 # homeassistant.components.intesishome -pyintesishome==1.6 +pyintesishome==1.7.1 # homeassistant.components.ipma pyipma==2.0.5 From 92d373055f9e977e8f94330b3d6461dc45f59f63 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2020 13:01:51 -0500 Subject: [PATCH 155/431] Modernize nuheat for new climate platform (#32714) * Modernize nuheat for new climate platform * Home Assistant state now mirrors the state displayed at mynewheat.com * Remove off mode as the device does not implement and setting was not implemented anyways * Implement missing set_hvac_mode for nuheat * Now shows as unavailable when offline * Add a unique id (serial number) * Fix hvac_mode as it was really implementing hvac_action * Presets now map to the open api spec published at https://api.mynuheat.com/swagger/ * ThermostatModel: scheduleMode * Empty commit to re-run ci * Revert test cleanup as it leaves files behind. Its going to be more invasive to modernize the tests so it will have to come in a new pr --- homeassistant/components/nuheat/climate.py | 106 +++++++++++++-------- tests/components/nuheat/test_climate.py | 10 +- 2 files changed, 72 insertions(+), 44 deletions(-) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index 5cf9bd6fc58..c13e2ab257f 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -2,14 +2,15 @@ from datetime import timedelta import logging +from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, HVAC_MODE_HEAT, - HVAC_MODE_OFF, - PRESET_NONE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -28,16 +29,25 @@ _LOGGER = logging.getLogger(__name__) MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -# Hold modes -MODE_AUTO = HVAC_MODE_AUTO # Run device schedule -MODE_HOLD_TEMPERATURE = "temperature" -MODE_TEMPORARY_HOLD = "temporary_temperature" +# The device does not have an off function. +# To turn it off set to min_temp and PRESET_PERMANENT_HOLD +OPERATION_LIST = [HVAC_MODE_AUTO, HVAC_MODE_HEAT] -OPERATION_LIST = [HVAC_MODE_HEAT, HVAC_MODE_OFF] +PRESET_RUN = "Run Schedule" +PRESET_TEMPORARY_HOLD = "Temporary Hold" +PRESET_PERMANENT_HOLD = "Permanent Hold" -SCHEDULE_HOLD = 3 -SCHEDULE_RUN = 1 -SCHEDULE_TEMPORARY_HOLD = 2 +PRESET_MODES = [PRESET_RUN, PRESET_TEMPORARY_HOLD, PRESET_PERMANENT_HOLD] + +PRESET_MODE_TO_SCHEDULE_MODE_MAP = { + PRESET_RUN: SCHEDULE_RUN, + PRESET_TEMPORARY_HOLD: SCHEDULE_TEMPORARY_HOLD, + PRESET_PERMANENT_HOLD: SCHEDULE_HOLD, +} + +SCHEDULE_MODE_TO_PRESET_MODE_MAP = { + value: key for key, value in PRESET_MODE_TO_SCHEDULE_MODE_MAP.items() +} SERVICE_RESUME_PROGRAM = "resume_program" @@ -118,12 +128,36 @@ class NuHeatThermostat(ClimateDevice): return self._thermostat.fahrenheit @property - def hvac_mode(self): - """Return current operation. ie. heat, idle.""" - if self._thermostat.heating: - return HVAC_MODE_HEAT + def unique_id(self): + """Return the unique id.""" + return self._thermostat.serial_number - return HVAC_MODE_OFF + @property + def available(self): + """Return the unique id.""" + return self._thermostat.online + + def set_hvac_mode(self, hvac_mode): + """Set the system mode.""" + + if hvac_mode == HVAC_MODE_AUTO: + self._thermostat.schedule_mode = SCHEDULE_RUN + elif hvac_mode == HVAC_MODE_HEAT: + self._thermostat.schedule_mode = SCHEDULE_HOLD + + self._schedule_update() + + @property + def hvac_mode(self): + """Return current setting heat or auto.""" + if self._thermostat.schedule_mode in (SCHEDULE_TEMPORARY_HOLD, SCHEDULE_HOLD): + return HVAC_MODE_HEAT + return HVAC_MODE_AUTO + + @property + def hvac_action(self): + """Return current operation heat or idle.""" + return CURRENT_HVAC_HEAT if self._thermostat.heating else CURRENT_HVAC_IDLE @property def min_temp(self): @@ -153,21 +187,12 @@ class NuHeatThermostat(ClimateDevice): def preset_mode(self): """Return current preset mode.""" schedule_mode = self._thermostat.schedule_mode - if schedule_mode == SCHEDULE_RUN: - return MODE_AUTO - - if schedule_mode == SCHEDULE_HOLD: - return MODE_HOLD_TEMPERATURE - - if schedule_mode == SCHEDULE_TEMPORARY_HOLD: - return MODE_TEMPORARY_HOLD - - return MODE_AUTO + return SCHEDULE_MODE_TO_PRESET_MODE_MAP.get(schedule_mode, PRESET_RUN) @property def preset_modes(self): """Return available preset modes.""" - return [PRESET_NONE, MODE_HOLD_TEMPERATURE, MODE_TEMPORARY_HOLD] + return PRESET_MODES @property def hvac_modes(self): @@ -177,37 +202,42 @@ class NuHeatThermostat(ClimateDevice): def resume_program(self): """Resume the thermostat's programmed schedule.""" self._thermostat.resume_schedule() - self._force_update = True + self._schedule_update() def set_preset_mode(self, preset_mode): """Update the hold mode of the thermostat.""" - if preset_mode == PRESET_NONE: - schedule_mode = SCHEDULE_RUN - elif preset_mode == MODE_HOLD_TEMPERATURE: - schedule_mode = SCHEDULE_HOLD - - elif preset_mode == MODE_TEMPORARY_HOLD: - schedule_mode = SCHEDULE_TEMPORARY_HOLD - - self._thermostat.schedule_mode = schedule_mode - self._force_update = True + self._thermostat.schedule_mode = PRESET_MODE_TO_SCHEDULE_MODE_MAP.get( + preset_mode, SCHEDULE_RUN + ) + self._schedule_update() def set_temperature(self, **kwargs): """Set a new target temperature.""" - temperature = kwargs.get(ATTR_TEMPERATURE) + self._set_temperature(kwargs.get(ATTR_TEMPERATURE)) + + def _set_temperature(self, temperature): if self._temperature_unit == "C": self._thermostat.target_celsius = temperature else: self._thermostat.target_fahrenheit = temperature + # If they set a temperature without changing the mode + # to heat, we behave like the device does locally + # and set a temp hold. + if self._thermostat.schedule_mode == SCHEDULE_RUN: + self._thermostat.schedule_mode = SCHEDULE_TEMPORARY_HOLD _LOGGER.debug( "Setting NuHeat thermostat temperature to %s %s", temperature, self.temperature_unit, ) + self._schedule_update() + def _schedule_update(self): self._force_update = True + if self.hass: + self.schedule_update_ha_state(True) def update(self): """Get the latest state from the thermostat.""" diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index c35497968ac..af16e3ffc26 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -3,8 +3,8 @@ import unittest from unittest.mock import Mock, patch from homeassistant.components.climate.const import ( + HVAC_MODE_AUTO, HVAC_MODE_HEAT, - HVAC_MODE_OFF, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -130,10 +130,8 @@ class TestNuHeat(unittest.TestCase): assert self.thermostat.current_temperature == 22 def test_current_operation(self): - """Test current operation.""" - assert self.thermostat.hvac_mode == HVAC_MODE_HEAT - self.thermostat._thermostat.heating = False - assert self.thermostat.hvac_mode == HVAC_MODE_OFF + """Test requested mode.""" + assert self.thermostat.hvac_mode == HVAC_MODE_AUTO def test_min_temp(self): """Test min temp.""" @@ -155,7 +153,7 @@ class TestNuHeat(unittest.TestCase): def test_operation_list(self): """Test the operation list.""" - assert self.thermostat.hvac_modes == [HVAC_MODE_HEAT, HVAC_MODE_OFF] + assert self.thermostat.hvac_modes == [HVAC_MODE_AUTO, HVAC_MODE_HEAT] def test_resume_program(self): """Test resume schedule.""" From acf41d03db6293b9897b8194f5530793de89952f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 20 Mar 2020 20:21:20 +0100 Subject: [PATCH 156/431] Remove alarmdotcom integration (ADR-0004) (#33056) --- .coveragerc | 1 - .../components/alarmdotcom/__init__.py | 1 - .../alarmdotcom/alarm_control_panel.py | 132 ------------------ .../components/alarmdotcom/manifest.json | 8 -- requirements_all.txt | 3 - 5 files changed, 145 deletions(-) delete mode 100644 homeassistant/components/alarmdotcom/__init__.py delete mode 100644 homeassistant/components/alarmdotcom/alarm_control_panel.py delete mode 100644 homeassistant/components/alarmdotcom/manifest.json diff --git a/.coveragerc b/.coveragerc index a1ad48e1d22..548e4712404 100644 --- a/.coveragerc +++ b/.coveragerc @@ -24,7 +24,6 @@ omit = homeassistant/components/airvisual/sensor.py homeassistant/components/aladdin_connect/cover.py homeassistant/components/alarmdecoder/* - homeassistant/components/alarmdotcom/alarm_control_panel.py homeassistant/components/alpha_vantage/sensor.py homeassistant/components/amazon_polly/tts.py homeassistant/components/ambiclimate/climate.py diff --git a/homeassistant/components/alarmdotcom/__init__.py b/homeassistant/components/alarmdotcom/__init__.py deleted file mode 100644 index 0a715230e9f..00000000000 --- a/homeassistant/components/alarmdotcom/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""The alarmdotcom component.""" diff --git a/homeassistant/components/alarmdotcom/alarm_control_panel.py b/homeassistant/components/alarmdotcom/alarm_control_panel.py deleted file mode 100644 index e5ff550df9a..00000000000 --- a/homeassistant/components/alarmdotcom/alarm_control_panel.py +++ /dev/null @@ -1,132 +0,0 @@ -"""Interfaces with Alarm.com alarm control panels.""" -import logging -import re - -from pyalarmdotcom import Alarmdotcom -import voluptuous as vol - -import homeassistant.components.alarm_control_panel as alarm -from homeassistant.components.alarm_control_panel import PLATFORM_SCHEMA -from homeassistant.components.alarm_control_panel.const import ( - SUPPORT_ALARM_ARM_AWAY, - SUPPORT_ALARM_ARM_HOME, -) -from homeassistant.const import ( - CONF_CODE, - CONF_NAME, - CONF_PASSWORD, - CONF_USERNAME, - STATE_ALARM_ARMED_AWAY, - STATE_ALARM_ARMED_HOME, - STATE_ALARM_DISARMED, -) -from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv - -_LOGGER = logging.getLogger(__name__) - -DEFAULT_NAME = "Alarm.com" - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE): cv.positive_int, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - } -) - - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): - """Set up a Alarm.com control panel.""" - name = config.get(CONF_NAME) - code = config.get(CONF_CODE) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - alarmdotcom = AlarmDotCom(hass, name, code, username, password) - await alarmdotcom.async_login() - async_add_entities([alarmdotcom]) - - -class AlarmDotCom(alarm.AlarmControlPanel): - """Representation of an Alarm.com status.""" - - def __init__(self, hass, name, code, username, password): - """Initialize the Alarm.com status.""" - - _LOGGER.debug("Setting up Alarm.com...") - self._hass = hass - self._name = name - self._code = str(code) if code else None - self._username = username - self._password = password - self._websession = async_get_clientsession(self._hass) - self._state = None - self._alarm = Alarmdotcom(username, password, self._websession, hass.loop) - - async def async_login(self): - """Login to Alarm.com.""" - await self._alarm.async_login() - - async def async_update(self): - """Fetch the latest state.""" - await self._alarm.async_update() - return self._alarm.state - - @property - def name(self): - """Return the name of the alarm.""" - return self._name - - @property - def code_format(self): - """Return one or more digits/characters.""" - if self._code is None: - return None - if isinstance(self._code, str) and re.search("^\\d+$", self._code): - return alarm.FORMAT_NUMBER - return alarm.FORMAT_TEXT - - @property - def state(self): - """Return the state of the device.""" - if self._alarm.state.lower() == "disarmed": - return STATE_ALARM_DISARMED - if self._alarm.state.lower() == "armed stay": - return STATE_ALARM_ARMED_HOME - if self._alarm.state.lower() == "armed away": - return STATE_ALARM_ARMED_AWAY - return None - - @property - def supported_features(self) -> int: - """Return the list of supported features.""" - return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY - - @property - def device_state_attributes(self): - """Return the state attributes.""" - return {"sensor_status": self._alarm.sensor_status} - - async def async_alarm_disarm(self, code=None): - """Send disarm command.""" - if self._validate_code(code): - await self._alarm.async_alarm_disarm() - - async def async_alarm_arm_home(self, code=None): - """Send arm home command.""" - if self._validate_code(code): - await self._alarm.async_alarm_arm_home() - - async def async_alarm_arm_away(self, code=None): - """Send arm away command.""" - if self._validate_code(code): - await self._alarm.async_alarm_arm_away() - - def _validate_code(self, code): - """Validate given code.""" - check = self._code is None or code == self._code - if not check: - _LOGGER.warning("Wrong code entered") - return check diff --git a/homeassistant/components/alarmdotcom/manifest.json b/homeassistant/components/alarmdotcom/manifest.json deleted file mode 100644 index 9468649171a..00000000000 --- a/homeassistant/components/alarmdotcom/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "alarmdotcom", - "name": "Alarm.com", - "documentation": "https://www.home-assistant.io/integrations/alarmdotcom", - "requirements": ["pyalarmdotcom==0.3.2"], - "dependencies": [], - "codeowners": [] -} diff --git a/requirements_all.txt b/requirements_all.txt index 1a27660a482..316f2bf3f50 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1152,9 +1152,6 @@ pyaftership==0.1.2 # homeassistant.components.airvisual pyairvisual==3.0.1 -# homeassistant.components.alarmdotcom -pyalarmdotcom==0.3.2 - # homeassistant.components.almond pyalmond==0.0.2 From 37687561c03d385fc535d48210a153b83e398cd0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2020 15:28:14 -0500 Subject: [PATCH 157/431] Add config flow for myq (#32890) * Add a config flow for myq * Discovered by homekit * Fix gates being treated as garage doors * Offline devices now show as unavailable * Homekit flow * strip out icon * return -> raise --- CODEOWNERS | 1 + .../components/myq/.translations/en.json | 22 ++++ homeassistant/components/myq/__init__.py | 65 ++++++++++- homeassistant/components/myq/config_flow.py | 84 ++++++++++++++ homeassistant/components/myq/const.py | 18 +++ homeassistant/components/myq/cover.py | 62 +++++++---- homeassistant/components/myq/manifest.json | 12 +- homeassistant/components/myq/strings.json | 22 ++++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 1 + requirements_test_all.txt | 3 + tests/components/myq/__init__.py | 1 + tests/components/myq/test_config_flow.py | 103 ++++++++++++++++++ 13 files changed, 373 insertions(+), 22 deletions(-) create mode 100644 homeassistant/components/myq/.translations/en.json create mode 100644 homeassistant/components/myq/config_flow.py create mode 100644 homeassistant/components/myq/const.py create mode 100644 homeassistant/components/myq/strings.json create mode 100644 tests/components/myq/__init__.py create mode 100644 tests/components/myq/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 4d9ec3a2f0f..9adf5110b4f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -233,6 +233,7 @@ homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core homeassistant/components/msteams/* @peroyvind +homeassistant/components/myq/* @bdraco homeassistant/components/mysensors/* @MartinHjelmare homeassistant/components/mystrom/* @fabaff homeassistant/components/neato/* @dshokouhi @Santobert diff --git a/homeassistant/components/myq/.translations/en.json b/homeassistant/components/myq/.translations/en.json new file mode 100644 index 00000000000..c31162b2894 --- /dev/null +++ b/homeassistant/components/myq/.translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "MyQ", + "step": { + "user": { + "title": "Connect to the MyQ Gateway", + "data": { + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "MyQ is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index e9fa7900d90..51ad9fb48f0 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1 +1,64 @@ -"""The myq component.""" +"""The MyQ integration.""" +import asyncio +import logging + +import pymyq +from pymyq.errors import InvalidCredentialsError, MyQError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN, PLATFORMS + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the MyQ component.""" + + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up MyQ from a config entry.""" + + websession = aiohttp_client.async_get_clientsession(hass) + conf = entry.data + + try: + myq = await pymyq.login(conf[CONF_USERNAME], conf[CONF_PASSWORD], websession) + except InvalidCredentialsError as err: + _LOGGER.error("There was an error while logging in: %s", err) + return False + except MyQError: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = myq + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py new file mode 100644 index 00000000000..baa7aad4cff --- /dev/null +++ b/homeassistant/components/myq/config_flow.py @@ -0,0 +1,84 @@ +"""Config flow for MyQ integration.""" +import logging + +import pymyq +from pymyq.errors import InvalidCredentialsError, MyQError +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import aiohttp_client + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + {vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str} +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + websession = aiohttp_client.async_get_clientsession(hass) + + try: + await pymyq.login(data[CONF_USERNAME], data[CONF_PASSWORD], websession) + except InvalidCredentialsError: + raise InvalidAuth + except MyQError: + raise CannotConnect + + return {"title": data[CONF_USERNAME]} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for MyQ.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + 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 + ) + + async def async_step_homekit(self, homekit_info): + """Handle HomeKit discovery.""" + return await self.async_step_user() + + async def async_step_import(self, user_input): + """Handle import.""" + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py new file mode 100644 index 00000000000..260811e54ce --- /dev/null +++ b/homeassistant/components/myq/const.py @@ -0,0 +1,18 @@ +"""The MyQ integration.""" +from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING + +DOMAIN = "myq" + +PLATFORMS = ["cover"] + +MYQ_DEVICE_TYPE = "device_type" +MYQ_DEVICE_TYPE_GATE = "gate" +MYQ_DEVICE_STATE = "state" +MYQ_DEVICE_STATE_ONLINE = "online" + +MYQ_TO_HASS = { + "closed": STATE_CLOSED, + "closing": STATE_CLOSING, + "open": STATE_OPEN, + "opening": STATE_OPENING, +} diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 3f0895d9931..0df61b4d5db 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,8 +1,6 @@ """Support for MyQ-Enabled Garage Doors.""" import logging -from pymyq import login -from pymyq.errors import MyQError import voluptuous as vol from homeassistant.components.cover import ( @@ -11,25 +9,21 @@ from homeassistant.components.cover import ( SUPPORT_OPEN, CoverDevice, ) +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_PASSWORD, CONF_TYPE, CONF_USERNAME, STATE_CLOSED, STATE_CLOSING, - STATE_OPEN, STATE_OPENING, ) -from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers import config_validation as cv + +from .const import DOMAIN, MYQ_DEVICE_STATE, MYQ_DEVICE_STATE_ONLINE, MYQ_TO_HASS _LOGGER = logging.getLogger(__name__) -MYQ_TO_HASS = { - "closed": STATE_CLOSED, - "closing": STATE_CLOSING, - "open": STATE_OPEN, - "opening": STATE_OPENING, -} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -38,23 +32,28 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( # This parameter is no longer used; keeping it to avoid a breaking change in # a hotfix, but in a future main release, this should be removed: vol.Optional(CONF_TYPE): cv.string, - } + }, ) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the platform.""" - websession = aiohttp_client.async_get_clientsession(hass) - username = config[CONF_USERNAME] - password = config[CONF_PASSWORD] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_USERNAME: config[CONF_USERNAME], + CONF_PASSWORD: config[CONF_PASSWORD], + }, + ) + ) - try: - myq = await login(username, password, websession) - except MyQError as err: - _LOGGER.error("There was an error while logging in: %s", err) - return +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up mysq covers.""" + myq = hass.data[DOMAIN][config_entry.entry_id] async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) @@ -75,6 +74,14 @@ class MyQDevice(CoverDevice): """Return the name of the garage door if any.""" return self._device.name + @property + def available(self): + """Return if the device is online.""" + # Not all devices report online so assume True if its missing + return self._device.device_json[MYQ_DEVICE_STATE].get( + MYQ_DEVICE_STATE_ONLINE, True + ) + @property def is_closed(self): """Return true if cover is closed, else False.""" @@ -103,11 +110,28 @@ class MyQDevice(CoverDevice): async def async_close_cover(self, **kwargs): """Issue close command to cover.""" await self._device.close() + # Writes closing state + self.async_write_ha_state() async def async_open_cover(self, **kwargs): """Issue open command to cover.""" await self._device.open() + # Writes opening state + self.async_write_ha_state() async def async_update(self): """Update status of cover.""" await self._device.update() + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self._device.name, + "manufacturer": "The Chamberlain Group Inc.", + "sw_version": self._device.firmware_version, + } + if self._device.parent_device_id: + device_info["via_device"] = (DOMAIN, self._device.parent_device_id) + return device_info diff --git a/homeassistant/components/myq/manifest.json b/homeassistant/components/myq/manifest.json index 7e00e025bd3..afee7d4d77f 100644 --- a/homeassistant/components/myq/manifest.json +++ b/homeassistant/components/myq/manifest.json @@ -2,7 +2,15 @@ "domain": "myq", "name": "MyQ", "documentation": "https://www.home-assistant.io/integrations/myq", - "requirements": ["pymyq==2.0.1"], + "requirements": [ + "pymyq==2.0.1" + ], "dependencies": [], - "codeowners": [] + "codeowners": ["@bdraco"], + "config_flow": true, + "homekit": { + "models": [ + "819LMB" + ] + } } diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json new file mode 100644 index 00000000000..c31162b2894 --- /dev/null +++ b/homeassistant/components/myq/strings.json @@ -0,0 +1,22 @@ +{ + "config": { + "title": "MyQ", + "step": { + "user": { + "title": "Connect to the MyQ Gateway", + "data": { + "username": "Username", + "password": "Password" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "MyQ is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index c981a88984e..0d59a67c665 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -70,6 +70,7 @@ FLOWS = [ "minecraft_server", "mobile_app", "mqtt", + "myq", "neato", "nest", "netatmo", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 1cf88a5c7ae..1a9972e9a6e 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -39,6 +39,7 @@ ZEROCONF = { } HOMEKIT = { + "819LMB": "myq", "BSB002": "hue", "LIFX": "lifx", "Netatmo Relay": "netatmo", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 33d81c03d5b..d1a159c2a3f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -535,6 +535,9 @@ pymodbus==1.5.2 # homeassistant.components.monoprice pymonoprice==0.3 +# homeassistant.components.myq +pymyq==2.0.1 + # homeassistant.components.nws pynws==0.10.4 diff --git a/tests/components/myq/__init__.py b/tests/components/myq/__init__.py new file mode 100644 index 00000000000..63dd25a4d0b --- /dev/null +++ b/tests/components/myq/__init__.py @@ -0,0 +1 @@ +"""Tests for the MyQ integration.""" diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py new file mode 100644 index 00000000000..c0bae8c5225 --- /dev/null +++ b/tests/components/myq/test_config_flow.py @@ -0,0 +1,103 @@ +"""Test the MyQ config flow.""" +from asynctest import patch +from pymyq.errors import InvalidCredentialsError, MyQError + +from homeassistant import config_entries, setup +from homeassistant.components.myq.const import DOMAIN + + +async def test_form_user(hass): + """Test we get the user form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", return_value=True, + ), patch( + "homeassistant.components.myq.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.myq.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "test-username" + assert result2["data"] == { + "username": "test-username", + "password": "test-password", + } + 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(hass): + """Test we can import.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", return_value=True, + ), patch( + "homeassistant.components.myq.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.myq.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={"username": "test-username", "password": "test-password"}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "test-username" + assert result["data"] == { + "username": "test-username", + "password": "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=InvalidCredentialsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", side_effect=MyQError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From c2ac8e813a6f159f1b5b3536bf596d4859018b47 Mon Sep 17 00:00:00 2001 From: Knapoc Date: Fri, 20 Mar 2020 21:34:17 +0100 Subject: [PATCH 158/431] Bump aioasuswrt to 1.2.3 and fix asuswrt sensor (#33064) * Bump aioasuswrt to 1.2.3 * Fix asuswrt connection setup parameters * fix typo --- homeassistant/components/asuswrt/__init__.py | 4 ++-- homeassistant/components/asuswrt/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index f2d7a72e54d..a0afbed69f1 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -73,8 +73,8 @@ async def async_setup(hass, config): conf.get("ssh_key", conf.get("pub_key", "")), conf[CONF_MODE], conf[CONF_REQUIRE_IP], - conf[CONF_INTERFACE], - conf[CONF_DNSMASQ], + interface=conf[CONF_INTERFACE], + dnsmasq=conf[CONF_DNSMASQ], ) await api.connection.async_connect() diff --git a/homeassistant/components/asuswrt/manifest.json b/homeassistant/components/asuswrt/manifest.json index c161dc4f536..2e032dedfe7 100644 --- a/homeassistant/components/asuswrt/manifest.json +++ b/homeassistant/components/asuswrt/manifest.json @@ -2,7 +2,7 @@ "domain": "asuswrt", "name": "ASUSWRT", "documentation": "https://www.home-assistant.io/integrations/asuswrt", - "requirements": ["aioasuswrt==1.2.2"], + "requirements": ["aioasuswrt==1.2.3"], "dependencies": [], "codeowners": ["@kennedyshead"] } diff --git a/requirements_all.txt b/requirements_all.txt index 316f2bf3f50..c49fe3f3a77 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -139,7 +139,7 @@ aio_georss_gdacs==0.3 aioambient==1.0.4 # homeassistant.components.asuswrt -aioasuswrt==1.2.2 +aioasuswrt==1.2.3 # homeassistant.components.automatic aioautomatic==0.6.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d1a159c2a3f..1f4946f115b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -50,7 +50,7 @@ aio_georss_gdacs==0.3 aioambient==1.0.4 # homeassistant.components.asuswrt -aioasuswrt==1.2.2 +aioasuswrt==1.2.3 # homeassistant.components.automatic aioautomatic==0.6.5 From d16d44d3e76b63812fa1efd5bb1e4bd325cc32ee Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Mar 2020 13:34:56 -0700 Subject: [PATCH 159/431] Add negative tests for identify schema for packages (#33050) --- homeassistant/config.py | 19 +++++++++---------- tests/test_config.py | 11 +++++++---- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index b1cd49b0852..bd956846886 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -562,12 +562,12 @@ def _log_pkg_error(package: str, component: str, config: Dict, message: str) -> _LOGGER.error(message) -def _identify_config_schema(module: ModuleType) -> Tuple[Optional[str], Optional[Dict]]: +def _identify_config_schema(module: ModuleType) -> Optional[str]: """Extract the schema and identify list or dict based.""" try: key = next(k for k in module.CONFIG_SCHEMA.schema if k == module.DOMAIN) # type: ignore except (AttributeError, StopIteration): - return None, None + return None schema = module.CONFIG_SCHEMA.schema[key] # type: ignore @@ -577,19 +577,19 @@ def _identify_config_schema(module: ModuleType) -> Tuple[Optional[str], Optional default_value = schema(key.default()) if isinstance(default_value, dict): - return "dict", schema + return "dict" if isinstance(default_value, list): - return "list", schema + return "list" - return None, None + return None t_schema = str(schema) if t_schema.startswith("{") or "schema_with_slug_keys" in t_schema: - return ("dict", schema) + return "dict" if t_schema.startswith(("[", "All( Union[bool, str]: @@ -642,8 +642,7 @@ async def merge_packages_config( merge_list = hasattr(component, "PLATFORM_SCHEMA") if not merge_list and hasattr(component, "CONFIG_SCHEMA"): - merge_type, _ = _identify_config_schema(component) - merge_list = merge_type == "list" + merge_list = _identify_config_schema(component) == "list" if merge_list: config[comp_name] = cv.remove_falsy( diff --git a/tests/test_config.py b/tests/test_config.py index 43f1263e581..ba0153e7a7d 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -722,7 +722,7 @@ async def test_merge_id_schema(hass): for domain, expected_type in types.items(): integration = await async_get_integration(hass, domain) module = integration.get_component() - typ, _ = config_util._identify_config_schema(module) + typ = config_util._identify_config_schema(module) assert typ == expected_type, f"{domain} expected {expected_type}, got {typ}" @@ -997,13 +997,16 @@ async def test_component_config_exceptions(hass, caplog): [ ("zone", vol.Schema({vol.Optional("zone", default=[]): list}), "list"), ("zone", vol.Schema({vol.Optional("zone", default=dict): dict}), "dict"), + ("zone", vol.Schema({vol.Optional("zone"): int}), None), + ("zone", vol.Schema({"zone": int}), None), + ("not_existing", vol.Schema({vol.Optional("zone", default=dict): dict}), None,), + ("non_existing", vol.Schema({"zone": int}), None), + ("zone", vol.Schema({}), None), ], ) def test_identify_config_schema(domain, schema, expected): """Test identify config schema.""" assert ( - config_util._identify_config_schema(Mock(DOMAIN=domain, CONFIG_SCHEMA=schema))[ - 0 - ] + config_util._identify_config_schema(Mock(DOMAIN=domain, CONFIG_SCHEMA=schema)) == expected ) From 836413a4a83da1f7ec87fb006e963b660ebeb9ea Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Fri, 20 Mar 2020 14:59:01 -0600 Subject: [PATCH 160/431] Bump simplisafe-python to 9.0.4 (#33059) --- 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 0fdcdcfcf5e..1d010c67692 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==9.0.3"], + "requirements": ["simplisafe-python==9.0.4"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index c49fe3f3a77..2525ccbb460 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1856,7 +1856,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.0.3 +simplisafe-python==9.0.4 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1f4946f115b..d7be16c87b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -667,7 +667,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.0.3 +simplisafe-python==9.0.4 # homeassistant.components.sleepiq sleepyq==0.7 From 85328399e0a61249b0f38107b777587e5e32cf9c Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 20 Mar 2020 18:49:42 -0500 Subject: [PATCH 161/431] Add support for nexia automations (#33049) * Add support for nexia automations Bump nexia to 0.7.1 Start adding tests Fix some of the climate attributes that were wrong (discovered while adding tests) Pass the name of the instance so the nexia UI does not display "My Mobile" * fix mocking * faster asserts, scene * scene makes so much more sense * pylint * Update homeassistant/components/nexia/scene.py Co-Authored-By: Martin Hjelmare * docstring cleanup Co-authored-by: Martin Hjelmare --- homeassistant/components/nexia/__init__.py | 7 +- homeassistant/components/nexia/climate.py | 50 +- homeassistant/components/nexia/config_flow.py | 1 + homeassistant/components/nexia/const.py | 4 +- homeassistant/components/nexia/manifest.json | 2 +- homeassistant/components/nexia/scene.py | 68 + requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nexia/test_climate.py | 45 + tests/components/nexia/test_scene.py | 72 + tests/components/nexia/util.py | 45 + .../fixtures/nexia/mobile_houses_123456.json | 8036 +++++++++++++++++ tests/fixtures/nexia/session_123456.json | 25 + tests/fixtures/nexia/sign_in.json | 10 + 14 files changed, 8362 insertions(+), 7 deletions(-) create mode 100644 homeassistant/components/nexia/scene.py create mode 100644 tests/components/nexia/test_climate.py create mode 100644 tests/components/nexia/test_scene.py create mode 100644 tests/components/nexia/util.py create mode 100644 tests/fixtures/nexia/mobile_houses_123456.json create mode 100644 tests/fixtures/nexia/session_123456.json create mode 100644 tests/fixtures/nexia/sign_in.json diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 40ea7b6dcc6..41ecf6f1045 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -62,7 +62,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: nexia_home = await hass.async_add_executor_job( - partial(NexiaHome, username=username, password=password) + partial( + NexiaHome, + username=username, + password=password, + device_name=hass.config.location_name, + ) ) except ConnectTimeout as ex: _LOGGER.error("Unable to connect to Nexia service: %s", ex) diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index a1f6bb155f9..7231f2b8ba9 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -10,6 +10,7 @@ from nexia.const import ( SYSTEM_STATUS_COOL, SYSTEM_STATUS_HEAT, SYSTEM_STATUS_IDLE, + UNIT_FAHRENHEIT, ) from homeassistant.components.climate import ClimateDevice @@ -32,6 +33,7 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE, + SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( ATTR_ATTRIBUTION, @@ -119,7 +121,12 @@ class NexiaZone(NexiaEntity, ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - supported = SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE + supported = ( + SUPPORT_TARGET_TEMPERATURE_RANGE + | SUPPORT_TARGET_TEMPERATURE + | SUPPORT_FAN_MODE + | SUPPORT_PRESET_MODE + ) if self._has_humidify_support or self._has_dehumidify_support: supported |= SUPPORT_TARGET_HUMIDITY @@ -159,6 +166,16 @@ class NexiaZone(NexiaEntity, ClimateDevice): """Return the list of available fan modes.""" return FAN_MODES + @property + def min_temp(self): + """Minimum temp for the current setting.""" + return (self._device.thermostat.get_setpoint_limits())[0] + + @property + def max_temp(self): + """Maximum temp for the current setting.""" + return (self._device.thermostat.get_setpoint_limits())[1] + def set_fan_mode(self, fan_mode): """Set new target fan mode.""" self.thermostat.set_fan_mode(fan_mode) @@ -198,8 +215,37 @@ class NexiaZone(NexiaEntity, ClimateDevice): @property def target_temperature(self): """Temperature we try to reach.""" - if self._device.get_current_mode() == "COOL": + current_mode = self._device.get_current_mode() + + if current_mode == OPERATION_MODE_COOL: return self._device.get_cooling_setpoint() + if current_mode == OPERATION_MODE_HEAT: + return self._device.get_heating_setpoint() + return None + + @property + def target_temperature_step(self): + """Step size of temperature units.""" + if self._device.thermostat.get_unit() == UNIT_FAHRENHEIT: + return 1.0 + return 0.5 + + @property + def target_temperature_high(self): + """Highest temperature we are trying to reach.""" + current_mode = self._device.get_current_mode() + + if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT): + return None + return self._device.get_cooling_setpoint() + + @property + def target_temperature_low(self): + """Lowest temperature we are trying to reach.""" + current_mode = self._device.get_current_mode() + + if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT): + return None return self._device.get_heating_setpoint() @property diff --git a/homeassistant/components/nexia/config_flow.py b/homeassistant/components/nexia/config_flow.py index a991b6056c3..5844cb8da20 100644 --- a/homeassistant/components/nexia/config_flow.py +++ b/homeassistant/components/nexia/config_flow.py @@ -26,6 +26,7 @@ async def validate_input(hass: core.HomeAssistant, data): password=data[CONF_PASSWORD], auto_login=False, auto_update=False, + device_name=hass.config.location_name, ) await hass.async_add_executor_job(nexia_home.login) except ConnectTimeout as ex: diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 7def5f156b4..384c3aad1b6 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -1,6 +1,6 @@ """Nexia constants.""" -PLATFORMS = ["sensor", "binary_sensor", "climate"] +PLATFORMS = ["sensor", "binary_sensor", "climate", "scene"] ATTRIBUTION = "Data provided by mynexia.com" @@ -14,6 +14,8 @@ NEXIA_SCAN_INTERVAL = "scan_interval" DOMAIN = "nexia" DEFAULT_ENTITY_NAMESPACE = "nexia" +ATTR_DESCRIPTION = "description" + ATTR_ZONE_STATUS = "zone_status" ATTR_HUMIDIFY_SUPPORTED = "humidify_supported" ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 02804bf0419..aec1f7c3e7b 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -2,7 +2,7 @@ "domain": "nexia", "name": "Nexia", "requirements": [ - "nexia==0.4.1" + "nexia==0.7.1" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py new file mode 100644 index 00000000000..4489a4de274 --- /dev/null +++ b/homeassistant/components/nexia/scene.py @@ -0,0 +1,68 @@ +"""Support for Nexia Automations.""" + +from homeassistant.components.scene import Scene +from homeassistant.const import ATTR_ATTRIBUTION + +from .const import ( + ATTR_DESCRIPTION, + ATTRIBUTION, + DATA_NEXIA, + DOMAIN, + NEXIA_DEVICE, + UPDATE_COORDINATOR, +) +from .entity import NexiaEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up automations for a Nexia device.""" + + nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_home = nexia_data[NEXIA_DEVICE] + coordinator = nexia_data[UPDATE_COORDINATOR] + entities = [] + + # Automation switches + for automation_id in nexia_home.get_automation_ids(): + automation = nexia_home.get_automation_by_id(automation_id) + + entities.append(NexiaAutomationScene(coordinator, automation)) + + async_add_entities(entities, True) + + +class NexiaAutomationScene(NexiaEntity, Scene): + """Provides Nexia automation support.""" + + def __init__(self, coordinator, automation): + """Initialize the automation scene.""" + super().__init__(coordinator) + self._automation = automation + + @property + def unique_id(self): + """Return the unique id of the automation scene.""" + # This is the automation unique_id + return self._automation.automation_id + + @property + def name(self): + """Return the name of the automation scene.""" + return self._automation.name + + @property + def device_state_attributes(self): + """Return the scene specific state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + ATTR_DESCRIPTION: self._automation.description, + } + + @property + def icon(self): + """Return the icon of the automation scene.""" + return "mdi:script-text-outline" + + def activate(self): + """Activate an automation scene.""" + self._automation.activate() diff --git a/requirements_all.txt b/requirements_all.txt index 2525ccbb460..ad72deb27a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -917,7 +917,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.4.1 +nexia==0.7.1 # homeassistant.components.niko_home_control niko-home-control==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d7be16c87b6..840905e85a0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -344,7 +344,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.4.1 +nexia==0.7.1 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py new file mode 100644 index 00000000000..327c611d277 --- /dev/null +++ b/tests/components/nexia/test_climate.py @@ -0,0 +1,45 @@ +"""The lock tests for the august platform.""" + +from homeassistant.components.climate.const import HVAC_MODE_HEAT_COOL + +from .util import async_init_integration + + +async def test_climate_zones(hass): + """Test creation climate zones.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.nick_office") + assert state.state == HVAC_MODE_HEAT_COOL + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "current_humidity": 52.0, + "current_temperature": 22.8, + "dehumidify_setpoint": 45.0, + "dehumidify_supported": True, + "fan_mode": "auto", + "fan_modes": ["auto", "on", "circulate"], + "friendly_name": "Nick Office", + "humidify_supported": False, + "humidity": 45.0, + "hvac_action": "cooling", + "hvac_modes": ["off", "auto", "heat_cool", "heat", "cool"], + "max_humidity": 65.0, + "max_temp": 37.2, + "min_humidity": 35.0, + "min_temp": 12.8, + "preset_mode": "None", + "preset_modes": ["None", "Home", "Away", "Sleep"], + "supported_features": 31, + "target_temp_high": 26.1, + "target_temp_low": 17.2, + "target_temp_step": 1.0, + "temperature": None, + "zone_status": "Relieving Air", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/nexia/test_scene.py b/tests/components/nexia/test_scene.py new file mode 100644 index 00000000000..e6a5e94f083 --- /dev/null +++ b/tests/components/nexia/test_scene.py @@ -0,0 +1,72 @@ +"""The lock tests for the august platform.""" + +from .util import async_init_integration + + +async def test_automation_scenees(hass): + """Test creation automation scenees.""" + + await async_init_integration(hass) + + state = hass.states.get("scene.away_short") + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to 63.0 " + "and cool to 80.0 AND Downstairs East Wing will " + "permanently hold the heat to 63.0 and cool to " + "79.0 AND Downstairs West Wing will permanently " + "hold the heat to 63.0 and cool to 79.0 AND " + "Upstairs West Wing will permanently hold the " + "heat to 63.0 and cool to 81.0 AND Upstairs West " + "Wing will change Fan Mode to Auto AND Downstairs " + "East Wing will change Fan Mode to Auto AND " + "Downstairs West Wing will change Fan Mode to " + "Auto AND Activate the mode named 'Away Short' " + "AND Master Suite will permanently hold the heat " + "to 63.0 and cool to 79.0 AND Master Suite will " + "change Fan Mode to Auto", + "friendly_name": "Away Short", + "icon": "mdi:script-text-outline", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("scene.power_outage") + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "description": "When IFTTT activates the automation Upstairs " + "West Wing will permanently hold the heat to 55.0 " + "and cool to 90.0 AND Downstairs East Wing will " + "permanently hold the heat to 55.0 and cool to " + "90.0 AND Downstairs West Wing will permanently " + "hold the heat to 55.0 and cool to 90.0 AND " + "Activate the mode named 'Power Outage'", + "friendly_name": "Power Outage", + "icon": "mdi:script-text-outline", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("scene.power_restored") + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "description": "When IFTTT activates the automation Upstairs " + "West Wing will Run Schedule AND Downstairs East " + "Wing will Run Schedule AND Downstairs West Wing " + "will Run Schedule AND Activate the mode named " + "'Home'", + "friendly_name": "Power Restored", + "icon": "mdi:script-text-outline", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py new file mode 100644 index 00000000000..cc2b11afcbe --- /dev/null +++ b/tests/components/nexia/util.py @@ -0,0 +1,45 @@ +"""Tests for the nexia integration.""" +import uuid + +from asynctest import patch +from nexia.home import NexiaHome +import requests_mock + +from homeassistant.components.nexia.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +async def async_init_integration( + hass: HomeAssistant, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the nexia integration in Home Assistant.""" + + house_fixture = "nexia/mobile_houses_123456.json" + session_fixture = "nexia/session_123456.json" + sign_in_fixture = "nexia/sign_in.json" + + with requests_mock.mock() as m, patch( + "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() + ): + m.post(NexiaHome.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) + m.get( + NexiaHome.API_MOBILE_HOUSES_URL.format(house_id=123456), + text=load_fixture(house_fixture), + ) + m.post( + NexiaHome.API_MOBILE_ACCOUNTS_SIGN_IN_URL, + text=load_fixture(sign_in_fixture), + ) + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} + ) + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/fixtures/nexia/mobile_houses_123456.json b/tests/fixtures/nexia/mobile_houses_123456.json new file mode 100644 index 00000000000..2bf3aa123b0 --- /dev/null +++ b/tests/fixtures/nexia/mobile_houses_123456.json @@ -0,0 +1,8036 @@ +{ + "success": true, + "error": null, + "result": { + "id": 123456, + "name": "Hidden", + "third_party_integrations": [], + "latitude": 12.7633, + "longitude": -12.3633, + "dealer_opt_in": true, + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456" + }, + "edit": [{ + "href": "https://www.mynexia.com/mobile/houses/123456/edit", + "method": "GET" + }], + "child": [{ + "href": "https://www.mynexia.com/mobile/houses/123456/devices", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [{ + "id": 2059661, + "name": "Downstairs East Wing", + "name_editable": true, + "features": [{ + "name": "advanced_info", + "items": [{ + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, { + "type": "label_value", + "label": "AUID", + "value": "000000" + }, { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + }] + }, { + "name": "thermostat", + "temperature": 71, + "status": "System Idle", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "group", + "members": [{ + "type": "xxl_zone", + "id": 83261002, + "name": "Living East", + "current_zone_mode": "AUTO", + "temperature": 71, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-71"] + }, + "features": [{ + "name": "thermostat", + "temperature": 71, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + } + }, { + "type": "xxl_zone", + "id": 83261005, + "name": "Kitchen", + "current_zone_mode": "AUTO", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [{ + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + } + }, { + "type": "xxl_zone", + "id": 83261008, + "name": "Down Bedroom", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [{ + "name": "thermostat", + "temperature": 72, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + } + }, { + "type": "xxl_zone", + "id": 83261011, + "name": "Tech Room", + "current_zone_mode": "AUTO", + "temperature": 78, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-78"] + }, + "features": [{ + "name": "thermostat", + "temperature": 78, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + } + }] + }, { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [{ + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, { + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" + } + } + }, { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.0 + }, { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059661?report_type=monthly" + } + } + }], + "icon": [{ + "name": "thermostat", + "modifiers": ["temperature-71"] + }, { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, { + "name": "thermostat", + "modifiers": ["temperature-78"] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059661" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=cd9a70e8-fd0d-4b58-b071-05a202fd8953" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/be6d8ede5cac02fe8be18c334b04d539c9200fa9230eef63" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [{ + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_mode" + } + } + }, { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }, { + "value": 0.7, + "label": "70%" + }, { + "value": 0.75, + "label": "75%" + }, { + "value": 0.8, + "label": "80%" + }, { + "value": 0.85, + "label": "85%" + }, { + "value": 0.9, + "label": "90%" + }, { + "value": 0.95, + "label": "95%" + }, { + "value": 1.0, + "label": "100%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_speed" + } + } + }, { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [{ + "value": 10, + "label": "10 minutes" + }, { + "value": 15, + "label": "15 minutes" + }, { + "value": 20, + "label": "20 minutes" + }, { + "value": 25, + "label": "25 minutes" + }, { + "value": 30, + "label": "30 minutes" + }, { + "value": 35, + "label": "35 minutes" + }, { + "value": 40, + "label": "40 minutes" + }, { + "value": 45, + "label": "45 minutes" + }, { + "value": 50, + "label": "50 minutes" + }, { + "value": 55, + "label": "55 minutes" + }], + "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/fan_circulation_time" + } + } + }, { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "quick", + "label": "Quick" + }, { + "value": "allergy", + "label": "Allergy" + }], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/air_cleaner_mode" + } + } + }, { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.5, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/dehumidify" + } + } + }, { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [{ + "value": "f", + "label": "F" + }, { + "value": "c", + "label": "C" + }], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059661/scale" + } + } + }], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "88", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "36", + "system_status": "System Idle", + "delta": 3, + "zones": [{ + "type": "xxl_zone", + "id": 83261002, + "name": "Living East", + "current_zone_mode": "AUTO", + "temperature": 71, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-71"] + }, + "features": [{ + "name": "thermostat", + "temperature": 71, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261002\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261002", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261002", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261002" + } + } + }, { + "type": "xxl_zone", + "id": 83261005, + "name": "Kitchen", + "current_zone_mode": "AUTO", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [{ + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261005\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261005", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261005", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261005" + } + } + }, { + "type": "xxl_zone", + "id": 83261008, + "name": "Down Bedroom", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [{ + "name": "thermostat", + "temperature": 72, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261008\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261008", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261008", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261008" + } + } + }, { + "type": "xxl_zone", + "id": 83261011, + "name": "Tech Room", + "current_zone_mode": "AUTO", + "temperature": 78, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-78"] + }, + "features": [{ + "name": "thermostat", + "temperature": 78, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261011\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261011", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261011", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261011" + } + } + }] + }, { + "id": 2059676, + "name": "Downstairs West Wing", + "name_editable": true, + "features": [{ + "name": "advanced_info", + "items": [{ + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, { + "type": "label_value", + "label": "AUID", + "value": "02853E08" + }, { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + }] + }, { + "name": "thermostat", + "temperature": 75, + "status": "System Idle", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "group", + "members": [{ + "type": "xxl_zone", + "id": 83261015, + "name": "Living West", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [{ + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + } + }, { + "type": "xxl_zone", + "id": 83261018, + "name": "David Office", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [{ + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + } + }] + }, { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [{ + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, { + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" + } + } + }, { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.0 + }, { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059676?report_type=monthly" + } + } + }], + "icon": [{ + "name": "thermostat", + "modifiers": ["temperature-75"] + }, { + "name": "thermostat", + "modifiers": ["temperature-75"] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059676" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=5aae72a6-1bd0-4d84-9bfd-673e7bc4907c" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/3412f1d96eb0c5edb5466c3c0598af60c06f8443f21e9bcb" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [{ + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_mode" + } + } + }, { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }, { + "value": 0.7, + "label": "70%" + }, { + "value": 0.75, + "label": "75%" + }, { + "value": 0.8, + "label": "80%" + }, { + "value": 0.85, + "label": "85%" + }, { + "value": 0.9, + "label": "90%" + }, { + "value": 0.95, + "label": "95%" + }, { + "value": 1.0, + "label": "100%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_speed" + } + } + }, { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [{ + "value": 10, + "label": "10 minutes" + }, { + "value": 15, + "label": "15 minutes" + }, { + "value": 20, + "label": "20 minutes" + }, { + "value": 25, + "label": "25 minutes" + }, { + "value": 30, + "label": "30 minutes" + }, { + "value": 35, + "label": "35 minutes" + }, { + "value": 40, + "label": "40 minutes" + }, { + "value": 45, + "label": "45 minutes" + }, { + "value": 50, + "label": "50 minutes" + }, { + "value": 55, + "label": "55 minutes" + }], + "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/fan_circulation_time" + } + } + }, { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "quick", + "label": "Quick" + }, { + "value": "allergy", + "label": "Allergy" + }], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/air_cleaner_mode" + } + } + }, { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.45, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/dehumidify" + } + } + }, { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [{ + "value": "f", + "label": "F" + }, { + "value": "c", + "label": "C" + }], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059676/scale" + } + } + }], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "88", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "52", + "system_status": "System Idle", + "delta": 3, + "zones": [{ + "type": "xxl_zone", + "id": 83261015, + "name": "Living West", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [{ + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261015\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261015", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261015", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261015" + } + } + }, { + "type": "xxl_zone", + "id": 83261018, + "name": "David Office", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [{ + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83261018\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83261018", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83261018", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83261018" + } + } + }] + }, { + "id": 2293892, + "name": "Master Suite", + "name_editable": true, + "features": [{ + "name": "advanced_info", + "items": [{ + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, { + "type": "label_value", + "label": "AUID", + "value": "0281B02C" + }, { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + }] + }, { + "name": "thermostat", + "temperature": 73, + "status": "Cooling", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "group", + "members": [{ + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [{ + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [{ + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [{ + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [{ + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [{ + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + }] + }, { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [{ + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, { + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_on", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.69 + }, { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2293892?report_type=monthly" + } + } + }], + "icon": [{ + "name": "thermostat", + "modifiers": ["temperature-73"] + }, { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, { + "name": "thermostat", + "modifiers": ["temperature-74"] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2293892" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e3fc90c7-2885-4f57-ae76-99e9ec81eef0" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/967361e8aed874aa5230930fd0e0bbd8b653261e982a6e0e" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [{ + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_mode" + } + } + }, { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }, { + "value": 0.7, + "label": "70%" + }, { + "value": 0.75, + "label": "75%" + }, { + "value": 0.8, + "label": "80%" + }, { + "value": 0.85, + "label": "85%" + }, { + "value": 0.9, + "label": "90%" + }, { + "value": 0.95, + "label": "95%" + }, { + "value": 1.0, + "label": "100%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_speed" + } + } + }, { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [{ + "value": 10, + "label": "10 minutes" + }, { + "value": 15, + "label": "15 minutes" + }, { + "value": 20, + "label": "20 minutes" + }, { + "value": 25, + "label": "25 minutes" + }, { + "value": 30, + "label": "30 minutes" + }, { + "value": 35, + "label": "35 minutes" + }, { + "value": 40, + "label": "40 minutes" + }, { + "value": 45, + "label": "45 minutes" + }, { + "value": 50, + "label": "50 minutes" + }, { + "value": 55, + "label": "55 minutes" + }], + "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/fan_circulation_time" + } + } + }, { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "quick", + "label": "Quick" + }, { + "value": "allergy", + "label": "Allergy" + }], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/air_cleaner_mode" + } + } + }, { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.45, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/dehumidify" + } + } + }, { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [{ + "value": "f", + "label": "F" + }, { + "value": "c", + "label": "C" + }], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2293892/scale" + } + } + }], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "87", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "52", + "system_status": "Cooling", + "delta": 3, + "zones": [{ + "type": "xxl_zone", + "id": 83394133, + "name": "Bath Closet", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [{ + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394133\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394133", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394133", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394133" + } + } + }, { + "type": "xxl_zone", + "id": 83394130, + "name": "Master", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 71 + }, + "operating_state": "Damper Open", + "heating_setpoint": 63, + "cooling_setpoint": 71, + "zone_status": "Damper Open", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [{ + "name": "thermostat", + "temperature": 74, + "status": "Damper Open", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 71, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394130\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394130", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394130", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394130" + } + } + }, { + "type": "xxl_zone", + "id": 83394136, + "name": "Nick Office", + "current_zone_mode": "AUTO", + "temperature": 73, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Relieving Air", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Relieving Air", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-73"] + }, + "features": [{ + "name": "thermostat", + "temperature": 73, + "status": "Relieving Air", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394136\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394136", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394136", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394136" + } + } + }, { + "type": "xxl_zone", + "id": 83394127, + "name": "Snooze Room", + "current_zone_mode": "AUTO", + "temperature": 72, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-72"] + }, + "features": [{ + "name": "thermostat", + "temperature": 72, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394127\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394127", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394127", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394127" + } + } + }, { + "type": "xxl_zone", + "id": 83394139, + "name": "Safe Room", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 79 + }, + "operating_state": "Damper Closed", + "heating_setpoint": 63, + "cooling_setpoint": 79, + "zone_status": "Damper Closed", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [{ + "name": "thermostat", + "temperature": 74, + "status": "Damper Closed", + "status_icon": { + "name": "cooling", + "modifiers": [] + }, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 79, + "system_status": "Cooling" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83394139\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83394139", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83394139", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83394139" + } + } + }] + }, { + "id": 2059652, + "name": "Upstairs West Wing", + "name_editable": true, + "features": [{ + "name": "advanced_info", + "items": [{ + "type": "label_value", + "label": "Model", + "value": "XL1050" + }, { + "type": "label_value", + "label": "AUID", + "value": "02853DF0" + }, { + "type": "label_value", + "label": "Firmware Build Number", + "value": "1581321824" + }, { + "type": "label_value", + "label": "Firmware Build Date", + "value": "2020-02-10 08:03:44 UTC" + }, { + "type": "label_value", + "label": "Firmware Version", + "value": "5.9.1" + }, { + "type": "label_value", + "label": "Zoning Enabled", + "value": "yes" + }] + }, { + "name": "thermostat", + "temperature": 77, + "status": "System Idle", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99 + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "group", + "members": [{ + "type": "xxl_zone", + "id": 83260991, + "name": "Hallway", + "current_zone_mode": "OFF", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 80 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 80, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "OFF", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [{ + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "OFF", + "display_value": "Off", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + } + }, { + "type": "xxl_zone", + "id": 83260994, + "name": "Mid Bedroom", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [{ + "name": "thermostat", + "temperature": 74, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + } + }, { + "type": "xxl_zone", + "id": 83260997, + "name": "West Bedroom", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [{ + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + } + }] + }, { + "name": "thermostat_fan_mode", + "label": "Fan Mode", + "options": [{ + "id": "thermostat_fan_mode", + "label": "Fan Mode", + "value": "thermostat_fan_mode", + "header": true + }, { + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "value": "auto", + "display_value": "Auto", + "status_icon": { + "name": "thermostat_fan_off", + "modifiers": [] + }, + "actions": { + "update_thermostat_fan_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" + } + } + }, { + "name": "thermostat_compressor_speed", + "compressor_speed": 0.0 + }, { + "name": "runtime_history", + "actions": { + "get_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=daily" + }, + "get_monthly_runtime_history": { + "method": "GET", + "href": "https://www.mynexia.com/mobile/runtime_history/2059652?report_type=monthly" + } + } + }], + "icon": [{ + "name": "thermostat", + "modifiers": ["temperature-77"] + }, { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, { + "name": "thermostat", + "modifiers": ["temperature-75"] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?device_id=2059652" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=3679e95b-7337-48ae-aff4-e0522e9dd0eb" + }, + "pending_request": { + "polling_path": "https://www.mynexia.com/backstage/announcements/c6627726f6339d104ee66897028d6a2ea38215675b336650" + } + }, + "last_updated_at": "2020-03-11T15:15:53.000-05:00", + "settings": [{ + "type": "fan_mode", + "title": "Fan Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "on", + "label": "On" + }, { + "value": "circulate", + "label": "Circulate" + }], + "labels": ["Auto", "On", "Circulate"], + "values": ["auto", "on", "circulate"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_mode" + } + } + }, { + "type": "fan_speed", + "title": "Fan Speed", + "current_value": 0.35, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }, { + "value": 0.7, + "label": "70%" + }, { + "value": 0.75, + "label": "75%" + }, { + "value": 0.8, + "label": "80%" + }, { + "value": 0.85, + "label": "85%" + }, { + "value": 0.9, + "label": "90%" + }, { + "value": 0.95, + "label": "95%" + }, { + "value": 1.0, + "label": "100%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%", "70%", "75%", "80%", "85%", "90%", "95%", "100%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65, 0.7, 0.75, 0.8, 0.85, 0.9, 0.95, 1.0], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_speed" + } + } + }, { + "type": "fan_circulation_time", + "title": "Fan Circulation Time", + "current_value": 30, + "options": [{ + "value": 10, + "label": "10 minutes" + }, { + "value": 15, + "label": "15 minutes" + }, { + "value": 20, + "label": "20 minutes" + }, { + "value": 25, + "label": "25 minutes" + }, { + "value": 30, + "label": "30 minutes" + }, { + "value": 35, + "label": "35 minutes" + }, { + "value": 40, + "label": "40 minutes" + }, { + "value": 45, + "label": "45 minutes" + }, { + "value": 50, + "label": "50 minutes" + }, { + "value": 55, + "label": "55 minutes" + }], + "labels": ["10 minutes", "15 minutes", "20 minutes", "25 minutes", "30 minutes", "35 minutes", "40 minutes", "45 minutes", "50 minutes", "55 minutes"], + "values": [10, 15, 20, 25, 30, 35, 40, 45, 50, 55], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/fan_circulation_time" + } + } + }, { + "type": "air_cleaner_mode", + "title": "Air Cleaner Mode", + "current_value": "auto", + "options": [{ + "value": "auto", + "label": "Auto" + }, { + "value": "quick", + "label": "Quick" + }, { + "value": "allergy", + "label": "Allergy" + }], + "labels": ["Auto", "Quick", "Allergy"], + "values": ["auto", "quick", "allergy"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/air_cleaner_mode" + } + } + }, { + "type": "dehumidify", + "title": "Cooling Dehumidify Set Point", + "current_value": 0.5, + "options": [{ + "value": 0.35, + "label": "35%" + }, { + "value": 0.4, + "label": "40%" + }, { + "value": 0.45, + "label": "45%" + }, { + "value": 0.5, + "label": "50%" + }, { + "value": 0.55, + "label": "55%" + }, { + "value": 0.6, + "label": "60%" + }, { + "value": 0.65, + "label": "65%" + }], + "labels": ["35%", "40%", "45%", "50%", "55%", "60%", "65%"], + "values": [0.35, 0.4, 0.45, 0.5, 0.55, 0.6, 0.65], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/dehumidify" + } + } + }, { + "type": "scale", + "title": "Temperature Scale", + "current_value": "f", + "options": [{ + "value": "f", + "label": "F" + }, { + "value": "c", + "label": "C" + }], + "labels": ["F", "C"], + "values": ["f", "c"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_thermostats/2059652/scale" + } + } + }], + "status_secondary": null, + "status_tertiary": null, + "type": "xxl_thermostat", + "has_outdoor_temperature": true, + "outdoor_temperature": "87", + "has_indoor_humidity": true, + "connected": true, + "indoor_humidity": "37", + "system_status": "System Idle", + "delta": 3, + "zones": [{ + "type": "xxl_zone", + "id": 83260991, + "name": "Hallway", + "current_zone_mode": "OFF", + "temperature": 77, + "setpoints": { + "heat": 63, + "cool": 80 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 80, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "OFF", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-77"] + }, + "features": [{ + "name": "thermostat", + "temperature": 77, + "status": "", + "status_icon": null, + "actions": {}, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "OFF", + "display_value": "Off", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260991\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260991", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260991", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260991" + } + } + }, { + "type": "xxl_zone", + "id": 83260994, + "name": "Mid Bedroom", + "current_zone_mode": "AUTO", + "temperature": 74, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-74"] + }, + "features": [{ + "name": "thermostat", + "temperature": 74, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260994\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260994", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260994", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260994" + } + } + }, { + "type": "xxl_zone", + "id": 83260997, + "name": "West Bedroom", + "current_zone_mode": "AUTO", + "temperature": 75, + "setpoints": { + "heat": 63, + "cool": 81 + }, + "operating_state": "", + "heating_setpoint": 63, + "cooling_setpoint": 81, + "zone_status": "", + "settings": [{ + "type": "preset_selected", + "title": "Preset", + "current_value": 0, + "options": [{ + "value": 0, + "label": "None" + }, { + "value": 1, + "label": "Home" + }, { + "value": 2, + "label": "Away" + }, { + "value": 3, + "label": "Sleep" + }], + "labels": ["None", "Home", "Away", "Sleep"], + "values": [0, 1, 2, 3], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/preset_selected" + } + } + }, { + "type": "zone_mode", + "title": "Zone Mode", + "current_value": "AUTO", + "options": [{ + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "labels": ["Auto", "Cooling", "Heating", "Off"], + "values": ["AUTO", "COOL", "HEAT", "OFF"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, { + "type": "run_mode", + "title": "Run Mode", + "current_value": "permanent_hold", + "options": [{ + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "labels": ["Permanent Hold", "Run Schedule"], + "values": ["permanent_hold", "run_schedule"], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, { + "type": "scheduling_enabled", + "title": "Scheduling", + "current_value": true, + "options": [{ + "value": true, + "label": "ON" + }, { + "value": false, + "label": "OFF" + }], + "labels": ["ON", "OFF"], + "values": [true, false], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled" + } + } + }], + "icon": { + "name": "thermostat", + "modifiers": ["temperature-75"] + }, + "features": [{ + "name": "thermostat", + "temperature": 75, + "status": "", + "status_icon": null, + "actions": { + "set_heat_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + }, + "set_cool_setpoint": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/setpoints" + } + }, + "setpoint_delta": 3, + "scale": "f", + "setpoint_increment": 1.0, + "setpoint_heat_min": 55, + "setpoint_heat_max": 90, + "setpoint_cool_min": 60, + "setpoint_cool_max": 99, + "setpoint_heat": 63, + "setpoint_cool": 81, + "system_status": "System Idle" + }, { + "name": "connection", + "signal_strength": "unknown", + "is_connected": true + }, { + "name": "thermostat_mode", + "label": "Zone Mode", + "value": "AUTO", + "display_value": "Auto", + "options": [{ + "id": "thermostat_mode", + "label": "Zone Mode", + "value": "thermostat_mode", + "header": true + }, { + "value": "AUTO", + "label": "Auto" + }, { + "value": "COOL", + "label": "Cooling" + }, { + "value": "HEAT", + "label": "Heating" + }, { + "value": "OFF", + "label": "Off" + }], + "actions": { + "update_thermostat_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/zone_mode" + } + } + }, { + "name": "thermostat_run_mode", + "label": "Run Mode", + "options": [{ + "id": "thermostat_run_mode", + "label": "Run Mode", + "value": "thermostat_run_mode", + "header": true + }, { + "id": "info_text", + "label": "Follow or override the schedule.", + "value": "info_text", + "info": true + }, { + "value": "permanent_hold", + "label": "Permanent Hold" + }, { + "value": "run_schedule", + "label": "Run Schedule" + }], + "value": "permanent_hold", + "display_value": "Hold", + "actions": { + "update_thermostat_run_mode": { + "method": "POST", + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/run_mode" + } + } + }, { + "name": "schedule", + "enabled": true, + "max_period_name_length": 10, + "setpoint_increment": 1, + "collection_url": "https://www.mynexia.com/mobile/schedules?device_identifier=XxlZone-83260997\u0026house_id=123456", + "actions": { + "get_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "set_active_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/set_active_schedule?device_identifier=XxlZone-83260997", + "method": "POST" + }, + "get_default_schedule": { + "href": "https://www.mynexia.com/mobile/thermostat_schedules/get_default_schedule?device_identifier=XxlZone-83260997", + "method": "GET" + }, + "enable_scheduling": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997/scheduling_enabled", + "method": "POST", + "data": { + "value": true + } + } + }, + "can_add_remove_periods": true, + "max_periods_per_day": 4 + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/xxl_zones/83260997" + } + } + }] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/devices" + }, + "template": { + "data": { + "title": null, + "fields": [], + "_links": { + "child-schema": [{ + "data": { + "label": "Connect New Device", + "icon": { + "name": "new_device", + "modifiers": [] + }, + "_links": { + "next": { + "href": "https://www.mynexia.com/mobile/houses/123456/enrollables_schema" + } + } + } + }, { + "data": { + "label": "Create Group", + "icon": { + "name": "create_group", + "modifiers": [] + }, + "_links": { + "next": { + "href": "https://www.mynexia.com/mobile/houses/123456/groups/new" + } + } + } + }] + } + } + } + }, + "item_type": "application/vnd.nexia.device+json" + } + }, { + "href": "https://www.mynexia.com/mobile/houses/123456/automations", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [{ + "id": 3467876, + "name": "Away for 12 Hours", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Downstairs East Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Downstairs West Wing will permanently hold the heat to 62.0 and cool to 83.0 AND Activate the mode named 'Away 12' AND Master Suite will permanently hold the heat to 62.0 and cool to 83.0", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "plane", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467876" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467876", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467876" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=472ae0d2-5d7c-4a1c-9e47-4d9035fdace5" + } + } + }, { + "id": 3467870, + "name": "Away For 24 Hours", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs East Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Activate the mode named 'Away 24' AND Master Suite will permanently hold the heat to 60.0 and cool to 85.0", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "plane", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3467870" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3467870", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3467870" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=f63ee20c-3146-49a1-87c5-47429a063d15" + } + } + }, { + "id": 3452469, + "name": "Away Short", + "enabled": false, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 63.0 and cool to 80.0 AND Downstairs East Wing will permanently hold the heat to 63.0 and cool to 79.0 AND Downstairs West Wing will permanently hold the heat to 63.0 and cool to 79.0 AND Upstairs West Wing will permanently hold the heat to 63.0 and cool to 81.0 AND Upstairs West Wing will change Fan Mode to Auto AND Downstairs East Wing will change Fan Mode to Auto AND Downstairs West Wing will change Fan Mode to Auto AND Activate the mode named 'Away Short' AND Master Suite will permanently hold the heat to 63.0 and cool to 79.0 AND Master Suite will change Fan Mode to Auto", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "key", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452469" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452469", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452469" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=e5c59b93-efca-4937-9499-3f4c896ab17c" + } + } + }, { + "id": 3452472, + "name": "Home", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Activate the mode named 'Home' AND Master Suite will Run Schedule", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "at_home", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3452472" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3452472", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3452472" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=861b9fec-d259-4492-a798-5712251666c4" + } + } + }, { + "id": 3454776, + "name": "IFTTT Power Spike", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs East Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Downstairs West Wing will permanently hold the heat to 60.0 and cool to 85.0 AND Upstairs West Wing will change Fan Mode to Auto AND Downstairs East Wing will change Fan Mode to Auto AND Downstairs West Wing will change Fan Mode to Auto AND Master Suite will permanently hold the heat to 60.0 and cool to 85.0 AND Master Suite will change Fan Mode to Auto", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454776" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454776", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454776" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=96c71d37-66aa-4cbb-84ff-a90412fd366a" + } + } + }, { + "id": 3454774, + "name": "IFTTT return to schedule", + "enabled": false, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Master Suite will Run Schedule", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3454774" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3454774", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3454774" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=880c5287-d92c-4368-8494-e10975e92733" + } + } + }, { + "id": 3486078, + "name": "Power Outage", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Downstairs East Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Downstairs West Wing will permanently hold the heat to 55.0 and cool to 90.0 AND Activate the mode named 'Power Outage'", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "climate", + "modifiers": [] + }, { + "name": "bell", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486078" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486078", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486078" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=d33c013b-2357-47a9-8c66-d2c3693173b0" + } + } + }, { + "id": 3486091, + "name": "Power Restored", + "enabled": true, + "settings": [], + "triggers": [], + "description": "When IFTTT activates the automation Upstairs West Wing will Run Schedule AND Downstairs East Wing will Run Schedule AND Downstairs West Wing will Run Schedule AND Activate the mode named 'Home'", + "icon": [{ + "name": "gears", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "settings", + "modifiers": [] + }, { + "name": "at_home", + "modifiers": [] + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/automations/3486091" + }, + "edit": { + "href": "https://www.mynexia.com/mobile/automation_edit_buffers?automation_id=3486091", + "method": "POST" + }, + "nexia:history": { + "href": "https://www.mynexia.com/mobile/houses/123456/events?automation_id=3486091" + }, + "filter_events": { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection?sys_guid=b9141df8-2e5e-4524-b8ef-efcbf48d775a" + } + } + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/automations" + }, + "template": { + "href": "https://www.mynexia.com/mobile/houses/123456/automation_edit_buffers", + "method": "POST" + } + }, + "item_type": "application/vnd.nexia.automation+json" + } + }, { + "href": "https://www.mynexia.com/mobile/houses/123456/modes", + "type": "application/vnd.nexia.collection+json", + "data": { + "items": [{ + "id": 3047801, + "name": "Home", + "current_mode": false, + "icon": "home.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3047801" + } + } + }, { + "id": 3174574, + "name": "Away Short", + "current_mode": true, + "icon": "key.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3174574" + } + } + }, { + "id": 3174576, + "name": "Away 12", + "current_mode": false, + "icon": "picture.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3174576" + } + } + }, { + "id": 3174577, + "name": "Away 24", + "current_mode": false, + "icon": "picture.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3174577" + } + } + }, { + "id": 3197871, + "name": "Power Outage", + "current_mode": false, + "icon": "bell.png", + "settings": [], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/modes/3197871" + } + } + }], + "_links": { + "self": { + "href": "https://www.mynexia.com/mobile/houses/123456/modes" + } + }, + "item_type": "application/vnd.nexia.mode+json" + } + }, { + "href": "https://www.mynexia.com/mobile/houses/123456/events/collection", + "type": "application/vnd.nexia.collection+json", + "data": { + "item_type": "application/vnd.nexia.event+json" + } + }, { + "href": "https://www.mynexia.com/mobile/houses/123456/videos/collection", + "type": "application/vnd.nexia.collection+json", + "data": { + "item_type": "application/vnd.nexia.video+json" + } + }] + } + } +} \ No newline at end of file diff --git a/tests/fixtures/nexia/session_123456.json b/tests/fixtures/nexia/session_123456.json new file mode 100644 index 00000000000..3991a7d565f --- /dev/null +++ b/tests/fixtures/nexia/session_123456.json @@ -0,0 +1,25 @@ +{ + "success" : true, + "result" : { + "is_activated_by_activation_code" : 0, + "can_receive_notifications" : true, + "can_manage_locks" : true, + "can_control_automations" : true, + "_links" : { + "child" : [ + { + "data" : { + "name" : "House", + "postal_code" : "12345", + "id" : 123456 + } + } + ], + "self" : { + "href" : "https://www.mynexia.com/mobile/session" + } + }, + "can_view_videos" : true + }, + "error" : null +} diff --git a/tests/fixtures/nexia/sign_in.json b/tests/fixtures/nexia/sign_in.json new file mode 100644 index 00000000000..aac2fb1ae62 --- /dev/null +++ b/tests/fixtures/nexia/sign_in.json @@ -0,0 +1,10 @@ +{ + "success": true, + "error": null, + "result": { + "mobile_id": 1, + "api_key": "mock", + "setup_step": "done", + "locale": "en_us" + } +} From b6d9454b54680abb90df7bbf875f05d82c632835 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 20 Mar 2020 20:27:37 -0700 Subject: [PATCH 162/431] Fix package default extraction (#33071) --- homeassistant/config.py | 4 +++- tests/test_config.py | 16 ++++++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/homeassistant/config.py b/homeassistant/config.py index bd956846886..b63acf4ab4c 100644 --- a/homeassistant/config.py +++ b/homeassistant/config.py @@ -574,7 +574,9 @@ def _identify_config_schema(module: ModuleType) -> Optional[str]: if hasattr(key, "default") and not isinstance( key.default, vol.schema_builder.Undefined ): - default_value = schema(key.default()) + default_value = module.CONFIG_SCHEMA({module.DOMAIN: key.default()})[ # type: ignore + module.DOMAIN # type: ignore + ] if isinstance(default_value, dict): return "dict" diff --git a/tests/test_config.py b/tests/test_config.py index ba0153e7a7d..f226c72f7ad 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -995,8 +995,20 @@ async def test_component_config_exceptions(hass, caplog): @pytest.mark.parametrize( "domain, schema, expected", [ - ("zone", vol.Schema({vol.Optional("zone", default=[]): list}), "list"), - ("zone", vol.Schema({vol.Optional("zone", default=dict): dict}), "dict"), + ("zone", vol.Schema({vol.Optional("zone", default=list): [int]}), "list"), + ("zone", vol.Schema({vol.Optional("zone", default=[]): [int]}), "list"), + ( + "zone", + vol.Schema({vol.Optional("zone", default={}): {vol.Optional("hello"): 1}}), + "dict", + ), + ( + "zone", + vol.Schema( + {vol.Optional("zone", default=dict): {vol.Optional("hello"): 1}} + ), + "dict", + ), ("zone", vol.Schema({vol.Optional("zone"): int}), None), ("zone", vol.Schema({"zone": int}), None), ("not_existing", vol.Schema({vol.Optional("zone", default=dict): dict}), None,), From e87fab6b5f9811beadf6ae101c2cb17933f4a235 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Mar 2020 07:09:48 +0100 Subject: [PATCH 163/431] Remove mopar integration (ADR-0004) (#33066) --- .coveragerc | 1 - homeassistant/components/mopar/__init__.py | 140 ------------------- homeassistant/components/mopar/lock.py | 52 ------- homeassistant/components/mopar/manifest.json | 8 -- homeassistant/components/mopar/sensor.py | 90 ------------ homeassistant/components/mopar/services.yaml | 6 - homeassistant/components/mopar/switch.py | 52 ------- requirements_all.txt | 3 - 8 files changed, 352 deletions(-) delete mode 100644 homeassistant/components/mopar/__init__.py delete mode 100644 homeassistant/components/mopar/lock.py delete mode 100644 homeassistant/components/mopar/manifest.json delete mode 100644 homeassistant/components/mopar/sensor.py delete mode 100644 homeassistant/components/mopar/services.yaml delete mode 100644 homeassistant/components/mopar/switch.py diff --git a/.coveragerc b/.coveragerc index 548e4712404..e8f349013fc 100644 --- a/.coveragerc +++ b/.coveragerc @@ -432,7 +432,6 @@ omit = homeassistant/components/mochad/* homeassistant/components/modbus/* homeassistant/components/modem_callerid/sensor.py - homeassistant/components/mopar/* homeassistant/components/mpchc/media_player.py homeassistant/components/mpd/media_player.py homeassistant/components/mqtt_room/sensor.py diff --git a/homeassistant/components/mopar/__init__.py b/homeassistant/components/mopar/__init__.py deleted file mode 100644 index 4801a7c43d6..00000000000 --- a/homeassistant/components/mopar/__init__.py +++ /dev/null @@ -1,140 +0,0 @@ -"""Support for Mopar vehicles.""" -from datetime import timedelta -import logging - -import motorparts -import voluptuous as vol - -from homeassistant.components.lock import DOMAIN as LOCK -from homeassistant.components.sensor import DOMAIN as SENSOR -from homeassistant.components.switch import DOMAIN as SWITCH -from homeassistant.const import ( - CONF_PASSWORD, - CONF_PIN, - CONF_SCAN_INTERVAL, - CONF_USERNAME, -) -from homeassistant.helpers import config_validation as cv -from homeassistant.helpers.discovery import load_platform -from homeassistant.helpers.dispatcher import dispatcher_send -from homeassistant.helpers.event import track_time_interval - -DOMAIN = "mopar" -DATA_UPDATED = f"{DOMAIN}_data_updated" - -_LOGGER = logging.getLogger(__name__) - -COOKIE_FILE = "mopar_cookies.pickle" -SUCCESS_RESPONSE = "completed" - -SUPPORTED_PLATFORMS = [LOCK, SENSOR, SWITCH] - -DEFAULT_INTERVAL = timedelta(days=7) - -CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_PIN): cv.positive_int, - vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_INTERVAL): vol.All( - cv.time_period, cv.positive_timedelta - ), - } - ) - }, - extra=vol.ALLOW_EXTRA, -) - -SERVICE_HORN = "sound_horn" -ATTR_VEHICLE_INDEX = "vehicle_index" -SERVICE_HORN_SCHEMA = vol.Schema({vol.Required(ATTR_VEHICLE_INDEX): cv.positive_int}) - - -def setup(hass, config): - """Set up the Mopar component.""" - conf = config[DOMAIN] - cookie = hass.config.path(COOKIE_FILE) - try: - session = motorparts.get_session( - conf[CONF_USERNAME], conf[CONF_PASSWORD], conf[CONF_PIN], cookie_path=cookie - ) - except motorparts.MoparError: - _LOGGER.error("Failed to login") - return False - - data = hass.data[DOMAIN] = MoparData(hass, session) - data.update(now=None) - - track_time_interval(hass, data.update, conf[CONF_SCAN_INTERVAL]) - - def handle_horn(call): - """Enable the horn on the Mopar vehicle.""" - data.actuate("horn", call.data[ATTR_VEHICLE_INDEX]) - - hass.services.register( - DOMAIN, SERVICE_HORN, handle_horn, schema=SERVICE_HORN_SCHEMA - ) - - for platform in SUPPORTED_PLATFORMS: - load_platform(hass, platform, DOMAIN, {}, config) - - return True - - -class MoparData: - """ - Container for Mopar vehicle data. - - Prevents session expiry re-login race condition. - """ - - def __init__(self, hass, session): - """Initialize data.""" - self._hass = hass - self._session = session - self.vehicles = [] - self.vhrs = {} - self.tow_guides = {} - - def update(self, now, **kwargs): - """Update data.""" - _LOGGER.debug("Updating vehicle data") - try: - self.vehicles = motorparts.get_summary(self._session)["vehicles"] - except motorparts.MoparError: - _LOGGER.exception("Failed to get summary") - return - - for index, _ in enumerate(self.vehicles): - try: - self.vhrs[index] = motorparts.get_report(self._session, index) - self.tow_guides[index] = motorparts.get_tow_guide(self._session, index) - except motorparts.MoparError: - _LOGGER.warning("Failed to update for vehicle index %s", index) - return - - dispatcher_send(self._hass, DATA_UPDATED) - - @property - def attribution(self): - """Get the attribution string from Mopar.""" - return motorparts.ATTRIBUTION - - def get_vehicle_name(self, index): - """Get the name corresponding with this vehicle.""" - vehicle = self.vehicles[index] - if not vehicle: - return None - return f"{vehicle['year']} {vehicle['make']} {vehicle['model']}" - - def actuate(self, command, index): - """Run a command on the specified Mopar vehicle.""" - try: - response = getattr(motorparts, command)(self._session, index) - except motorparts.MoparError as error: - _LOGGER.error(error) - return False - - return response == SUCCESS_RESPONSE diff --git a/homeassistant/components/mopar/lock.py b/homeassistant/components/mopar/lock.py deleted file mode 100644 index 3933e567723..00000000000 --- a/homeassistant/components/mopar/lock.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Support for the Mopar vehicle lock.""" -import logging - -from homeassistant.components.lock import LockDevice -from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN -from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Mopar lock platform.""" - data = hass.data[MOPAR_DOMAIN] - add_entities( - [MoparLock(data, index) for index, _ in enumerate(data.vehicles)], True - ) - - -class MoparLock(LockDevice): - """Representation of a Mopar vehicle lock.""" - - def __init__(self, data, index): - """Initialize the Mopar lock.""" - self._index = index - self._name = f"{data.get_vehicle_name(self._index)} Lock" - self._actuate = data.actuate - self._state = None - - @property - def name(self): - """Return the name of the lock.""" - return self._name - - @property - def is_locked(self): - """Return true if vehicle is locked.""" - return self._state == STATE_LOCKED - - @property - def should_poll(self): - """Return the polling requirement for this lock.""" - return False - - def lock(self, **kwargs): - """Lock the vehicle.""" - if self._actuate("lock", self._index): - self._state = STATE_LOCKED - - def unlock(self, **kwargs): - """Unlock the vehicle.""" - if self._actuate("unlock", self._index): - self._state = STATE_UNLOCKED diff --git a/homeassistant/components/mopar/manifest.json b/homeassistant/components/mopar/manifest.json deleted file mode 100644 index e8fae4fb069..00000000000 --- a/homeassistant/components/mopar/manifest.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "domain": "mopar", - "name": "Mopar", - "documentation": "https://www.home-assistant.io/integrations/mopar", - "requirements": ["motorparts==1.1.0"], - "dependencies": [], - "codeowners": [] -} diff --git a/homeassistant/components/mopar/sensor.py b/homeassistant/components/mopar/sensor.py deleted file mode 100644 index 2243fcdaa22..00000000000 --- a/homeassistant/components/mopar/sensor.py +++ /dev/null @@ -1,90 +0,0 @@ -"""Support for the Mopar vehicle sensor platform.""" -from homeassistant.components.mopar import ( - ATTR_VEHICLE_INDEX, - DATA_UPDATED, - DOMAIN as MOPAR_DOMAIN, -) -from homeassistant.const import ATTR_ATTRIBUTION, LENGTH_KILOMETERS -from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity - -ICON = "mdi:car" - - -async def async_setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Mopar platform.""" - data = hass.data[MOPAR_DOMAIN] - add_entities( - [MoparSensor(data, index) for index, _ in enumerate(data.vehicles)], True - ) - - -class MoparSensor(Entity): - """Mopar vehicle sensor.""" - - def __init__(self, data, index): - """Initialize the sensor.""" - self._index = index - self._vehicle = {} - self._vhr = {} - self._tow_guide = {} - self._odometer = None - self._data = data - self._name = self._data.get_vehicle_name(self._index) - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def state(self): - """Return the state of the sensor.""" - return self._odometer - - @property - def device_state_attributes(self): - """Return the state attributes.""" - attributes = { - ATTR_VEHICLE_INDEX: self._index, - ATTR_ATTRIBUTION: self._data.attribution, - } - attributes.update(self._vehicle) - attributes.update(self._vhr) - attributes.update(self._tow_guide) - return attributes - - @property - def unit_of_measurement(self): - """Return the unit of measurement.""" - return self.hass.config.units.length_unit - - @property - def icon(self): - """Return the icon.""" - return ICON - - @property - def should_poll(self): - """Return the polling requirement for this sensor.""" - return False - - async def async_added_to_hass(self): - """Handle entity which will be added.""" - async_dispatcher_connect( - self.hass, DATA_UPDATED, self._schedule_immediate_update - ) - - def update(self): - """Update device state.""" - self._vehicle = self._data.vehicles[self._index] - self._vhr = self._data.vhrs.get(self._index, {}) - self._tow_guide = self._data.tow_guides.get(self._index, {}) - if "odometer" in self._vhr: - odo = float(self._vhr["odometer"]) - self._odometer = int(self.hass.config.units.length(odo, LENGTH_KILOMETERS)) - - @callback - def _schedule_immediate_update(self): - self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/mopar/services.yaml b/homeassistant/components/mopar/services.yaml deleted file mode 100644 index 7915aefcb0f..00000000000 --- a/homeassistant/components/mopar/services.yaml +++ /dev/null @@ -1,6 +0,0 @@ -sound_horn: - description: Trigger the vehicle's horn - fields: - vehicle_index: - description: The index of the vehicle to trigger. This is exposed in the sensor's device attributes. - example: 1 \ No newline at end of file diff --git a/homeassistant/components/mopar/switch.py b/homeassistant/components/mopar/switch.py deleted file mode 100644 index c7a8c762fbc..00000000000 --- a/homeassistant/components/mopar/switch.py +++ /dev/null @@ -1,52 +0,0 @@ -"""Support for the Mopar vehicle switch.""" -import logging - -from homeassistant.components.mopar import DOMAIN as MOPAR_DOMAIN -from homeassistant.components.switch import SwitchDevice -from homeassistant.const import STATE_OFF, STATE_ON - -_LOGGER = logging.getLogger(__name__) - - -def setup_platform(hass, config, add_entities, discovery_info=None): - """Set up the Mopar Switch platform.""" - data = hass.data[MOPAR_DOMAIN] - add_entities( - [MoparSwitch(data, index) for index, _ in enumerate(data.vehicles)], True - ) - - -class MoparSwitch(SwitchDevice): - """Representation of a Mopar switch.""" - - def __init__(self, data, index): - """Initialize the Switch.""" - self._index = index - self._name = f"{data.get_vehicle_name(self._index)} Switch" - self._actuate = data.actuate - self._state = None - - @property - def name(self): - """Return the name of the switch.""" - return self._name - - @property - def is_on(self): - """Return True if the entity is on.""" - return self._state == STATE_ON - - @property - def should_poll(self): - """Return the polling requirement for this switch.""" - return False - - def turn_on(self, **kwargs): - """Turn on the Mopar Vehicle.""" - if self._actuate("engine_on", self._index): - self._state = STATE_ON - - def turn_off(self, **kwargs): - """Turn off the Mopar Vehicle.""" - if self._actuate("engine_off", self._index): - self._state = STATE_OFF diff --git a/requirements_all.txt b/requirements_all.txt index ad72deb27a9..af8e8741c64 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -882,9 +882,6 @@ minio==4.0.9 # homeassistant.components.mitemp_bt mitemp_bt==0.0.3 -# homeassistant.components.mopar -motorparts==1.1.0 - # homeassistant.components.tts mutagen==1.43.0 From ffc9bcb4d7396312d2a9fa8d174bb6e81a3e0d5f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= Date: Sat, 21 Mar 2020 11:18:32 +0200 Subject: [PATCH 164/431] Use PEP 526 syntax with NamedTuples (#33081) --- homeassistant/auth/models.py | 6 +++- homeassistant/components/tplink/light.py | 40 +++++++++++------------- tests/components/tplink/test_light.py | 27 ++++++++-------- tests/components/vera/common.py | 6 +++- 4 files changed, 41 insertions(+), 38 deletions(-) diff --git a/homeassistant/auth/models.py b/homeassistant/auth/models.py index 8b4e6355700..502155df129 100644 --- a/homeassistant/auth/models.py +++ b/homeassistant/auth/models.py @@ -123,4 +123,8 @@ class Credentials: is_new = attr.ib(type=bool, default=True) -UserMeta = NamedTuple("UserMeta", [("name", Optional[str]), ("is_active", bool)]) +class UserMeta(NamedTuple): + """User metadata.""" + + name: Optional[str] + is_active: bool diff --git a/homeassistant/components/tplink/light.py b/homeassistant/components/tplink/light.py index 0e7be471f43..7e07f7931f5 100644 --- a/homeassistant/components/tplink/light.py +++ b/homeassistant/components/tplink/light.py @@ -75,30 +75,26 @@ def brightness_from_percentage(percent): return (percent * 255.0) / 100.0 -LightState = NamedTuple( - "LightState", - ( - ("state", bool), - ("brightness", int), - ("color_temp", float), - ("hs", Tuple[int, int]), - ("emeter_params", dict), - ), -) +class LightState(NamedTuple): + """Light state.""" + + state: bool + brightness: int + color_temp: float + hs: Tuple[int, int] + emeter_params: dict -LightFeatures = NamedTuple( - "LightFeatures", - ( - ("sysinfo", Dict[str, Any]), - ("mac", str), - ("alias", str), - ("model", str), - ("supported_features", int), - ("min_mireds", float), - ("max_mireds", float), - ), -) +class LightFeatures(NamedTuple): + """Light features.""" + + sysinfo: Dict[str, Any] + mac: str + alias: str + model: str + supported_features: int + min_mireds: float + max_mireds: float class TPLinkSmartBulb(Light): diff --git a/tests/components/tplink/test_light.py b/tests/components/tplink/test_light.py index 8e5a2a775b9..e13870b8ee2 100644 --- a/tests/components/tplink/test_light.py +++ b/tests/components/tplink/test_light.py @@ -26,20 +26,19 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -LightMockData = NamedTuple( - "LightMockData", - ( - ("sys_info", dict), - ("light_state", dict), - ("set_light_state", Callable[[dict], None]), - ("set_light_state_mock", Mock), - ("get_light_state_mock", Mock), - ("current_consumption_mock", Mock), - ("get_sysinfo_mock", Mock), - ("get_emeter_daily_mock", Mock), - ("get_emeter_monthly_mock", Mock), - ), -) + +class LightMockData(NamedTuple): + """Mock light data.""" + + sys_info: dict + light_state: dict + set_light_state: Callable[[dict], None] + set_light_state_mock: Mock + get_light_state_mock: Mock + current_consumption_mock: Mock + get_sysinfo_mock: Mock + get_emeter_daily_mock: Mock + get_emeter_monthly_mock: Mock @pytest.fixture(name="light_mock_data") diff --git a/tests/components/vera/common.py b/tests/components/vera/common.py index e99540e675e..649cf9af6a5 100644 --- a/tests/components/vera/common.py +++ b/tests/components/vera/common.py @@ -9,7 +9,11 @@ from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component -ComponentData = NamedTuple("ComponentData", (("controller", VeraController),)) + +class ComponentData(NamedTuple): + """Component data.""" + + controller: VeraController class ComponentFactory: From ebc4804e04b1cabfac8e0d747862eba71e46844a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Mar 2020 15:29:46 +0100 Subject: [PATCH 165/431] Don't exempt project for PR's for stalebot (#33086) --- .github/stale.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/stale.yml b/.github/stale.yml index e75d791a57c..f09f3733651 100644 --- a/.github/stale.yml +++ b/.github/stale.yml @@ -57,6 +57,7 @@ limitPerRun: 30 # Handle pull requests a little bit faster and with an adjusted comment. pulls: daysUntilStale: 30 + exemptProjects: false markComment: > There hasn't been any activity on this pull request recently. This pull request has been automatically marked as stale because of that and will From 49c2a4a4e3290de32cf3a10fd17f5c7a1f0234de Mon Sep 17 00:00:00 2001 From: guillempages Date: Sat, 21 Mar 2020 15:43:12 +0100 Subject: [PATCH 166/431] Validate UUID for tankerkoenig (#32805) * Validate UUIDs against custom validator Instead of just validating against strings, use a custom validator, so that the format can be checked. * Add tests for custom UUID4 validator --- .../components/tankerkoenig/__init__.py | 20 ++++++++++-- requirements_test_all.txt | 3 ++ tests/components/tankerkoenig/__init__.py | 1 + .../test_tankerkoenig_validators.py | 32 +++++++++++++++++++ 4 files changed, 54 insertions(+), 2 deletions(-) create mode 100755 tests/components/tankerkoenig/__init__.py create mode 100755 tests/components/tankerkoenig/test_tankerkoenig_validators.py diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index fde2f1c57cd..aa8c6f87dc8 100755 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -1,6 +1,7 @@ """Ask tankerkoenig.de for petrol price information.""" from datetime import timedelta import logging +from uuid import UUID import pytankerkoenig import voluptuous as vol @@ -24,11 +25,26 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_RADIUS = 2 DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) + +def uuid4_string(value): + """Validate a v4 UUID in string format.""" + try: + result = UUID(value, version=4) + except (ValueError, AttributeError, TypeError) as error: + raise vol.Invalid("Invalid Version4 UUID", error_message=str(error)) + + if str(result) != value.lower(): + # UUID() will create a uuid4 if input is invalid + raise vol.Invalid("Invalid Version4 UUID") + + return str(result) + + CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_API_KEY): cv.string, + vol.Required(CONF_API_KEY): uuid4_string, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, @@ -49,7 +65,7 @@ CONFIG_SCHEMA = vol.Schema( cv.positive_int, vol.Range(min=1) ), vol.Optional(CONF_STATIONS, default=[]): vol.All( - cv.ensure_list, [cv.string] + cv.ensure_list, [uuid4_string] ), } ) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 840905e85a0..4ecf3dc9d22 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -588,6 +588,9 @@ pysonos==0.0.24 # homeassistant.components.spc pyspcwebgw==0.4.0 +# homeassistant.components.tankerkoenig +pytankerkoenig==0.0.6 + # homeassistant.components.ecobee python-ecobee-api==0.2.2 diff --git a/tests/components/tankerkoenig/__init__.py b/tests/components/tankerkoenig/__init__.py new file mode 100755 index 00000000000..c420d23e157 --- /dev/null +++ b/tests/components/tankerkoenig/__init__.py @@ -0,0 +1 @@ +"""Tests for the tankerkoenig integration.""" diff --git a/tests/components/tankerkoenig/test_tankerkoenig_validators.py b/tests/components/tankerkoenig/test_tankerkoenig_validators.py new file mode 100755 index 00000000000..7dba88b24fb --- /dev/null +++ b/tests/components/tankerkoenig/test_tankerkoenig_validators.py @@ -0,0 +1,32 @@ +"""The tests for the custom tankerkoenig validators.""" +import unittest +import uuid + +import pytest +import voluptuous as vol + +from homeassistant.components.tankerkoenig import uuid4_string + + +class TestUUID4StringValidator(unittest.TestCase): + """Test the UUID4 string custom validator.""" + + def test_uuid4_string(caplog): + """Test string uuid validation.""" + schema = vol.Schema(uuid4_string) + + for value in ["Not a hex string", "0", 0]: + with pytest.raises(vol.Invalid): + schema(value) + + with pytest.raises(vol.Invalid): + # the third block should start with 4 + schema("a03d31b2-2eee-2acc-bb90-eec40be6ed23") + + with pytest.raises(vol.Invalid): + # the fourth block should start with 8-a + schema("a03d31b2-2eee-4acc-1b90-eec40be6ed23") + + _str = str(uuid.uuid4()) + assert schema(_str) == _str + assert schema(_str.upper()) == _str From e18bea02152ab166df59bb6cdfad1a8078f85d04 Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Sat, 21 Mar 2020 15:43:55 +0100 Subject: [PATCH 167/431] bump version zigpy-cc (#33097) --- 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 da437e5f4d4..8bb7c3c2149 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows-homeassistant==0.14.0", "zha-quirks==0.0.37", - "zigpy-cc==0.2.3", + "zigpy-cc==0.3.0", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.16.0", "zigpy-xbee-homeassistant==0.10.0", diff --git a/requirements_all.txt b/requirements_all.txt index af8e8741c64..bcb6fbb5988 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2168,7 +2168,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.2.3 +zigpy-cc==0.3.0 # homeassistant.components.zha zigpy-deconz==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4ecf3dc9d22..65d400b4a3b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -782,7 +782,7 @@ zeroconf==0.24.5 zha-quirks==0.0.37 # homeassistant.components.zha -zigpy-cc==0.2.3 +zigpy-cc==0.3.0 # homeassistant.components.zha zigpy-deconz==0.7.0 From 57820be92adf74d440f16ffb2be705f461474afb Mon Sep 17 00:00:00 2001 From: Jonas Kohlbrenner Date: Sat, 21 Mar 2020 16:45:10 +0100 Subject: [PATCH 168/431] Add scan_interval support for NUT integration (#31167) * Added scan_interval support * Changed to config[key] for required keys and optional keys with default value * Removed usage of Throttle --- homeassistant/components/nut/sensor.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 15dee84dd9b..f97f7212e10 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -24,7 +24,6 @@ from homeassistant.const import ( from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) @@ -35,7 +34,7 @@ DEFAULT_PORT = 3493 KEY_STATUS = "ups.status" KEY_STATUS_DISPLAY = "ups.status.display" -MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) +SCAN_INTERVAL = timedelta(seconds=60) SENSOR_TYPES = { "ups.status.display": ["Status", "", "mdi:information-outline"], @@ -166,9 +165,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the NUT sensors.""" - name = config.get(CONF_NAME) - host = config.get(CONF_HOST) - port = config.get(CONF_PORT) + name = config[CONF_NAME] + host = config[CONF_HOST] + port = config[CONF_PORT] + alias = config.get(CONF_ALIAS) username = config.get(CONF_USERNAME) password = config.get(CONF_PASSWORD) @@ -325,7 +325,6 @@ class PyNUTData: _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) return None - @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self, **kwargs): """Fetch the latest status from NUT.""" self._status = self._get_status() From 2bb29485bee415a2fc983f8a7e96ddaa249cdb24 Mon Sep 17 00:00:00 2001 From: escoand Date: Sat, 21 Mar 2020 16:50:18 +0100 Subject: [PATCH 169/431] Try all Samsung TV websocket ports (#33001) * Update bridge.py * add test * silence pylint * correct pylint * add some tests * Update test_media_player.py --- homeassistant/components/samsungtv/bridge.py | 7 +- .../components/samsungtv/test_config_flow.py | 76 +++++++++++++------ .../components/samsungtv/test_media_player.py | 76 ++++++++++++++++++- 3 files changed, 133 insertions(+), 26 deletions(-) diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index b582f6269e4..a0f16e91cf5 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -206,6 +206,7 @@ class SamsungTVWSBridge(SamsungTVBridge): CONF_TIMEOUT: 31, } + result = None try: LOGGER.debug("Try config: %s", config) with SamsungTVWS( @@ -223,9 +224,13 @@ class SamsungTVWSBridge(SamsungTVBridge): return RESULT_SUCCESS except WebSocketException: LOGGER.debug("Working but unsupported config: %s", config) - return RESULT_NOT_SUPPORTED + result = RESULT_NOT_SUPPORTED except (OSError, ConnectionFailure) as err: LOGGER.debug("Failing config: %s, error: %s", config, err) + # pylint: disable=useless-else-on-loop + else: + if result: + return result return RESULT_NOT_SUCCESSFUL diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index 5485ee95827..65807602f09 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,5 +1,5 @@ """Tests for Samsung TV config flow.""" -from unittest.mock import call, patch +from unittest.mock import Mock, PropertyMock, call, patch from asynctest import mock import pytest @@ -19,7 +19,7 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_MODEL_NAME, ATTR_UPNP_UDN, ) -from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME +from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { @@ -46,6 +46,20 @@ AUTODETECT_LEGACY = { "host": "fake_host", "timeout": 31, } +AUTODETECT_WEBSOCKET_PLAIN = { + "host": "fake_host", + "name": "HomeAssistant", + "port": 8001, + "timeout": 31, + "token": None, +} +AUTODETECT_WEBSOCKET_SSL = { + "host": "fake_host", + "name": "HomeAssistant", + "port": 8002, + "timeout": 31, + "token": None, +} @pytest.fixture(name="remote") @@ -446,20 +460,48 @@ async def test_autodetect_websocket(hass, remote, remotews): with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: + enter = Mock() + type(enter).token = PropertyMock(return_value="123456789") + remote = Mock() + remote.__enter__ = Mock(return_value=enter) + remote.__exit__ = Mock(return_value=False) + remotews.return_value = remote + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA ) assert result["type"] == "create_entry" assert result["data"][CONF_METHOD] == "websocket" + assert result["data"][CONF_TOKEN] == "123456789" assert remotews.call_count == 1 + assert remotews.call_args_list == [call(**AUTODETECT_WEBSOCKET_PLAIN)] + + +async def test_autodetect_websocket_ssl(hass, remote, remotews): + """Test for send key with autodetection of protocol.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS", + side_effect=[WebSocketProtocolException("Boom"), mock.DEFAULT], + ) as remotews: + enter = Mock() + type(enter).token = PropertyMock(return_value="123456789") + remote = Mock() + remote.__enter__ = Mock(return_value=enter) + remote.__exit__ = Mock(return_value=False) + remotews.return_value = remote + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_USER_DATA + ) + assert result["type"] == "create_entry" + assert result["data"][CONF_METHOD] == "websocket" + assert result["data"][CONF_TOKEN] == "123456789" + assert remotews.call_count == 2 assert remotews.call_args_list == [ - call( - host="fake_host", - name="HomeAssistant", - port=8001, - timeout=31, - token=None, - ) + call(**AUTODETECT_WEBSOCKET_PLAIN), + call(**AUTODETECT_WEBSOCKET_SSL), ] @@ -524,18 +566,6 @@ async def test_autodetect_none(hass, remote, remotews): ] assert remotews.call_count == 2 assert remotews.call_args_list == [ - call( - host="fake_host", - name="HomeAssistant", - port=8001, - timeout=31, - token=None, - ), - call( - host="fake_host", - name="HomeAssistant", - port=8002, - timeout=31, - token=None, - ), + call(**AUTODETECT_WEBSOCKET_PLAIN), + call(**AUTODETECT_WEBSOCKET_SSL), ] diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index dff7525d980..b7881d2ddaa 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -34,8 +34,11 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_SUPPORTED_FEATURES, CONF_HOST, + CONF_IP_ADDRESS, + CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_TOKEN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, @@ -51,7 +54,7 @@ from homeassistant.const import ( from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake" MOCK_CONFIG = { @@ -64,17 +67,40 @@ MOCK_CONFIG = { } ] } - MOCK_CONFIGWS = { SAMSUNGTV_DOMAIN: [ { CONF_HOST: "fake", CONF_NAME: "fake", CONF_PORT: 8001, + CONF_TOKEN: "123456789", CONF_ON_ACTION: [{"delay": "00:00:01"}], } ] } +MOCK_CALLS_WS = { + "host": "fake", + "port": 8001, + "token": None, + "timeout": 31, + "name": "HomeAssistant", +} + +MOCK_ENTRY_WS = { + CONF_IP_ADDRESS: "test", + CONF_HOST: "fake", + CONF_METHOD: "websocket", + CONF_NAME: "fake", + CONF_PORT: 8001, + CONF_TOKEN: "abcde", +} +MOCK_CALLS_ENTRY_WS = { + "host": "fake", + "name": "HomeAssistant", + "port": 8001, + "timeout": 1, + "token": "abcde", +} ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon" MOCK_CONFIG_NOTURNON = { @@ -155,6 +181,52 @@ async def test_setup_without_turnon(hass, remote): assert hass.states.get(ENTITY_ID_NOTURNON) +async def test_setup_websocket(hass, remotews, mock_now): + """Test setup of platform.""" + with patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remote_class: + enter = mock.Mock() + type(enter).token = mock.PropertyMock(return_value="987654321") + remote = mock.Mock() + remote.__enter__ = mock.Mock(return_value=enter) + remote.__exit__ = mock.Mock() + remote_class.return_value = remote + + await setup_samsungtv(hass, MOCK_CONFIGWS) + + assert remote_class.call_count == 1 + assert remote_class.call_args_list == [call(**MOCK_CALLS_WS)] + assert hass.states.get(ENTITY_ID) + + +async def test_setup_websocket_2(hass, mock_now): + """Test setup of platform from config entry.""" + entity_id = f"{DOMAIN}.fake" + + entry = MockConfigEntry( + domain=SAMSUNGTV_DOMAIN, data=MOCK_ENTRY_WS, unique_id=entity_id, + ) + entry.add_to_hass(hass) + + config_entries = hass.config_entries.async_entries(SAMSUNGTV_DOMAIN) + assert len(config_entries) == 1 + assert entry is config_entries[0] + + assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) + await hass.async_block_till_done() + + next_update = mock_now + timedelta(minutes=5) + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remote, patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + state = hass.states.get(entity_id) + assert state + assert remote.call_count == 1 + assert remote.call_args_list == [call(**MOCK_CALLS_ENTRY_WS)] + + async def test_update_on(hass, remote, mock_now): """Testing update tv on.""" await setup_samsungtv(hass, MOCK_CONFIG) From 81cef9a2811c63f4f5f47f4846d1b6db604d52b5 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 21 Mar 2020 13:25:43 -0400 Subject: [PATCH 170/431] Split ZHA device loading and entities adding (#33075) * Split ZHA device loading and entities adding. Load zha devices from ZHA gateway, but add entities after integration was setup. * Use hass.loop.create_task() * Split ZHA device initialization from loading. Restore and initialize ZHA devices separately. * Use hass.async_create_task() Load devices prior group initialization. --- homeassistant/components/zha/__init__.py | 45 ++++++++++++------- homeassistant/components/zha/binary_sensor.py | 2 +- .../components/zha/core/discovery.py | 10 +++++ homeassistant/components/zha/core/gateway.py | 27 ++++++----- homeassistant/components/zha/cover.py | 2 +- .../components/zha/device_tracker.py | 2 +- homeassistant/components/zha/fan.py | 2 +- homeassistant/components/zha/light.py | 2 +- homeassistant/components/zha/lock.py | 2 +- homeassistant/components/zha/sensor.py | 2 +- homeassistant/components/zha/switch.py | 2 +- 11 files changed, 62 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index ac5648e097b..63659a47e0d 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -8,6 +8,8 @@ import voluptuous as vol from homeassistant import config_entries, const as ha_const import homeassistant.helpers.config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE +from homeassistant.helpers.dispatcher import async_dispatcher_send +from homeassistant.helpers.typing import HomeAssistantType from . import api from .core import ZHAGateway @@ -27,6 +29,7 @@ from .core.const import ( DEFAULT_BAUDRATE, DEFAULT_RADIO_TYPE, DOMAIN, + SIGNAL_ADD_ENTITIES, RadioType, ) @@ -90,8 +93,15 @@ async def async_setup_entry(hass, config_entry): """ zha_data = hass.data.setdefault(DATA_ZHA, {}) + zha_data[DATA_ZHA_PLATFORM_LOADED] = {} config = zha_data.get(DATA_ZHA_CONFIG, {}) + zha_data[DATA_ZHA_DISPATCHERS] = [] + for component in COMPONENTS: + zha_data[component] = [] + coro = hass.config_entries.async_forward_entry_setup(config_entry, component) + zha_data[DATA_ZHA_PLATFORM_LOADED][component] = hass.async_create_task(coro) + if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading # before zhaquirks is imported @@ -100,22 +110,6 @@ async def async_setup_entry(hass, config_entry): zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() - zha_data[DATA_ZHA_DISPATCHERS] = [] - zha_data[DATA_ZHA_PLATFORM_LOADED] = asyncio.Event() - platforms = [] - for component in COMPONENTS: - platforms.append( - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, component) - ) - ) - - async def _platforms_loaded(): - await asyncio.gather(*platforms) - zha_data[DATA_ZHA_PLATFORM_LOADED].set() - - hass.async_create_task(_platforms_loaded()) - device_registry = await hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -134,7 +128,7 @@ async def async_setup_entry(hass, config_entry): await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown) - hass.async_create_task(zha_gateway.async_load_devices()) + hass.async_create_task(async_load_entities(hass, config_entry)) return True @@ -152,3 +146,20 @@ async def async_unload_entry(hass, config_entry): await hass.config_entries.async_forward_entry_unload(config_entry, component) return True + + +async def async_load_entities( + hass: HomeAssistantType, config_entry: config_entries.ConfigEntry +) -> None: + """Load entities after integration was setup.""" + await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_prepare_entities() + to_setup = [ + hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED][comp] + for comp in COMPONENTS + if hass.data[DATA_ZHA][comp] + ] + results = await asyncio.gather(*to_setup, return_exceptions=True) + for res in results: + if isinstance(res, Exception): + _LOGGER.warning("Couldn't setup zha platform: %s", res) + async_dispatcher_send(hass, SIGNAL_ADD_ENTITIES) diff --git a/homeassistant/components/zha/binary_sensor.py b/homeassistant/components/zha/binary_sensor.py index 6c88f3e1013..9ed1bbfca16 100644 --- a/homeassistant/components/zha/binary_sensor.py +++ b/homeassistant/components/zha/binary_sensor.py @@ -49,7 +49,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation binary sensor from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index b60357cf9f3..5f8f6b593f8 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -8,6 +8,16 @@ from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from . import const as zha_const, registries as zha_regs, typing as zha_typing +from .. import ( # noqa: F401 pylint: disable=unused-import, + binary_sensor, + cover, + device_tracker, + fan, + light, + lock, + sensor, + switch, +) from .channels import base _LOGGER = logging.getLogger(__name__) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 1ad10710c60..78b5f939cae 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -36,7 +36,6 @@ from .const import ( DATA_ZHA, DATA_ZHA_BRIDGE_ID, DATA_ZHA_GATEWAY, - DATA_ZHA_PLATFORM_LOADED, DEBUG_COMP_BELLOWS, DEBUG_COMP_ZHA, DEBUG_COMP_ZIGPY, @@ -157,34 +156,40 @@ class ZHAGateway: self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( self.application_controller.ieee ) + await self.async_load_devices() self._initialize_groups() async def async_load_devices(self) -> None: """Restore ZHA devices from zigpy application state.""" - await self._hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED].wait() + zigpy_devices = self.application_controller.devices.values() + for zigpy_device in zigpy_devices: + self._async_get_or_create_device(zigpy_device, restored=True) + async def async_prepare_entities(self) -> None: + """Prepare entities by initializing device channels.""" semaphore = asyncio.Semaphore(2) - async def _throttle(device: zha_typing.ZigpyDeviceType): + async def _throttle(zha_device: zha_typing.ZhaDeviceType, cached: bool): async with semaphore: - await self.async_device_restored(device) + await zha_device.async_initialize(from_cache=cached) - zigpy_devices = self.application_controller.devices.values() _LOGGER.debug("Loading battery powered devices") await asyncio.gather( *[ - _throttle(dev) - for dev in zigpy_devices - if not dev.node_desc.is_mains_powered + _throttle(dev, cached=True) + for dev in self.devices.values() + if not dev.is_mains_powered ] ) - async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) _LOGGER.debug("Loading mains powered devices") await asyncio.gather( - *[_throttle(dev) for dev in zigpy_devices if dev.node_desc.is_mains_powered] + *[ + _throttle(dev, cached=False) + for dev in self.devices.values() + if dev.is_mains_powered + ] ) - async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) def device_joined(self, device): """Handle device joined. diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 46f7dd0e031..571741da7c3 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -29,7 +29,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation cover from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/device_tracker.py b/homeassistant/components/zha/device_tracker.py index 5fe1dbc0060..2a53fc3bf3c 100644 --- a/homeassistant/components/zha/device_tracker.py +++ b/homeassistant/components/zha/device_tracker.py @@ -26,7 +26,7 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation device tracker from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 234566267f6..d04453cd675 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -53,7 +53,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation fan from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 15ea8c0340b..bf3a457ff68 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -51,7 +51,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" - entities_to_create = hass.data[DATA_ZHA][light.DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][light.DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 5c0d54430e0..ba802120044 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -36,7 +36,7 @@ VALUE_TO_STATE = dict(enumerate(STATE_LIST)) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation Door Lock from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index 8182fdcabcf..5e2e8bf4a0d 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -68,7 +68,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation sensor from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 156183ce95d..6be3a9b3347 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -26,7 +26,7 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation switch from config entry.""" - entities_to_create = hass.data[DATA_ZHA][DOMAIN] = [] + entities_to_create = hass.data[DATA_ZHA][DOMAIN] unsub = async_dispatcher_connect( hass, From 014870861317b571c6dc63e836341e99829e1482 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 21 Mar 2020 13:17:06 -0500 Subject: [PATCH 171/431] Add DEVICE_CLASS_GATE (#33074) Gates are found outside of a structure and are typically part of a fence. --- homeassistant/components/cover/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index abefd3263bc..e63054d23b2 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -46,9 +46,11 @@ 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, @@ -56,6 +58,7 @@ DEVICE_CLASSES = [ DEVICE_CLASS_DAMPER, DEVICE_CLASS_DOOR, DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, DEVICE_CLASS_SHADE, DEVICE_CLASS_SHUTTER, DEVICE_CLASS_WINDOW, From a7b08c48f3610856cbdc1e45c5f56c8685ab5543 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Mar 2020 19:25:29 +0100 Subject: [PATCH 172/431] Upgrade spotipy to 2.10.0 (#33083) --- 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 be58d2bab40..f246c657708 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.7.1"], + "requirements": ["spotipy==2.10.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/requirements_all.txt b/requirements_all.txt index bcb6fbb5988..602fa0a59bf 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1921,7 +1921,7 @@ spiderpy==1.3.1 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.7.1 +spotipy==2.10.0 # homeassistant.components.recorder # homeassistant.components.sql diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 65d400b4a3b..4b3aa5878a2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -688,7 +688,7 @@ somecomfort==0.5.2 speak2mary==1.4.0 # homeassistant.components.spotify -spotipy==2.7.1 +spotipy==2.10.0 # homeassistant.components.recorder # homeassistant.components.sql From 5893f6b14b0098c41fdef05e00eef0d81bb5bb81 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Mar 2020 19:36:35 +0100 Subject: [PATCH 173/431] Fix Extend ONVIF unique ID with profile index (#33103) --- homeassistant/components/onvif/camera.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index ce241f779b1..cb518d6c5ee 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -505,4 +505,6 @@ class ONVIFHassCamera(Camera): @property def unique_id(self) -> Optional[str]: """Return a unique ID.""" + if self._profile_index: + return f"{self._mac}_{self._profile_index}" return self._mac From 699ca44260f6c790f69976d5a3d8728ce160b122 Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 21 Mar 2020 20:17:00 +0100 Subject: [PATCH 174/431] Clean up Sonos attributes for radio streams (#33063) --- CODEOWNERS | 1 + homeassistant/components/sonos/manifest.json | 4 +- .../components/sonos/media_player.py | 122 +++++------------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 34 insertions(+), 97 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 9adf5110b4f..f0efe63ada2 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -336,6 +336,7 @@ homeassistant/components/solax/* @squishykid homeassistant/components/soma/* @ratsept homeassistant/components/somfy/* @tetienne homeassistant/components/songpal/* @rytilahti +homeassistant/components/sonos/* @amelchio homeassistant/components/spaceapi/* @fabaff homeassistant/components/speedtestdotnet/* @rohankapoorcom homeassistant/components/spider/* @peternijssen diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index 74a8dea06d4..a015e7a5095 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,12 +3,12 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.24"], + "requirements": ["pysonos==0.0.25"], "dependencies": [], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" } ], - "codeowners": [] + "codeowners": ["@amelchio"] } diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 8828c27e9c7..46e3765ad44 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -4,12 +4,12 @@ import datetime import functools as ft import logging import socket -import urllib import async_timeout import pysonos from pysonos import alarms from pysonos.exceptions import SoCoException, SoCoUPnPException +import pysonos.music_library import pysonos.snapshot import voluptuous as vol @@ -338,19 +338,6 @@ def _timespan_secs(timespan): return sum(60 ** x[0] * int(x[1]) for x in enumerate(reversed(timespan.split(":")))) -def _is_radio_uri(uri): - """Return whether the URI is a stream (not a playlist).""" - radio_schemes = ( - "x-rincon-mp3radio:", - "x-sonosapi-stream:", - "x-sonosapi-radio:", - "x-sonosapi-hls:", - "hls-radio:", - "x-rincon-stream:", - ) - return uri.startswith(radio_schemes) - - class SonosEntity(MediaPlayerDevice): """Representation of a Sonos entity.""" @@ -515,17 +502,6 @@ class SonosEntity(MediaPlayerDevice): # Skip unknown types _LOGGER.error("Unhandled favorite '%s': %s", fav.title, ex) - def _radio_artwork(self, url): - """Return the private URL with artwork for a radio stream.""" - if url in UNAVAILABLE_VALUES: - return None - - if url.find("tts_proxy") > 0: - # If the content is a tts don't try to fetch an image from it. - return None - - return f"http://{self.soco.ip_address}:1400/getaa?s=1&u={urllib.parse.quote(url, safe='')}" - def _attach_player(self): """Get basic information and add event subscriptions.""" try: @@ -576,6 +552,14 @@ class SonosEntity(MediaPlayerDevice): self._shuffle = self.soco.shuffle self._uri = None + self._media_duration = None + self._media_position = None + self._media_position_updated_at = None + self._media_image_url = None + self._media_artist = None + self._media_album_name = None + self._media_title = None + self._source_name = None update_position = new_status != self._status self._status = new_status @@ -587,8 +571,11 @@ class SonosEntity(MediaPlayerDevice): else: track_info = self.soco.get_current_track_info() self._uri = track_info["uri"] + self._media_artist = track_info.get("artist") + self._media_album_name = track_info.get("album") + self._media_title = track_info.get("title") - if _is_radio_uri(track_info["uri"]): + if self.soco.is_radio_uri(track_info["uri"]): variables = event and event.variables self.update_media_radio(variables, track_info) else: @@ -604,74 +591,29 @@ class SonosEntity(MediaPlayerDevice): def update_media_linein(self, source): """Update state when playing from line-in/tv.""" - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None - - self._media_image_url = None - - self._media_artist = None - self._media_album_name = None self._media_title = source - self._source_name = source def update_media_radio(self, variables, track_info): """Update state when streaming radio.""" - self._media_duration = None - self._media_position = None - self._media_position_updated_at = None + try: + library = pysonos.music_library.MusicLibrary(self.soco) + album_art_uri = variables["current_track_meta_data"].album_art_uri + self._media_image_url = library.build_album_art_full_uri(album_art_uri) + except (TypeError, KeyError, AttributeError): + pass - media_info = self.soco.avTransport.GetMediaInfo([("InstanceID", 0)]) - self._media_image_url = self._radio_artwork(media_info["CurrentURI"]) - - self._media_artist = track_info.get("artist") - self._media_album_name = None - self._media_title = track_info.get("title") - - if self._media_artist and self._media_title: - # artist and album name are in the data, concatenate - # that do display as artist. - # "Information" field in the sonos pc app - self._media_artist = "{artist} - {title}".format( - artist=self._media_artist, title=self._media_title - ) - elif variables: - # "On Now" field in the sonos pc app - current_track_metadata = variables.get("current_track_meta_data") - if current_track_metadata: - self._media_artist = current_track_metadata.radio_show.split(",")[0] - - # For radio streams we set the radio station name as the title. - current_uri_metadata = media_info["CurrentURIMetaData"] - if current_uri_metadata not in UNAVAILABLE_VALUES: - # currently soco does not have an API for this - current_uri_metadata = pysonos.xml.XML.fromstring( - pysonos.utils.really_utf8(current_uri_metadata) - ) - - md_title = current_uri_metadata.findtext( - ".//{http://purl.org/dc/elements/1.1/}title" - ) - - if md_title not in UNAVAILABLE_VALUES: - self._media_title = md_title - - if self._media_artist and self._media_title: - # some radio stations put their name into the artist - # name, e.g.: - # media_title = "Station" - # media_artist = "Station - Artist - Title" - # detect this case and trim from the front of - # media_artist for cosmetics - trim = f"{self._media_title} - " - chars = min(len(self._media_artist), len(trim)) - - if self._media_artist[:chars].upper() == trim[:chars].upper(): - self._media_artist = self._media_artist[chars:] + # Radios without tagging can have the radio URI as title. Non-playing + # radios will not have a current title. In these cases we try to use + # the radio name instead. + try: + if self.soco.is_radio_uri(self._media_title) or self.state != STATE_PLAYING: + self._media_title = variables["enqueued_transport_uri_meta_data"].title + except (TypeError, KeyError, AttributeError): + pass # Check if currently playing radio station is in favorites - self._source_name = None + media_info = self.soco.avTransport.GetMediaInfo([("InstanceID", 0)]) for fav in self._favorites: if fav.reference.get_uri() == media_info["CurrentURI"]: self._source_name = fav.title @@ -710,12 +652,6 @@ class SonosEntity(MediaPlayerDevice): self._media_image_url = track_info.get("album_art") - self._media_artist = track_info.get("artist") - self._media_album_name = track_info.get("album") - self._media_title = track_info.get("title") - - self._source_name = None - def update_volume(self, event=None): """Update information about currently volume settings.""" if event: @@ -936,7 +872,7 @@ class SonosEntity(MediaPlayerDevice): if len(fav) == 1: src = fav.pop() uri = src.reference.get_uri() - if _is_radio_uri(uri): + if self.soco.is_radio_uri(uri): self.soco.play_uri(uri, title=source) else: self.soco.clear_queue() diff --git a/requirements_all.txt b/requirements_all.txt index 602fa0a59bf..8cde5e26c92 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1543,7 +1543,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.24 +pysonos==0.0.25 # homeassistant.components.spc pyspcwebgw==0.4.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4b3aa5878a2..956d88402c1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -583,7 +583,7 @@ pysmartthings==0.7.0 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.24 +pysonos==0.0.25 # homeassistant.components.spc pyspcwebgw==0.4.0 From 8272c71811ba48dc076f4de7412b24c226d31c9f Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Mar 2020 20:24:23 +0100 Subject: [PATCH 175/431] Handle query and anchors in Spotify URI's (#33084) * Handle query and anchors in Spotify URI's * Use yarl for cleaning up the URL --- homeassistant/components/spotify/media_player.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/homeassistant/components/spotify/media_player.py b/homeassistant/components/spotify/media_player.py index 9588f428a66..7a00fb02146 100644 --- a/homeassistant/components/spotify/media_player.py +++ b/homeassistant/components/spotify/media_player.py @@ -7,6 +7,7 @@ from typing import Any, Callable, Dict, List, Optional from aiohttp import ClientError from spotipy import Spotify, SpotifyException +from yarl import URL from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( @@ -295,6 +296,10 @@ class SpotifyMediaPlayer(MediaPlayerDevice): """Play media.""" kwargs = {} + # Spotify can't handle URI's with query strings or anchors + # Yet, they do generate those types of URI in their official clients. + media_id = str(URL(media_id).with_query(None).with_fragment(None)) + if media_type == MEDIA_TYPE_MUSIC: kwargs["uris"] = [media_id] elif media_type == MEDIA_TYPE_PLAYLIST: From 403b4a2e0beb6f766af3bfb68c2b4e9d9ea534f6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sat, 21 Mar 2020 16:01:39 -0600 Subject: [PATCH 176/431] Bump simplisafe-python to 9.0.5 (#33116) --- 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 1d010c67692..917722a61b8 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==9.0.4"], + "requirements": ["simplisafe-python==9.0.5"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 8cde5e26c92..20c62a42136 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1853,7 +1853,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.0.4 +simplisafe-python==9.0.5 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 956d88402c1..f0df372cde9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -670,7 +670,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.0.4 +simplisafe-python==9.0.5 # homeassistant.components.sleepiq sleepyq==0.7 From 79f6d55fe8020c1ef8ac327593844e137b328b3a Mon Sep 17 00:00:00 2001 From: guillempages Date: Sat, 21 Mar 2020 23:12:14 +0100 Subject: [PATCH 177/431] Fix tankerkoenig with more than 10 stations (#33098) * Fix tankerkoenig with more than 10 stations There seemed to be a problem, if more than 10 fuel stations were tracked. Added a warning in this case, and split the calls to the API in chunks of 10, so that the data can be fetched anyway. * Update homeassistant/components/tankerkoenig/__init__.py Co-Authored-By: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../components/tankerkoenig/__init__.py | 35 +++++++++++++------ 1 file changed, 25 insertions(+), 10 deletions(-) diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index aa8c6f87dc8..3f654f24522 100755 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -1,6 +1,7 @@ """Ask tankerkoenig.de for petrol price information.""" from datetime import timedelta import logging +from math import ceil from uuid import UUID import pytankerkoenig @@ -180,27 +181,41 @@ class TankerkoenigData: ) return False self.add_station(additional_station_data["station"]) + if len(self.stations) > 10: + _LOGGER.warning( + "Found more than 10 stations to check. " + "This might invalidate your api-key on the long run. " + "Try using a smaller radius" + ) return True async def fetch_data(self): """Get the latest data from tankerkoenig.de.""" _LOGGER.debug("Fetching new data from tankerkoenig.de") station_ids = list(self.stations) - data = await self._hass.async_add_executor_job( - pytankerkoenig.getPriceList, self._api_key, station_ids - ) - if data["ok"]: + prices = {} + + # The API seems to only return at most 10 results, so split the list in chunks of 10 + # and merge it together. + for index in range(ceil(len(station_ids) / 10)): + data = await self._hass.async_add_executor_job( + pytankerkoenig.getPriceList, + self._api_key, + station_ids[index * 10 : (index + 1) * 10], + ) + _LOGGER.debug("Received data: %s", data) + if not data["ok"]: + _LOGGER.error( + "Error fetching data from tankerkoenig.de: %s", data["message"] + ) + raise TankerkoenigError(data["message"]) if "prices" not in data: _LOGGER.error("Did not receive price information from tankerkoenig.de") raise TankerkoenigError("No prices in data") - else: - _LOGGER.error( - "Error fetching data from tankerkoenig.de: %s", data["message"] - ) - raise TankerkoenigError(data["message"]) - return data["prices"] + prices.update(data["prices"]) + return prices def add_station(self, station: dict): """Add fuel station to the entity list.""" From ba2558790d9f8ae4db38708b3f4c0999b15c4055 Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 21 Mar 2020 15:14:19 -0700 Subject: [PATCH 178/431] Fix totalconnect AttributeError introduced in 0.107 (#33079) * Fit git * Remove period from loggging message * Remove tests for now * Add const.py * Fix lint error --- .../totalconnect/alarm_control_panel.py | 41 +++++++------------ .../components/totalconnect/const.py | 3 ++ 2 files changed, 18 insertions(+), 26 deletions(-) create mode 100644 homeassistant/components/totalconnect/const.py diff --git a/homeassistant/components/totalconnect/alarm_control_panel.py b/homeassistant/components/totalconnect/alarm_control_panel.py index b255132a365..2ab06e2f6bd 100644 --- a/homeassistant/components/totalconnect/alarm_control_panel.py +++ b/homeassistant/components/totalconnect/alarm_control_panel.py @@ -18,7 +18,7 @@ from homeassistant.const import ( STATE_ALARM_TRIGGERED, ) -from . import DOMAIN as TOTALCONNECT_DOMAIN +from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -30,7 +30,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): alarms = [] - client = hass.data[TOTALCONNECT_DOMAIN].client + client = hass.data[DOMAIN].client for location_id, location in client.locations.items(): location_name = location.location_name @@ -71,7 +71,7 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): def update(self): """Return the state of the device.""" - status = self._client.get_armed_status(self._location_id) + self._client.get_armed_status(self._location_id) attr = { "location_name": self._name, "location_id": self._location_id, @@ -79,47 +79,36 @@ class TotalConnectAlarm(alarm.AlarmControlPanel): "low_battery": self._client.locations[self._location_id].low_battery, "cover_tampered": self._client.locations[ self._location_id - ].is_cover_tampered, + ].is_cover_tampered(), "triggered_source": None, "triggered_zone": None, } - if status in (self._client.DISARMED, self._client.DISARMED_BYPASS): + if self._client.locations[self._location_id].is_disarmed(): state = STATE_ALARM_DISARMED - elif status in ( - self._client.ARMED_STAY, - self._client.ARMED_STAY_INSTANT, - self._client.ARMED_STAY_INSTANT_BYPASS, - ): + elif self._client.locations[self._location_id].is_armed_home(): state = STATE_ALARM_ARMED_HOME - elif status == self._client.ARMED_STAY_NIGHT: + elif self._client.locations[self._location_id].is_armed_night(): state = STATE_ALARM_ARMED_NIGHT - elif status in ( - self._client.ARMED_AWAY, - self._client.ARMED_AWAY_BYPASS, - self._client.ARMED_AWAY_INSTANT, - self._client.ARMED_AWAY_INSTANT_BYPASS, - ): + elif self._client.locations[self._location_id].is_armed_away(): state = STATE_ALARM_ARMED_AWAY - elif status == self._client.ARMED_CUSTOM_BYPASS: + elif self._client.locations[self._location_id].is_armed_custom_bypass(): state = STATE_ALARM_ARMED_CUSTOM_BYPASS - elif status == self._client.ARMING: + elif self._client.locations[self._location_id].is_arming(): state = STATE_ALARM_ARMING - elif status == self._client.DISARMING: + elif self._client.locations[self._location_id].is_disarming(): state = STATE_ALARM_DISARMING - elif status == self._client.ALARMING: + elif self._client.locations[self._location_id].is_triggered_police(): state = STATE_ALARM_TRIGGERED attr["triggered_source"] = "Police/Medical" - elif status == self._client.ALARMING_FIRE_SMOKE: + elif self._client.locations[self._location_id].is_triggered_fire(): state = STATE_ALARM_TRIGGERED attr["triggered_source"] = "Fire/Smoke" - elif status == self._client.ALARMING_CARBON_MONOXIDE: + elif self._client.locations[self._location_id].is_triggered_gas(): state = STATE_ALARM_TRIGGERED attr["triggered_source"] = "Carbon Monoxide" else: - logging.info( - "Total Connect Client returned unknown status code: %s", status - ) + logging.info("Total Connect Client returned unknown status") state = None self._state = state diff --git a/homeassistant/components/totalconnect/const.py b/homeassistant/components/totalconnect/const.py new file mode 100644 index 00000000000..6c19bf0a217 --- /dev/null +++ b/homeassistant/components/totalconnect/const.py @@ -0,0 +1,3 @@ +"""TotalConnect constants.""" + +DOMAIN = "totalconnect" From 6d4fa761071cd76456093d786bcc31f8d635f901 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 21 Mar 2020 23:14:41 +0100 Subject: [PATCH 179/431] Add Wi-Fi sensors to WLED integration (#33055) --- homeassistant/components/wled/const.py | 1 + homeassistant/components/wled/sensor.py | 100 +++++++++++++++++++++- tests/components/wled/test_sensor.py | 107 +++++++++++++++++++++--- tests/fixtures/wled/rgb.json | 6 ++ tests/fixtures/wled/rgbw.json | 6 ++ 5 files changed, 206 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 4844f37a126..6c5cd9eaad4 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -25,3 +25,4 @@ ATTR_UDP_PORT = "udp_port" # Units of measurement CURRENT_MA = "mA" +SIGNAL_DBM = "dBm" diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index e0d92aecd56..b684bdc0977 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -4,13 +4,18 @@ import logging from typing import Any, Callable, Dict, List, Optional, Union from homeassistant.config_entries import ConfigEntry -from homeassistant.const import DATA_BYTES, DEVICE_CLASS_TIMESTAMP +from homeassistant.const import ( + DATA_BYTES, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TIMESTAMP, + UNIT_PERCENTAGE, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util.dt import utcnow from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity -from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN +from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN, SIGNAL_DBM _LOGGER = logging.getLogger(__name__) @@ -27,6 +32,10 @@ async def async_setup_entry( WLEDEstimatedCurrentSensor(entry.entry_id, coordinator), WLEDUptimeSensor(entry.entry_id, coordinator), WLEDFreeHeapSensor(entry.entry_id, coordinator), + WLEDWifiBSSIDSensor(entry.entry_id, coordinator), + WLEDWifiChannelSensor(entry.entry_id, coordinator), + WLEDWifiRSSISensor(entry.entry_id, coordinator), + WLEDWifiSignalSensor(entry.entry_id, coordinator), ] async_add_entities(sensors, True) @@ -142,3 +151,90 @@ class WLEDFreeHeapSensor(WLEDSensor): def state(self) -> Union[None, str, int, float]: """Return the state of the sensor.""" return self.coordinator.data.info.free_heap + + +class WLEDWifiSignalSensor(WLEDSensor): + """Defines a WLED Wi-Fi signal sensor.""" + + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED Wi-Fi signal sensor.""" + super().__init__( + coordinator=coordinator, + enabled_default=False, + entry_id=entry_id, + icon="mdi:wifi", + key="wifi_signal", + name=f"{coordinator.data.info.name} Wi-Fi Signal", + unit_of_measurement=UNIT_PERCENTAGE, + ) + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.info.wifi.signal + + +class WLEDWifiRSSISensor(WLEDSensor): + """Defines a WLED Wi-Fi RSSI sensor.""" + + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED Wi-Fi RSSI sensor.""" + super().__init__( + coordinator=coordinator, + enabled_default=False, + entry_id=entry_id, + icon="mdi:wifi", + key="wifi_rssi", + name=f"{coordinator.data.info.name} Wi-Fi RSSI", + unit_of_measurement=SIGNAL_DBM, + ) + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.info.wifi.rssi + + @property + def device_class(self) -> Optional[str]: + """Return the class of this sensor.""" + return DEVICE_CLASS_SIGNAL_STRENGTH + + +class WLEDWifiChannelSensor(WLEDSensor): + """Defines a WLED Wi-Fi Channel sensor.""" + + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED Wi-Fi Channel sensor.""" + super().__init__( + coordinator=coordinator, + enabled_default=False, + entry_id=entry_id, + icon="mdi:wifi", + key="wifi_channel", + name=f"{coordinator.data.info.name} Wi-Fi Channel", + ) + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.info.wifi.channel + + +class WLEDWifiBSSIDSensor(WLEDSensor): + """Defines a WLED Wi-Fi BSSID sensor.""" + + def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + """Initialize WLED Wi-Fi BSSID sensor.""" + super().__init__( + coordinator=coordinator, + enabled_default=False, + entry_id=entry_id, + icon="mdi:wifi", + key="wifi_bssid", + name=f"{coordinator.data.info.name} Wi-Fi BSSID", + ) + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.info.wifi.bssid diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index 894968f5db4..d77bd99b97c 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -2,6 +2,7 @@ from datetime import datetime from asynctest import patch +import pytest from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.wled.const import ( @@ -9,8 +10,14 @@ from homeassistant.components.wled.const import ( ATTR_MAX_POWER, CURRENT_MA, DOMAIN, + SIGNAL_DBM, +) +from homeassistant.const import ( + ATTR_ICON, + ATTR_UNIT_OF_MEASUREMENT, + DATA_BYTES, + UNIT_PERCENTAGE, ) -from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, DATA_BYTES from homeassistant.core import HomeAssistant from homeassistant.util import dt as dt_util @@ -43,6 +50,38 @@ async def test_sensors( disabled_by=None, ) + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aabbccddeeff_wifi_signal", + suggested_object_id="wled_rgb_light_wifi_signal", + disabled_by=None, + ) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aabbccddeeff_wifi_rssi", + suggested_object_id="wled_rgb_light_wifi_rssi", + disabled_by=None, + ) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aabbccddeeff_wifi_channel", + suggested_object_id="wled_rgb_light_wifi_channel", + disabled_by=None, + ) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aabbccddeeff_wifi_bssid", + suggested_object_id="wled_rgb_light_wifi_bssid", + disabled_by=None, + ) + # Setup test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) with patch("homeassistant.components.wled.sensor.utcnow", return_value=test_time): @@ -81,26 +120,70 @@ async def test_sensors( assert entry assert entry.unique_id == "aabbccddeeff_free_heap" + state = hass.states.get("sensor.wled_rgb_light_wifi_signal") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == UNIT_PERCENTAGE + assert state.state == "76" + entry = registry.async_get("sensor.wled_rgb_light_wifi_signal") + assert entry + assert entry.unique_id == "aabbccddeeff_wifi_signal" + + state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_DBM + assert state.state == "-62" + + entry = registry.async_get("sensor.wled_rgb_light_wifi_rssi") + assert entry + assert entry.unique_id == "aabbccddeeff_wifi_rssi" + + state = hass.states.get("sensor.wled_rgb_light_wifi_channel") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "11" + + entry = registry.async_get("sensor.wled_rgb_light_wifi_channel") + assert entry + assert entry.unique_id == "aabbccddeeff_wifi_channel" + + state = hass.states.get("sensor.wled_rgb_light_wifi_bssid") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "AA:AA:AA:AA:AA:BB" + + entry = registry.async_get("sensor.wled_rgb_light_wifi_bssid") + assert entry + assert entry.unique_id == "aabbccddeeff_wifi_bssid" + + +@pytest.mark.parametrize( + "entity_id", + ( + "sensor.wled_rgb_light_uptime", + "sensor.wled_rgb_light_free_memory", + "sensor.wled_rgb_light_wi_fi_signal", + "sensor.wled_rgb_light_wi_fi_rssi", + "sensor.wled_rgb_light_wi_fi_channel", + "sensor.wled_rgb_light_wi_fi_bssid", + ), +) async def test_disabled_by_default_sensors( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, entity_id: str ) -> None: """Test the disabled by default WLED sensors.""" await init_integration(hass, aioclient_mock) registry = await hass.helpers.entity_registry.async_get_registry() + print(registry.entities) - state = hass.states.get("sensor.wled_rgb_light_uptime") + state = hass.states.get(entity_id) assert state is None - entry = registry.async_get("sensor.wled_rgb_light_uptime") - assert entry - assert entry.disabled - assert entry.disabled_by == "integration" - - state = hass.states.get("sensor.wled_rgb_light_free_memory") - assert state is None - - entry = registry.async_get("sensor.wled_rgb_light_free_memory") + entry = registry.async_get(entity_id) assert entry assert entry.disabled assert entry.disabled_by == "integration" diff --git a/tests/fixtures/wled/rgb.json b/tests/fixtures/wled/rgb.json index 70a54f06644..41d2c69d63c 100644 --- a/tests/fixtures/wled/rgb.json +++ b/tests/fixtures/wled/rgb.json @@ -62,6 +62,12 @@ "live": false, "fxcount": 81, "palcount": 50, + "wifi": { + "bssid": "AA:AA:AA:AA:AA:BB", + "rssi": -62, + "signal": 76, + "channel": 11 + }, "arch": "esp8266", "core": "2_4_2", "freeheap": 14600, diff --git a/tests/fixtures/wled/rgbw.json b/tests/fixtures/wled/rgbw.json index 0d51dfedd2d..ce7033c5888 100644 --- a/tests/fixtures/wled/rgbw.json +++ b/tests/fixtures/wled/rgbw.json @@ -48,6 +48,12 @@ "live": false, "fxcount": 83, "palcount": 50, + "wifi": { + "bssid": "AA:AA:AA:AA:AA:BB", + "rssi": -62, + "signal": 76, + "channel": 11 + }, "arch": "esp8266", "core": "2_5_2", "freeheap": 20136, From d5e606640cbee87a8c6a282423f9657b602e57a3 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Sun, 22 Mar 2020 00:57:12 +0100 Subject: [PATCH 180/431] Fix scheduled update time-drift in data update coordinator (#32974) * Fix scheduled update time-drift in data update coordinator As next schedule is calculated **after** the update is done, setting now + update_interval makes 1 second drift in practice, as the tick is 1Hz. * Floor the utcnow timestamp to remove sub-second error precision, and generate constant frequency updates as long as the update takes < 1s. --- homeassistant/helpers/update_coordinator.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 85db657c441..b2b04816616 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -88,8 +88,14 @@ class DataUpdateCoordinator: self._unsub_refresh() self._unsub_refresh = None + # We _floor_ utcnow to create a schedule on a rounded second, + # minimizing the time between the point and the real activation. + # That way we obtain a constant update frequency, + # as long as the update process takes less than a second self._unsub_refresh = async_track_point_in_utc_time( - self.hass, self._handle_refresh_interval, utcnow() + self.update_interval + self.hass, + self._handle_refresh_interval, + utcnow().replace(microsecond=0) + self.update_interval, ) async def _handle_refresh_interval(self, _now: datetime) -> None: From 991afccbd4b8c9396b790eb3998fc355b8ddf43f Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 21 Mar 2020 19:59:32 -0400 Subject: [PATCH 181/431] Refactor ZHA EventChannels (#32709) * Refactor EventChannels. * Rename EventRelayChannel. * Fully rename EventRelayChannel. - update docstrings - rename all references from "relay" to "client" * Remove CHANNEL_NAME. * Update stale docstring. --- .../components/zha/core/channels/__init__.py | 28 +++++++++---------- .../components/zha/core/channels/base.py | 10 ++----- .../components/zha/core/channels/general.py | 26 ++++++++++++++--- .../components/zha/core/channels/lighting.py | 10 +++++-- .../components/zha/core/registries.py | 2 +- homeassistant/components/zha/core/typing.py | 4 +-- tests/components/zha/test_channels.py | 14 ++++++---- tests/components/zha/test_device_action.py | 2 +- tests/components/zha/test_device_trigger.py | 2 +- tests/components/zha/test_discover.py | 4 +-- 10 files changed, 62 insertions(+), 40 deletions(-) diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index a4848fbaa63..91a23e17f12 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -163,12 +163,12 @@ class ChannelPool: self._channels: Channels = channels self._claimed_channels: ChannelsDict = {} self._id: int = ep_id - self._relay_channels: Dict[str, zha_typing.EventRelayChannelType] = {} + self._client_channels: Dict[str, zha_typing.ClientChannelType] = {} self._unique_id: str = f"{channels.unique_id}-{ep_id}" @property def all_channels(self) -> ChannelsDict: - """All channels of an endpoint.""" + """All server channels of an endpoint.""" return self._all_channels @property @@ -176,6 +176,11 @@ class ChannelPool: """Channels in use.""" return self._claimed_channels + @property + def client_channels(self) -> Dict[str, zha_typing.ClientChannelType]: + """Return a dict of client channels.""" + return self._client_channels + @property def endpoint(self) -> zha_typing.ZigpyEndpointType: """Return endpoint of zigpy device.""" @@ -216,11 +221,6 @@ class ChannelPool: """Return device model.""" return self._channels.zha_device.model - @property - def relay_channels(self) -> Dict[str, zha_typing.EventRelayChannelType]: - """Return a dict of event relay channels.""" - return self._relay_channels - @property def skip_configuration(self) -> bool: """Return True if device does not require channel configuration.""" @@ -236,7 +236,7 @@ class ChannelPool: """Create new channels for an endpoint.""" pool = cls(channels, ep_id) pool.add_all_channels() - pool.add_relay_channels() + pool.add_client_channels() zha_disc.PROBE.discover_entities(pool) return pool @@ -270,13 +270,13 @@ class ChannelPool: self.all_channels[channel.id] = channel @callback - def add_relay_channels(self) -> None: - """Create relay channels for all output clusters if in the registry.""" - for cluster_id in zha_regs.EVENT_RELAY_CLUSTERS: + def add_client_channels(self) -> None: + """Create client channels for all output clusters if in the registry.""" + for cluster_id, channel_class in zha_regs.CLIENT_CHANNELS_REGISTRY.items(): cluster = self.endpoint.out_clusters.get(cluster_id) if cluster is not None: - channel = base.EventRelayChannel(cluster, self) - self.relay_channels[channel.id] = channel + channel = channel_class(cluster, self) + self.client_channels[channel.id] = channel async def async_initialize(self, from_cache: bool = False) -> None: """Initialize claimed channels.""" @@ -293,7 +293,7 @@ class ChannelPool: async with self._channels.semaphore: return await coro - channels = [*self.claimed_channels.values(), *self.relay_channels.values()] + channels = [*self.claimed_channels.values(), *self.client_channels.values()] tasks = [_throttle(getattr(ch, func_name)(*args)) for ch in channels] results = await asyncio.gather(*tasks, return_exceptions=True) for channel, outcome in zip(channels, results): diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index dfe564ec2c1..e718e688c50 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -19,7 +19,6 @@ from ..const import ( ATTR_COMMAND, ATTR_UNIQUE_ID, ATTR_VALUE, - CHANNEL_EVENT_RELAY, CHANNEL_ZDO, REPORT_CONFIG_MAX_INT, REPORT_CONFIG_MIN_INT, @@ -78,7 +77,6 @@ class ChannelStatus(Enum): class ZigbeeChannel(LogMixin): """Base channel for a Zigbee cluster.""" - CHANNEL_NAME = None REPORT_CONFIG = () def __init__( @@ -87,8 +85,6 @@ class ZigbeeChannel(LogMixin): """Initialize ZigbeeChannel.""" self._generic_id = f"channel_0x{cluster.cluster_id:04x}" self._channel_name = getattr(cluster, "ep_attribute", self._generic_id) - if self.CHANNEL_NAME: - self._channel_name = self.CHANNEL_NAME self._ch_pool = ch_pool self._cluster = cluster self._id = f"{ch_pool.id}:0x{cluster.cluster_id:04x}" @@ -361,10 +357,8 @@ class ZDOChannel(LogMixin): _LOGGER.log(level, msg, *args) -class EventRelayChannel(ZigbeeChannel): - """Event relay that can be attached to zigbee clusters.""" - - CHANNEL_NAME = CHANNEL_EVENT_RELAY +class ClientChannel(ZigbeeChannel): + """Channel listener for Zigbee client (output) clusters.""" @callback def attribute_updated(self, attrid, value): diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index 783188248f3..ffd5a03fc13 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -20,7 +20,7 @@ from ..const import ( SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, ) -from .base import ZigbeeChannel, parse_and_log_command +from .base import ClientChannel, ZigbeeChannel, parse_and_log_command _LOGGER = logging.getLogger(__name__) @@ -166,8 +166,14 @@ class Identify(ZigbeeChannel): self.async_send_signal(f"{self.unique_id}_{cmd}", args[0]) +@registries.CLIENT_CHANNELS_REGISTRY.register(general.LevelControl.cluster_id) +class LevelControlClientChannel(ClientChannel): + """LevelControl client cluster.""" + + pass + + @registries.BINDABLE_CLUSTERS.register(general.LevelControl.cluster_id) -@registries.EVENT_RELAY_CLUSTERS.register(general.LevelControl.cluster_id) @registries.LIGHT_CLUSTERS.register(general.LevelControl.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.LevelControl.cluster_id) class LevelControlChannel(ZigbeeChannel): @@ -233,9 +239,15 @@ class MultistateValue(ZigbeeChannel): REPORT_CONFIG = [{"attr": "present_value", "config": REPORT_CONFIG_DEFAULT}] +@registries.CLIENT_CHANNELS_REGISTRY.register(general.OnOff.cluster_id) +class OnOffClientChannel(ClientChannel): + """OnOff client channel.""" + + pass + + @registries.BINARY_SENSOR_CLUSTERS.register(general.OnOff.cluster_id) @registries.BINDABLE_CLUSTERS.register(general.OnOff.cluster_id) -@registries.EVENT_RELAY_CLUSTERS.register(general.OnOff.cluster_id) @registries.LIGHT_CLUSTERS.register(general.OnOff.cluster_id) @registries.SWITCH_CLUSTERS.register(general.OnOff.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.OnOff.cluster_id) @@ -437,7 +449,13 @@ class RSSILocation(ZigbeeChannel): pass -@registries.EVENT_RELAY_CLUSTERS.register(general.Scenes.cluster_id) +@registries.CLIENT_CHANNELS_REGISTRY.register(general.Scenes.cluster_id) +class ScenesClientChannel(ClientChannel): + """Scenes channel.""" + + pass + + @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Scenes.cluster_id) class Scenes(ZigbeeChannel): """Scenes channel.""" diff --git a/homeassistant/components/zha/core/channels/lighting.py b/homeassistant/components/zha/core/channels/lighting.py index 7dc98d04515..25f6c05d739 100644 --- a/homeassistant/components/zha/core/channels/lighting.py +++ b/homeassistant/components/zha/core/channels/lighting.py @@ -5,7 +5,7 @@ import zigpy.zcl.clusters.lighting as lighting from .. import registries, typing as zha_typing from ..const import REPORT_CONFIG_DEFAULT -from .base import ZigbeeChannel +from .base import ClientChannel, ZigbeeChannel _LOGGER = logging.getLogger(__name__) @@ -17,8 +17,14 @@ class Ballast(ZigbeeChannel): pass +@registries.CLIENT_CHANNELS_REGISTRY.register(lighting.Color.cluster_id) +class ColorClientChannel(ClientChannel): + """Color client channel.""" + + pass + + @registries.BINDABLE_CLUSTERS.register(lighting.Color.cluster_id) -@registries.EVENT_RELAY_CLUSTERS.register(lighting.Color.cluster_id) @registries.LIGHT_CLUSTERS.register(lighting.Color.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(lighting.Color.cluster_id) class ColorChannel(ZigbeeChannel): diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 9aeb7832f63..0a1a81df5ff 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -123,9 +123,9 @@ DEVICE_CLASS = { DEVICE_CLASS = collections.defaultdict(dict, DEVICE_CLASS) DEVICE_TRACKER_CLUSTERS = SetRegistry() -EVENT_RELAY_CLUSTERS = SetRegistry() LIGHT_CLUSTERS = SetRegistry() OUTPUT_CHANNEL_ONLY_CLUSTERS = SetRegistry() +CLIENT_CHANNELS_REGISTRY = DictRegistry() RADIO_TYPES = { RadioType.deconz.name: { diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index fb397ea15ae..a1cbc9f0fef 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -12,7 +12,7 @@ CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable) ChannelType = "ZigbeeChannel" ChannelsType = "Channels" ChannelPoolType = "ChannelPool" -EventRelayChannelType = "EventRelayChannel" +ClientChannelType = "ClientChannel" ZDOChannelType = "ZDOChannel" ZhaDeviceType = "ZHADevice" ZhaEntityType = "ZHAEntity" @@ -33,7 +33,7 @@ if TYPE_CHECKING: ChannelType = base_channels.ZigbeeChannel ChannelsType = channels.Channels ChannelPoolType = channels.ChannelPool - EventRelayChannelType = base_channels.EventRelayChannel + ClientChannelType = base_channels.ClientChannel ZDOChannelType = base_channels.ZDOChannel ZhaDeviceType = homeassistant.components.zha.core.device.ZHADevice ZhaEntityType = homeassistant.components.zha.entity.ZhaEntity diff --git a/tests/components/zha/test_channels.py b/tests/components/zha/test_channels.py index ec9c172430c..1196cdc3b40 100644 --- a/tests/components/zha/test_channels.py +++ b/tests/components/zha/test_channels.py @@ -274,7 +274,9 @@ def test_epch_claim_channels(channel): assert "1:0x0300" in ep_channels.claimed_channels -@mock.patch("homeassistant.components.zha.core.channels.ChannelPool.add_relay_channels") +@mock.patch( + "homeassistant.components.zha.core.channels.ChannelPool.add_client_channels" +) @mock.patch( "homeassistant.components.zha.core.discovery.PROBE.discover_entities", mock.MagicMock(), @@ -319,7 +321,9 @@ def test_ep_channels_all_channels(m1, zha_device_mock): assert "2:0x0300" in ep_channels.all_channels -@mock.patch("homeassistant.components.zha.core.channels.ChannelPool.add_relay_channels") +@mock.patch( + "homeassistant.components.zha.core.channels.ChannelPool.add_client_channels" +) @mock.patch( "homeassistant.components.zha.core.discovery.PROBE.discover_entities", mock.MagicMock(), @@ -387,14 +391,14 @@ async def test_ep_channels_configure(channel): ep_channels = zha_channels.ChannelPool(channels, mock.sentinel.ep) claimed = {ch_1.id: ch_1, ch_2.id: ch_2, ch_3.id: ch_3} - relay = {ch_4.id: ch_4, ch_5.id: ch_5} + client_chans = {ch_4.id: ch_4, ch_5.id: ch_5} with mock.patch.dict(ep_channels.claimed_channels, claimed, clear=True): - with mock.patch.dict(ep_channels.relay_channels, relay, clear=True): + with mock.patch.dict(ep_channels.client_channels, client_chans, clear=True): await ep_channels.async_configure() await ep_channels.async_initialize(mock.sentinel.from_cache) - for ch in [*claimed.values(), *relay.values()]: + for ch in [*claimed.values(), *client_chans.values()]: assert ch.async_initialize.call_count == 1 assert ch.async_initialize.await_count == 1 assert ch.async_initialize.call_args[0][0] is mock.sentinel.from_cache diff --git a/tests/components/zha/test_device_action.py b/tests/components/zha/test_device_action.py index c779dda6cf8..40e64934f89 100644 --- a/tests/components/zha/test_device_action.py +++ b/tests/components/zha/test_device_action.py @@ -103,7 +103,7 @@ async def test_action(hass, device_ias): await hass.async_block_till_done() calls = async_mock_service(hass, DOMAIN, "warning_device_warn") - channel = zha_device.channels.pools[0].relay_channels["1:0x0006"] + channel = zha_device.channels.pools[0].client_channels["1:0x0006"] channel.zha_send_event(COMMAND_SINGLE, []) await hass.async_block_till_done() diff --git a/tests/components/zha/test_device_trigger.py b/tests/components/zha/test_device_trigger.py index 9b69ba06e4f..266094963f2 100644 --- a/tests/components/zha/test_device_trigger.py +++ b/tests/components/zha/test_device_trigger.py @@ -172,7 +172,7 @@ async def test_if_fires_on_event(hass, mock_devices, calls): await hass.async_block_till_done() - channel = zha_device.channels.pools[0].relay_channels["1:0x0006"] + channel = zha_device.channels.pools[0].client_channels["1:0x0006"] channel.zha_send_event(COMMAND_SINGLE, []) await hass.async_block_till_done() diff --git a/tests/components/zha/test_discover.py b/tests/components/zha/test_discover.py index e1733ac44bd..e58fd740655 100644 --- a/tests/components/zha/test_discover.py +++ b/tests/components/zha/test_discover.py @@ -111,7 +111,7 @@ async def test_devices( ) event_channels = { - ch.id for pool in zha_dev.channels.pools for ch in pool.relay_channels.values() + ch.id for pool in zha_dev.channels.pools for ch in pool.client_channels.values() } entity_map = device["entity_map"] @@ -266,7 +266,7 @@ async def test_discover_endpoint(device_info, channels_mock, hass): ) assert device_info["event_channels"] == sorted( - [ch.id for pool in channels.pools for ch in pool.relay_channels.values()] + [ch.id for pool in channels.pools for ch in pool.client_channels.values()] ) assert new_ent.call_count == len( [ From 99d732b9748948799b50aaef8ff5acc30a6a70fe Mon Sep 17 00:00:00 2001 From: guillempages Date: Sun, 22 Mar 2020 01:48:18 +0100 Subject: [PATCH 182/431] Revert "Validate UUID for tankerkoenig (#32805)" (#33123) This reverts commit 49c2a4a4e3290de32cf3a10fd17f5c7a1f0234de. The validation was causing more problems than it was fixing, so removed it completely --- .../components/tankerkoenig/__init__.py | 20 ++---------- requirements_test_all.txt | 3 -- tests/components/tankerkoenig/__init__.py | 1 - .../test_tankerkoenig_validators.py | 32 ------------------- 4 files changed, 2 insertions(+), 54 deletions(-) delete mode 100755 tests/components/tankerkoenig/__init__.py delete mode 100755 tests/components/tankerkoenig/test_tankerkoenig_validators.py diff --git a/homeassistant/components/tankerkoenig/__init__.py b/homeassistant/components/tankerkoenig/__init__.py index 3f654f24522..e8b4e92327c 100755 --- a/homeassistant/components/tankerkoenig/__init__.py +++ b/homeassistant/components/tankerkoenig/__init__.py @@ -2,7 +2,6 @@ from datetime import timedelta import logging from math import ceil -from uuid import UUID import pytankerkoenig import voluptuous as vol @@ -26,26 +25,11 @@ _LOGGER = logging.getLogger(__name__) DEFAULT_RADIUS = 2 DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) - -def uuid4_string(value): - """Validate a v4 UUID in string format.""" - try: - result = UUID(value, version=4) - except (ValueError, AttributeError, TypeError) as error: - raise vol.Invalid("Invalid Version4 UUID", error_message=str(error)) - - if str(result) != value.lower(): - # UUID() will create a uuid4 if input is invalid - raise vol.Invalid("Invalid Version4 UUID") - - return str(result) - - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( { - vol.Required(CONF_API_KEY): uuid4_string, + vol.Required(CONF_API_KEY): cv.string, vol.Optional( CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL ): cv.time_period, @@ -66,7 +50,7 @@ CONFIG_SCHEMA = vol.Schema( cv.positive_int, vol.Range(min=1) ), vol.Optional(CONF_STATIONS, default=[]): vol.All( - cv.ensure_list, [uuid4_string] + cv.ensure_list, [cv.string] ), } ) diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f0df372cde9..ba6f4d05565 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -588,9 +588,6 @@ pysonos==0.0.25 # homeassistant.components.spc pyspcwebgw==0.4.0 -# homeassistant.components.tankerkoenig -pytankerkoenig==0.0.6 - # homeassistant.components.ecobee python-ecobee-api==0.2.2 diff --git a/tests/components/tankerkoenig/__init__.py b/tests/components/tankerkoenig/__init__.py deleted file mode 100755 index c420d23e157..00000000000 --- a/tests/components/tankerkoenig/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Tests for the tankerkoenig integration.""" diff --git a/tests/components/tankerkoenig/test_tankerkoenig_validators.py b/tests/components/tankerkoenig/test_tankerkoenig_validators.py deleted file mode 100755 index 7dba88b24fb..00000000000 --- a/tests/components/tankerkoenig/test_tankerkoenig_validators.py +++ /dev/null @@ -1,32 +0,0 @@ -"""The tests for the custom tankerkoenig validators.""" -import unittest -import uuid - -import pytest -import voluptuous as vol - -from homeassistant.components.tankerkoenig import uuid4_string - - -class TestUUID4StringValidator(unittest.TestCase): - """Test the UUID4 string custom validator.""" - - def test_uuid4_string(caplog): - """Test string uuid validation.""" - schema = vol.Schema(uuid4_string) - - for value in ["Not a hex string", "0", 0]: - with pytest.raises(vol.Invalid): - schema(value) - - with pytest.raises(vol.Invalid): - # the third block should start with 4 - schema("a03d31b2-2eee-2acc-bb90-eec40be6ed23") - - with pytest.raises(vol.Invalid): - # the fourth block should start with 8-a - schema("a03d31b2-2eee-4acc-1b90-eec40be6ed23") - - _str = str(uuid.uuid4()) - assert schema(_str) == _str - assert schema(_str.upper()) == _str From e8bd1b921607c292f2b218e2eb027cb4b7d058a3 Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 22 Mar 2020 03:12:32 +0200 Subject: [PATCH 183/431] Config Flow and Entity registry support for Monoprice (#30337) * Entity registry support for monoprice * Add test for unique_id * Add unique id namespace to monoprice * Config Flow for Monoprice * Update monoprice tests * Remove TODOs * Handle entity unloading * Fix update test * Streamline entity handling in monoprice services * Increase coverage * Remove devices cache * Async validation in monoprice config flow --- .../components/monoprice/__init__.py | 37 +- .../components/monoprice/config_flow.py | 95 +++ homeassistant/components/monoprice/const.py | 10 + .../components/monoprice/manifest.json | 3 +- .../components/monoprice/media_player.py | 148 ++-- .../components/monoprice/strings.json | 26 + homeassistant/generated/config_flows.py | 1 + .../components/monoprice/test_config_flow.py | 88 +++ .../components/monoprice/test_media_player.py | 660 ++++++++---------- 9 files changed, 614 insertions(+), 454 deletions(-) create mode 100644 homeassistant/components/monoprice/config_flow.py create mode 100644 homeassistant/components/monoprice/strings.json create mode 100644 tests/components/monoprice/test_config_flow.py diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index d968e270390..7845c70f1a8 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -1 +1,36 @@ -"""The monoprice component.""" +"""The Monoprice 6-Zone Amplifier integration.""" +import asyncio + +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant + +PLATFORMS = ["media_player"] + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Monoprice 6-Zone Amplifier component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Monoprice 6-Zone Amplifier from a config entry.""" + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + return unload_ok diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py new file mode 100644 index 00000000000..1ff02b3529f --- /dev/null +++ b/homeassistant/components/monoprice/config_flow.py @@ -0,0 +1,95 @@ +"""Config flow for Monoprice 6-Zone Amplifier integration.""" +import logging + +from pymonoprice import get_async_monoprice +from serial import SerialException +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PORT + +from .const import ( + CONF_SOURCE_1, + CONF_SOURCE_2, + CONF_SOURCE_3, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCE_6, + CONF_SOURCES, +) +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PORT): str, + vol.Optional(CONF_SOURCE_1): str, + vol.Optional(CONF_SOURCE_2): str, + vol.Optional(CONF_SOURCE_3): str, + vol.Optional(CONF_SOURCE_4): str, + vol.Optional(CONF_SOURCE_5): str, + vol.Optional(CONF_SOURCE_6): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + try: + await get_async_monoprice(data[CONF_PORT], hass.loop) + except SerialException: + _LOGGER.error("Error connecting to Monoprice controller") + raise CannotConnect + + sources_config = { + 1: data.get(CONF_SOURCE_1), + 2: data.get(CONF_SOURCE_2), + 3: data.get(CONF_SOURCE_3), + 4: data.get(CONF_SOURCE_4), + 5: data.get(CONF_SOURCE_5), + 6: data.get(CONF_SOURCE_6), + } + sources = { + index: name.strip() + for index, name in sources_config.items() + if (name is not None and name.strip() != "") + } + # Return info that you want to store in the config entry. + return {CONF_PORT: data[CONF_PORT], CONF_SOURCES: sources} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Monoprice 6-Zone Amplifier.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + + return self.async_create_entry(title=user_input[CONF_PORT], data=info) + except CannotConnect: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + 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 InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py index e8d813d2529..ea4667a77ff 100644 --- a/homeassistant/components/monoprice/const.py +++ b/homeassistant/components/monoprice/const.py @@ -1,5 +1,15 @@ """Constants for the Monoprice 6-Zone Amplifier Media Player component.""" DOMAIN = "monoprice" + +CONF_SOURCES = "sources" + +CONF_SOURCE_1 = "source_1" +CONF_SOURCE_2 = "source_2" +CONF_SOURCE_3 = "source_3" +CONF_SOURCE_4 = "source_4" +CONF_SOURCE_5 = "source_5" +CONF_SOURCE_6 = "source_6" + SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index d071276bcec..d9497c1c29c 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -4,5 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": ["pymonoprice==0.3"], "dependencies": [], - "codeowners": ["@etsinko"] + "codeowners": ["@etsinko"], + "config_flow": true } diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 20b2ecebcf4..e0585705ad8 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -3,9 +3,8 @@ import logging from pymonoprice import get_monoprice from serial import SerialException -import voluptuous as vol -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, @@ -14,16 +13,10 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_SET, SUPPORT_VOLUME_STEP, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_ON, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON +from homeassistant.helpers import config_validation as cv, entity_platform, service -from .const import DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT +from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT _LOGGER = logging.getLogger(__name__) @@ -36,104 +29,89 @@ SUPPORT_MONOPRICE = ( | SUPPORT_SELECT_SOURCE ) -ZONE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) -SOURCE_SCHEMA = vol.Schema({vol.Required(CONF_NAME): cv.string}) +def _get_sources(sources_config): + source_id_name = {int(index): name for index, name in sources_config.items()} -CONF_ZONES = "zones" -CONF_SOURCES = "sources" + source_name_id = {v: k for k, v in source_id_name.items()} -DATA_MONOPRICE = "monoprice" + source_names = sorted(source_name_id.keys(), key=lambda v: source_name_id[v]) -# Valid zone ids: 11-16 or 21-26 or 31-36 -ZONE_IDS = vol.All( - vol.Coerce(int), - vol.Any( - vol.Range(min=11, max=16), vol.Range(min=21, max=26), vol.Range(min=31, max=36) - ), -) - -# Valid source ids: 1-6 -SOURCE_IDS = vol.All(vol.Coerce(int), vol.Range(min=1, max=6)) - -MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) - -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_PORT): cv.string, - vol.Required(CONF_ZONES): vol.Schema({ZONE_IDS: ZONE_SCHEMA}), - vol.Required(CONF_SOURCES): vol.Schema({SOURCE_IDS: SOURCE_SCHEMA}), - } -) + return [source_id_name, source_name_id, source_names] -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_devices): """Set up the Monoprice 6-zone amplifier platform.""" - port = config.get(CONF_PORT) + port = config_entry.data.get(CONF_PORT) try: - monoprice = get_monoprice(port) + monoprice = await hass.async_add_executor_job(get_monoprice, port) except SerialException: _LOGGER.error("Error connecting to Monoprice controller") return - sources = { - source_id: extra[CONF_NAME] for source_id, extra in config[CONF_SOURCES].items() - } + sources = _get_sources(config_entry.data.get(CONF_SOURCES)) - hass.data[DATA_MONOPRICE] = [] - for zone_id, extra in config[CONF_ZONES].items(): - _LOGGER.info("Adding zone %d - %s", zone_id, extra[CONF_NAME]) - hass.data[DATA_MONOPRICE].append( - MonopriceZone(monoprice, sources, zone_id, extra[CONF_NAME]) - ) + devices = [] + for i in range(1, 4): + for j in range(1, 7): + zone_id = (i * 10) + j + _LOGGER.info("Adding zone %d for port %s", zone_id, port) + devices.append( + MonopriceZone(monoprice, sources, config_entry.entry_id, zone_id) + ) - add_entities(hass.data[DATA_MONOPRICE], True) + async_add_devices(devices, True) - def service_handle(service): + platform = entity_platform.current_platform.get() + + def _call_service(entities, service_call): + for entity in entities: + if service_call.service == SERVICE_SNAPSHOT: + entity.snapshot() + elif service_call.service == SERVICE_RESTORE: + entity.restore() + + @service.verify_domain_control(hass, DOMAIN) + async def async_service_handle(service_call): """Handle for services.""" - entity_ids = service.data.get(ATTR_ENTITY_ID) + entities = await platform.async_extract_from_service(service_call) - if entity_ids: - devices = [ - device - for device in hass.data[DATA_MONOPRICE] - if device.entity_id in entity_ids - ] - else: - devices = hass.data[DATA_MONOPRICE] + if not entities: + return - for device in devices: - if service.service == SERVICE_SNAPSHOT: - device.snapshot() - elif service.service == SERVICE_RESTORE: - device.restore() + hass.async_add_executor_job(_call_service, entities, service_call) - hass.services.register( - DOMAIN, SERVICE_SNAPSHOT, service_handle, schema=MEDIA_PLAYER_SCHEMA + hass.services.async_register( + DOMAIN, + SERVICE_SNAPSHOT, + async_service_handle, + schema=cv.make_entity_service_schema({}), ) - hass.services.register( - DOMAIN, SERVICE_RESTORE, service_handle, schema=MEDIA_PLAYER_SCHEMA + hass.services.async_register( + DOMAIN, + SERVICE_RESTORE, + async_service_handle, + schema=cv.make_entity_service_schema({}), ) class MonopriceZone(MediaPlayerDevice): """Representation of a Monoprice amplifier zone.""" - def __init__(self, monoprice, sources, zone_id, zone_name): + def __init__(self, monoprice, sources, namespace, zone_id): """Initialize new zone.""" self._monoprice = monoprice # dict source_id -> source name - self._source_id_name = sources + self._source_id_name = sources[0] # dict source name -> source_id - self._source_name_id = {v: k for k, v in sources.items()} + self._source_name_id = sources[1] # ordered list of all source names - self._source_names = sorted( - self._source_name_id.keys(), key=lambda v: self._source_name_id[v] - ) + self._source_names = sources[2] self._zone_id = zone_id - self._name = zone_name + self._unique_id = f"{namespace}_{self._zone_id}" + self._name = f"Zone {self._zone_id}" self._snapshot = None self._state = None @@ -156,6 +134,26 @@ class MonopriceZone(MediaPlayerDevice): self._source = None return True + @property + def entity_registry_enabled_default(self): + """Return if the entity should be enabled when first added to the entity registry.""" + return self._zone_id < 20 + + @property + def device_info(self): + """Return device info for this device.""" + return { + "identifiers": {(DOMAIN, self.unique_id)}, + "name": self.name, + "manufacturer": "Monoprice", + "model": "6-Zone Amplifier", + } + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._unique_id + @property def name(self): """Return the name of the zone.""" diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json new file mode 100644 index 00000000000..d0f5badbeb0 --- /dev/null +++ b/homeassistant/components/monoprice/strings.json @@ -0,0 +1,26 @@ +{ + "config": { + "title": "Monoprice 6-Zone Amplifier", + "step": { + "user": { + "title": "Connect to the device", + "data": { + "port": "Serial port", + "source_1": "Name of source #1", + "source_2": "Name of source #2", + "source_3": "Name of source #3", + "source_4": "Name of source #4", + "source_5": "Name of source #5", + "source_6": "Name of source #6" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 0d59a67c665..e318def042e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -69,6 +69,7 @@ FLOWS = [ "mikrotik", "minecraft_server", "mobile_app", + "monoprice", "mqtt", "myq", "neato", diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py new file mode 100644 index 00000000000..234f7538f19 --- /dev/null +++ b/tests/components/monoprice/test_config_flow.py @@ -0,0 +1,88 @@ +"""Test the Monoprice 6-Zone Amplifier config flow.""" +from asynctest import patch +from serial import SerialException + +from homeassistant import config_entries, setup +from homeassistant.components.monoprice.const import ( + CONF_SOURCE_1, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCES, + DOMAIN, +) +from homeassistant.const import CONF_PORT + +CONFIG = { + CONF_PORT: "/test/port", + CONF_SOURCE_1: "one", + CONF_SOURCE_4: "four", + CONF_SOURCE_5: " ", +} + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.monoprice.config_flow.get_async_monoprice", + return_value=True, + ), patch( + "homeassistant.components.monoprice.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.monoprice.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == CONFIG[CONF_PORT] + assert result2["data"] == { + CONF_PORT: CONFIG[CONF_PORT], + CONF_SOURCES: {1: CONFIG[CONF_SOURCE_1], 4: CONFIG[CONF_SOURCE_4]}, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.monoprice.config_flow.get_async_monoprice", + side_effect=SerialException, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_generic_exception(hass): + """Test we handle cannot generic exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.monoprice.config_flow.get_async_monoprice", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], CONFIG + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index e6747dfc4bf..80c6f7db169 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -1,12 +1,15 @@ """The tests for Monoprice Media player platform.""" from collections import defaultdict -import unittest -from unittest import mock -import pytest -import voluptuous as vol +from asynctest import patch +from serial import SerialException from homeassistant.components.media_player.const import ( + ATTR_INPUT_SOURCE, + ATTR_INPUT_SOURCE_LIST, + ATTR_MEDIA_VOLUME_LEVEL, + DOMAIN as MEDIA_PLAYER_DOMAIN, + SERVICE_SELECT_SOURCE, SUPPORT_SELECT_SOURCE, SUPPORT_TURN_OFF, SUPPORT_TURN_ON, @@ -15,18 +18,28 @@ from homeassistant.components.media_player.const import ( SUPPORT_VOLUME_STEP, ) from homeassistant.components.monoprice.const import ( + CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT, ) -from homeassistant.components.monoprice.media_player import ( - DATA_MONOPRICE, - PLATFORM_SCHEMA, - setup_platform, +from homeassistant.const import ( + CONF_PORT, + SERVICE_TURN_OFF, + SERVICE_TURN_ON, + SERVICE_VOLUME_DOWN, + SERVICE_VOLUME_MUTE, + SERVICE_VOLUME_SET, + SERVICE_VOLUME_UP, ) -from homeassistant.const import STATE_OFF, STATE_ON +from homeassistant.helpers.entity_component import async_update_entity -import tests.common +from tests.common import MockConfigEntry + +MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} + +ZONE_1_ID = "media_player.zone_11" +ZONE_2_ID = "media_player.zone_12" class AttrDict(dict): @@ -77,426 +90,319 @@ class MockMonoprice: self.zones[zone.zone] = AttrDict(zone) -class TestMonopriceSchema(unittest.TestCase): - """Test Monoprice schema.""" +async def test_cannot_connect(hass): + """Test connection error.""" - def test_valid_schema(self): - """Test valid schema.""" - valid_schema = { - "platform": "monoprice", - "port": "/dev/ttyUSB0", - "zones": { - 11: {"name": "a"}, - 12: {"name": "a"}, - 13: {"name": "a"}, - 14: {"name": "a"}, - 15: {"name": "a"}, - 16: {"name": "a"}, - 21: {"name": "a"}, - 22: {"name": "a"}, - 23: {"name": "a"}, - 24: {"name": "a"}, - 25: {"name": "a"}, - 26: {"name": "a"}, - 31: {"name": "a"}, - 32: {"name": "a"}, - 33: {"name": "a"}, - 34: {"name": "a"}, - 35: {"name": "a"}, - 36: {"name": "a"}, - }, - "sources": { - 1: {"name": "a"}, - 2: {"name": "a"}, - 3: {"name": "a"}, - 4: {"name": "a"}, - 5: {"name": "a"}, - 6: {"name": "a"}, - }, - } - PLATFORM_SCHEMA(valid_schema) - - def test_invalid_schemas(self): - """Test invalid schemas.""" - schemas = ( - {}, # Empty - None, # None - # Missing port - { - "platform": "monoprice", - "name": "Name", - "zones": {11: {"name": "a"}}, - "sources": {1: {"name": "b"}}, - }, - # Invalid zone number - { - "platform": "monoprice", - "port": "aaa", - "name": "Name", - "zones": {10: {"name": "a"}}, - "sources": {1: {"name": "b"}}, - }, - # Invalid source number - { - "platform": "monoprice", - "port": "aaa", - "name": "Name", - "zones": {11: {"name": "a"}}, - "sources": {0: {"name": "b"}}, - }, - # Zone missing name - { - "platform": "monoprice", - "port": "aaa", - "name": "Name", - "zones": {11: {}}, - "sources": {1: {"name": "b"}}, - }, - # Source missing name - { - "platform": "monoprice", - "port": "aaa", - "name": "Name", - "zones": {11: {"name": "a"}}, - "sources": {1: {}}, - }, - ) - for value in schemas: - with pytest.raises(vol.MultipleInvalid): - PLATFORM_SCHEMA(value) + with patch( + "homeassistant.components.monoprice.media_player.get_monoprice", + side_effect=SerialException, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + # setup_component(self.hass, DOMAIN, MOCK_CONFIG) + # self.hass.async_block_till_done() + await hass.async_block_till_done() + assert hass.states.get(ZONE_1_ID) is None -class TestMonopriceMediaPlayer(unittest.TestCase): - """Test the media_player module.""" +async def _setup_monoprice(hass, monoprice): + with patch( + "homeassistant.components.monoprice.media_player.get_monoprice", + new=lambda *a: monoprice, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + # setup_component(self.hass, DOMAIN, MOCK_CONFIG) + # self.hass.async_block_till_done() + await hass.async_block_till_done() - def setUp(self): - """Set up the test case.""" - self.monoprice = MockMonoprice() - self.hass = tests.common.get_test_home_assistant() - self.hass.start() - # Note, source dictionary is unsorted! - with mock.patch( - "homeassistant.components.monoprice.media_player.get_monoprice", - new=lambda *a: self.monoprice, - ): - setup_platform( - self.hass, - { - "platform": "monoprice", - "port": "/dev/ttyS0", - "name": "Name", - "zones": {12: {"name": "Zone name"}}, - "sources": { - 1: {"name": "one"}, - 3: {"name": "three"}, - 2: {"name": "two"}, - }, - }, - lambda *args, **kwargs: None, - {}, - ) - self.hass.block_till_done() - self.media_player = self.hass.data[DATA_MONOPRICE][0] - self.media_player.hass = self.hass - self.media_player.entity_id = "media_player.zone_1" - def tearDown(self): - """Tear down the test case.""" - self.hass.stop() +async def _call_media_player_service(hass, name, data): + await hass.services.async_call( + MEDIA_PLAYER_DOMAIN, name, service_data=data, blocking=True + ) - def test_setup_platform(self, *args): - """Test setting up platform.""" - # Two services must be registered - assert self.hass.services.has_service(DOMAIN, SERVICE_RESTORE) - assert self.hass.services.has_service(DOMAIN, SERVICE_SNAPSHOT) - assert len(self.hass.data[DATA_MONOPRICE]) == 1 - assert self.hass.data[DATA_MONOPRICE][0].name == "Zone name" - def test_service_calls_with_entity_id(self): - """Test snapshot save/restore service calls.""" - self.media_player.update() - assert "Zone name" == self.media_player.name - assert STATE_ON == self.media_player.state - assert 0.0 == self.media_player.volume_level, 0.0001 - assert self.media_player.is_volume_muted - assert "one" == self.media_player.source +async def _call_homeassistant_service(hass, name, data): + await hass.services.async_call( + "homeassistant", name, service_data=data, blocking=True + ) - # Saving default values - self.hass.services.call( - DOMAIN, - SERVICE_SNAPSHOT, - {"entity_id": "media_player.zone_1"}, - blocking=True, - ) - # self.hass.block_till_done() - # Changing media player to new state - self.media_player.set_volume_level(1) - self.media_player.select_source("two") - self.media_player.mute_volume(False) - self.media_player.turn_off() +async def _call_monoprice_service(hass, name, data): + await hass.services.async_call(DOMAIN, name, service_data=data, blocking=True) - # Checking that values were indeed changed - self.media_player.update() - assert "Zone name" == self.media_player.name - assert STATE_OFF == self.media_player.state - assert 1.0 == self.media_player.volume_level, 0.0001 - assert not self.media_player.is_volume_muted - assert "two" == self.media_player.source - # Restoring wrong media player to its previous state - # Nothing should be done - self.hass.services.call( - DOMAIN, SERVICE_RESTORE, {"entity_id": "media.not_existing"}, blocking=True - ) - # self.hass.block_till_done() +async def test_service_calls_with_entity_id(hass): + """Test snapshot save/restore service calls.""" + await _setup_monoprice(hass, MockMonoprice()) - # Checking that values were not (!) restored - self.media_player.update() - assert "Zone name" == self.media_player.name - assert STATE_OFF == self.media_player.state - assert 1.0 == self.media_player.volume_level, 0.0001 - assert not self.media_player.is_volume_muted - assert "two" == self.media_player.source + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) - # Restoring media player to its previous state - self.hass.services.call( - DOMAIN, SERVICE_RESTORE, {"entity_id": "media_player.zone_1"}, blocking=True - ) - self.hass.block_till_done() + # Saving existing values + await _call_monoprice_service(hass, SERVICE_SNAPSHOT, {"entity_id": ZONE_1_ID}) - # Checking that values were restored - assert "Zone name" == self.media_player.name - assert STATE_ON == self.media_player.state - assert 0.0 == self.media_player.volume_level, 0.0001 - assert self.media_player.is_volume_muted - assert "one" == self.media_player.source + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} + ) - def test_service_calls_without_entity_id(self): - """Test snapshot save/restore service calls.""" - self.media_player.update() - assert "Zone name" == self.media_player.name - assert STATE_ON == self.media_player.state - assert 0.0 == self.media_player.volume_level, 0.0001 - assert self.media_player.is_volume_muted - assert "one" == self.media_player.source + # Restoring other media player to its previous state + # The zone should not be restored + await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_2_ID}) + await hass.async_block_till_done() - # Restoring media player - # since there is no snapshot, nothing should be done - self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True) - self.hass.block_till_done() - self.media_player.update() - assert "Zone name" == self.media_player.name - assert STATE_ON == self.media_player.state - assert 0.0 == self.media_player.volume_level, 0.0001 - assert self.media_player.is_volume_muted - assert "one" == self.media_player.source + # Checking that values were not (!) restored + state = hass.states.get(ZONE_1_ID) - # Saving default values - self.hass.services.call(DOMAIN, SERVICE_SNAPSHOT, blocking=True) - self.hass.block_till_done() + assert 1.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL] + assert "three" == state.attributes[ATTR_INPUT_SOURCE] - # Changing media player to new state - self.media_player.set_volume_level(1) - self.media_player.select_source("two") - self.media_player.mute_volume(False) - self.media_player.turn_off() + # Restoring media player to its previous state + await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) + await hass.async_block_till_done() - # Checking that values were indeed changed - self.media_player.update() - assert "Zone name" == self.media_player.name - assert STATE_OFF == self.media_player.state - assert 1.0 == self.media_player.volume_level, 0.0001 - assert not self.media_player.is_volume_muted - assert "two" == self.media_player.source + state = hass.states.get(ZONE_1_ID) - # Restoring media player to its previous state - self.hass.services.call(DOMAIN, SERVICE_RESTORE, blocking=True) - self.hass.block_till_done() + assert 0.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL] + assert "one" == state.attributes[ATTR_INPUT_SOURCE] - # Checking that values were restored - assert "Zone name" == self.media_player.name - assert STATE_ON == self.media_player.state - assert 0.0 == self.media_player.volume_level, 0.0001 - assert self.media_player.is_volume_muted - assert "one" == self.media_player.source - def test_update(self): - """Test updating values from monoprice.""" - assert self.media_player.state is None - assert self.media_player.volume_level is None - assert self.media_player.is_volume_muted is None - assert self.media_player.source is None +async def test_service_calls_with_all_entities(hass): + """Test snapshot save/restore service calls.""" + await _setup_monoprice(hass, MockMonoprice()) - self.media_player.update() + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) - assert STATE_ON == self.media_player.state - assert 0.0 == self.media_player.volume_level, 0.0001 - assert self.media_player.is_volume_muted - assert "one" == self.media_player.source + # Saving existing values + await _call_monoprice_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) - def test_name(self): - """Test name property.""" - assert "Zone name" == self.media_player.name + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} + ) - def test_state(self): - """Test state property.""" - assert self.media_player.state is None + # Restoring media player to its previous state + await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "all"}) + await hass.async_block_till_done() - self.media_player.update() - assert STATE_ON == self.media_player.state + state = hass.states.get(ZONE_1_ID) - self.monoprice.zones[12].power = False - self.media_player.update() - assert STATE_OFF == self.media_player.state + assert 0.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL] + assert "one" == state.attributes[ATTR_INPUT_SOURCE] - def test_volume_level(self): - """Test volume level property.""" - assert self.media_player.volume_level is None - self.media_player.update() - assert 0.0 == self.media_player.volume_level, 0.0001 - self.monoprice.zones[12].volume = 38 - self.media_player.update() - assert 1.0 == self.media_player.volume_level, 0.0001 +async def test_service_calls_without_relevant_entities(hass): + """Test snapshot save/restore service calls.""" + await _setup_monoprice(hass, MockMonoprice()) - self.monoprice.zones[12].volume = 19 - self.media_player.update() - assert 0.5 == self.media_player.volume_level, 0.0001 + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) - def test_is_volume_muted(self): - """Test volume muted property.""" - assert self.media_player.is_volume_muted is None + # Saving existing values + await _call_monoprice_service(hass, SERVICE_SNAPSHOT, {"entity_id": "all"}) - self.media_player.update() - assert self.media_player.is_volume_muted + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "three"} + ) - self.monoprice.zones[12].mute = False - self.media_player.update() - assert not self.media_player.is_volume_muted + # Restoring media player to its previous state + await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": "light.demo"}) + await hass.async_block_till_done() - def test_supported_features(self): - """Test supported features property.""" - assert ( - SUPPORT_VOLUME_MUTE - | SUPPORT_VOLUME_SET - | SUPPORT_VOLUME_STEP - | SUPPORT_TURN_ON - | SUPPORT_TURN_OFF - | SUPPORT_SELECT_SOURCE - == self.media_player.supported_features - ) + state = hass.states.get(ZONE_1_ID) - def test_source(self): - """Test source property.""" - assert self.media_player.source is None - self.media_player.update() - assert "one" == self.media_player.source + assert 1.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL] + assert "three" == state.attributes[ATTR_INPUT_SOURCE] - def test_media_title(self): - """Test media title property.""" - assert self.media_player.media_title is None - self.media_player.update() - assert "one" == self.media_player.media_title - def test_source_list(self): - """Test source list property.""" - # Note, the list is sorted! - assert ["one", "two", "three"] == self.media_player.source_list +async def test_restore_without_snapshort(hass): + """Test restore when snapshot wasn't called.""" + await _setup_monoprice(hass, MockMonoprice()) - def test_select_source(self): - """Test source selection methods.""" - self.media_player.update() + with patch.object(MockMonoprice, "restore_zone") as method_call: + await _call_monoprice_service(hass, SERVICE_RESTORE, {"entity_id": ZONE_1_ID}) + await hass.async_block_till_done() - assert "one" == self.media_player.source + assert not method_call.called - self.media_player.select_source("two") - assert 2 == self.monoprice.zones[12].source - self.media_player.update() - assert "two" == self.media_player.source - # Trying to set unknown source - self.media_player.select_source("no name") - assert 2 == self.monoprice.zones[12].source - self.media_player.update() - assert "two" == self.media_player.source +async def test_update(hass): + """Test updating values from monoprice.""" + """Test snapshot save/restore service calls.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) - def test_turn_on(self): - """Test turning on the zone.""" - self.monoprice.zones[12].power = False - self.media_player.update() - assert STATE_OFF == self.media_player.state + # Changing media player to new state + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + await _call_media_player_service( + hass, SERVICE_SELECT_SOURCE, {"entity_id": ZONE_1_ID, "source": "one"} + ) - self.media_player.turn_on() - assert self.monoprice.zones[12].power - self.media_player.update() - assert STATE_ON == self.media_player.state + monoprice.set_source(11, 3) + monoprice.set_volume(11, 38) - def test_turn_off(self): - """Test turning off the zone.""" - self.monoprice.zones[12].power = True - self.media_player.update() - assert STATE_ON == self.media_player.state + await async_update_entity(hass, ZONE_1_ID) + await hass.async_block_till_done() - self.media_player.turn_off() - assert not self.monoprice.zones[12].power - self.media_player.update() - assert STATE_OFF == self.media_player.state + state = hass.states.get(ZONE_1_ID) - def test_mute_volume(self): - """Test mute functionality.""" - self.monoprice.zones[12].mute = True - self.media_player.update() - assert self.media_player.is_volume_muted + assert 1.0 == state.attributes[ATTR_MEDIA_VOLUME_LEVEL] + assert "three" == state.attributes[ATTR_INPUT_SOURCE] - self.media_player.mute_volume(False) - assert not self.monoprice.zones[12].mute - self.media_player.update() - assert not self.media_player.is_volume_muted - self.media_player.mute_volume(True) - assert self.monoprice.zones[12].mute - self.media_player.update() - assert self.media_player.is_volume_muted +async def test_supported_features(hass): + """Test supported features property.""" + await _setup_monoprice(hass, MockMonoprice()) - def test_set_volume_level(self): - """Test set volume level.""" - self.media_player.set_volume_level(1.0) - assert 38 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) + state = hass.states.get(ZONE_1_ID) + assert ( + SUPPORT_VOLUME_MUTE + | SUPPORT_VOLUME_SET + | SUPPORT_VOLUME_STEP + | SUPPORT_TURN_ON + | SUPPORT_TURN_OFF + | SUPPORT_SELECT_SOURCE + == state.attributes["supported_features"] + ) - self.media_player.set_volume_level(0.0) - assert 0 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) - self.media_player.set_volume_level(0.5) - assert 19 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) +async def test_source_list(hass): + """Test source list property.""" + await _setup_monoprice(hass, MockMonoprice()) - def test_volume_up(self): - """Test increasing volume by one.""" - self.monoprice.zones[12].volume = 37 - self.media_player.update() - self.media_player.volume_up() - assert 38 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) + state = hass.states.get(ZONE_1_ID) + # Note, the list is sorted! + assert ["one", "three"] == state.attributes[ATTR_INPUT_SOURCE_LIST] - # Try to raise value beyond max - self.media_player.update() - self.media_player.volume_up() - assert 38 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) - def test_volume_down(self): - """Test decreasing volume by one.""" - self.monoprice.zones[12].volume = 1 - self.media_player.update() - self.media_player.volume_down() - assert 0 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) +async def test_select_source(hass): + """Test source selection methods.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) - # Try to lower value beyond minimum - self.media_player.update() - self.media_player.volume_down() - assert 0 == self.monoprice.zones[12].volume - assert isinstance(self.monoprice.zones[12].volume, int) + await _call_media_player_service( + hass, + SERVICE_SELECT_SOURCE, + {"entity_id": ZONE_1_ID, ATTR_INPUT_SOURCE: "three"}, + ) + assert 3 == monoprice.zones[11].source + + # Trying to set unknown source + await _call_media_player_service( + hass, + SERVICE_SELECT_SOURCE, + {"entity_id": ZONE_1_ID, ATTR_INPUT_SOURCE: "no name"}, + ) + assert 3 == monoprice.zones[11].source + + +async def test_unknown_source(hass): + """Test behavior when device has unknown source.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + monoprice.set_source(11, 5) + + await async_update_entity(hass, ZONE_1_ID) + await hass.async_block_till_done() + + state = hass.states.get(ZONE_1_ID) + + assert state.attributes.get(ATTR_INPUT_SOURCE) is None + + +async def test_turn_on_off(hass): + """Test turning on the zone.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + await _call_media_player_service(hass, SERVICE_TURN_OFF, {"entity_id": ZONE_1_ID}) + assert not monoprice.zones[11].power + + await _call_media_player_service(hass, SERVICE_TURN_ON, {"entity_id": ZONE_1_ID}) + assert monoprice.zones[11].power + + +async def test_mute_volume(hass): + """Test mute functionality.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.5} + ) + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": False} + ) + assert not monoprice.zones[11].mute + + await _call_media_player_service( + hass, SERVICE_VOLUME_MUTE, {"entity_id": ZONE_1_ID, "is_volume_muted": True} + ) + assert monoprice.zones[11].mute + + +async def test_volume_up_down(hass): + """Test increasing volume by one.""" + monoprice = MockMonoprice() + await _setup_monoprice(hass, monoprice) + + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 0.0} + ) + assert 0 == monoprice.zones[11].volume + + await _call_media_player_service( + hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} + ) + # should not go below zero + assert 0 == monoprice.zones[11].volume + + await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) + assert 1 == monoprice.zones[11].volume + + await _call_media_player_service( + hass, SERVICE_VOLUME_SET, {"entity_id": ZONE_1_ID, "volume_level": 1.0} + ) + assert 38 == monoprice.zones[11].volume + + await _call_media_player_service(hass, SERVICE_VOLUME_UP, {"entity_id": ZONE_1_ID}) + # should not go above 38 + assert 38 == monoprice.zones[11].volume + + await _call_media_player_service( + hass, SERVICE_VOLUME_DOWN, {"entity_id": ZONE_1_ID} + ) + assert 37 == monoprice.zones[11].volume From 252d934caa8f830a2f682000e22a37aa811d0e3d Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 22 Mar 2020 12:17:48 +0100 Subject: [PATCH 184/431] Update azure-pipelines-wheels.yml --- azure-pipelines-wheels.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index cd04feb4638..dafae49d89c 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.4-3.7-alpine3.10' + value: '1.7.0-3.7-alpine3.11' resources: repositories: - repository: azure @@ -32,6 +32,7 @@ jobs: builderVersion: '$(versionWheels)' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy' + skipBinary: 'aiohttp' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' preBuild: From 4510e831501f811d304c2a6fd3bf53248387b07c Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 22 Mar 2020 12:31:14 +0100 Subject: [PATCH 185/431] [skip ci] update wheels builder version --- azure-pipelines-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index dafae49d89c..81d76708edc 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -18,7 +18,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.7.0-3.7-alpine3.11' + value: '1.8.0-3.7-alpine3.11' resources: repositories: - repository: azure From 99877c32b11dba621e30fe65cf622349ee7076ae Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 22 Mar 2020 07:19:26 -0500 Subject: [PATCH 186/431] Bump pyecobee to 0.2.3 (#33130) --- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index 9f6b861c8fb..dd96191e4fe 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.2.2"], + "requirements": ["python-ecobee-api==0.2.3"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 20c62a42136..027c7d9f632 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1582,7 +1582,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.2 +python-ecobee-api==0.2.3 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ba6f4d05565..e578add5966 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -589,7 +589,7 @@ pysonos==0.0.25 pyspcwebgw==0.4.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.2 +python-ecobee-api==0.2.3 # homeassistant.components.darksky python-forecastio==1.4.0 From 66402b9b38dfabcc78143c637180fe9a9dee944b Mon Sep 17 00:00:00 2001 From: On Freund Date: Sun, 22 Mar 2020 14:25:27 +0200 Subject: [PATCH 187/431] Monoprice PR followups (#33133) --- .../components/monoprice/__init__.py | 19 +++++++++++++++++++ .../components/monoprice/config_flow.py | 4 ---- .../components/monoprice/media_player.py | 19 ++++++------------- .../components/monoprice/test_media_player.py | 6 ++---- 4 files changed, 27 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 7845c70f1a8..d18229e3d09 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -1,11 +1,21 @@ """The Monoprice 6-Zone Amplifier integration.""" import asyncio +import logging + +from pymonoprice import get_monoprice +from serial import SerialException from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PORT from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN PLATFORMS = ["media_player"] +_LOGGER = logging.getLogger(__name__) + async def async_setup(hass: HomeAssistant, config: dict): """Set up the Monoprice 6-Zone Amplifier component.""" @@ -14,6 +24,15 @@ async def async_setup(hass: HomeAssistant, config: dict): async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): """Set up Monoprice 6-Zone Amplifier from a config entry.""" + port = entry.data[CONF_PORT] + + try: + monoprice = await hass.async_add_executor_job(get_monoprice, port) + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = monoprice + except SerialException: + _LOGGER.error("Error connecting to Monoprice controller at %s", port) + raise ConfigEntryNotReady + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 1ff02b3529f..45434fac131 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -89,7 +89,3 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index e0585705ad8..6e898cd6d4c 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,9 +1,6 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging -from pymonoprice import get_monoprice -from serial import SerialException - from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, @@ -40,28 +37,24 @@ def _get_sources(sources_config): return [source_id_name, source_name_id, source_names] -async def async_setup_entry(hass, config_entry, async_add_devices): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Monoprice 6-zone amplifier platform.""" - port = config_entry.data.get(CONF_PORT) + port = config_entry.data[CONF_PORT] - try: - monoprice = await hass.async_add_executor_job(get_monoprice, port) - except SerialException: - _LOGGER.error("Error connecting to Monoprice controller") - return + monoprice = hass.data[DOMAIN][config_entry.entry_id] sources = _get_sources(config_entry.data.get(CONF_SOURCES)) - devices = [] + entities = [] for i in range(1, 4): for j in range(1, 7): zone_id = (i * 10) + j _LOGGER.info("Adding zone %d for port %s", zone_id, port) - devices.append( + entities.append( MonopriceZone(monoprice, sources, config_entry.entry_id, zone_id) ) - async_add_devices(devices, True) + async_add_entities(entities, True) platform = entity_platform.current_platform.get() diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 80c6f7db169..2aad854652b 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -94,8 +94,7 @@ async def test_cannot_connect(hass): """Test connection error.""" with patch( - "homeassistant.components.monoprice.media_player.get_monoprice", - side_effect=SerialException, + "homeassistant.components.monoprice.get_monoprice", side_effect=SerialException, ): config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) config_entry.add_to_hass(hass) @@ -108,8 +107,7 @@ async def test_cannot_connect(hass): async def _setup_monoprice(hass, monoprice): with patch( - "homeassistant.components.monoprice.media_player.get_monoprice", - new=lambda *a: monoprice, + "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, ): config_entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONFIG) config_entry.add_to_hass(hass) From f95c3e265d7197186607fcd194ced3bb245c4f87 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sun, 22 Mar 2020 05:29:50 -0700 Subject: [PATCH 188/431] Fix script logging with name (#33120) --- homeassistant/helpers/script.py | 4 +++- tests/helpers/test_script.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 7d1088eebe4..145bb42af5b 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -817,7 +817,9 @@ class Script: def _log(self, msg, *args, level=logging.INFO): if self.name: - msg = f"{self.name}: {msg}" + msg = f"%s: {msg}" + args = [self.name, *args] + if level == _LOG_EXCEPTION: self._logger.exception(msg, *args) else: diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index eb1d5e15020..9d7e7751c10 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1138,3 +1138,15 @@ async def test_script_mode_queue(hass): assert not script_obj.is_running assert len(events) == 4 assert events[3].data["value"] == 2 + + +async def test_script_logging(caplog): + """Test script logging.""" + script_obj = script.Script(None, [], "Script with % Name") + script_obj._log("Test message with name %s", 1) + + assert "Script with % Name: Test message with name 1" in caplog.text + + script_obj = script.Script(None, []) + script_obj._log("Test message without name %s", 2) + assert "Test message without name 2" in caplog.text From 9d87c1ab1a21b80d6ad4503c1f5e27d90dad049b Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 22 Mar 2020 13:44:57 +0100 Subject: [PATCH 189/431] Bump gios to 0.0.4 (#33141) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index b3d125d8ab6..a48ec7b5c2f 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gios", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["gios==0.0.3"], + "requirements": ["gios==0.0.4"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 027c7d9f632..af8ca7bc562 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -603,7 +603,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.3 +gios==0.0.4 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e578add5966..bcf9a2fea82 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -230,7 +230,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.3 +gios==0.0.4 # homeassistant.components.glances glances_api==0.2.0 From 8423d18d8d6d423b5c82403372fe255303af2ff8 Mon Sep 17 00:00:00 2001 From: Christian Ferbar <5595808+ferbar@users.noreply.github.com> Date: Sun, 22 Mar 2020 14:00:09 +0100 Subject: [PATCH 190/431] Add Miflora go_unavailable_timeout (#31156) * Clear state on exception Clear state if querying the device fails. The state is then set to unknown, so it can be tracked if a miflora device isn't responding any more. * Add available() Signal valid data via available() * miflora: add timeout to go unavailable * code cleanup * miflora: tox cleanup --- homeassistant/components/miflora/sensor.py | 52 +++++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index bd551517562..776e2151a7e 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -1,4 +1,5 @@ """Support for Xiaomi Mi Flora BLE plant sensor.""" + from datetime import timedelta import logging @@ -20,6 +21,7 @@ from homeassistant.const import ( from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +import homeassistant.util.dt as dt_util try: import bluepy.btle # noqa: F401 pylint: disable=unused-import @@ -32,14 +34,18 @@ _LOGGER = logging.getLogger(__name__) CONF_ADAPTER = "adapter" CONF_MEDIAN = "median" +CONF_GO_UNAVAILABLE_TIMEOUT = "go_unavailable_timeout" DEFAULT_ADAPTER = "hci0" DEFAULT_FORCE_UPDATE = False DEFAULT_MEDIAN = 3 DEFAULT_NAME = "Mi Flora" +DEFAULT_GO_UNAVAILABLE_TIMEOUT = timedelta(seconds=7200) SCAN_INTERVAL = timedelta(seconds=1200) +ATTR_LAST_SUCCESSFUL_UPDATE = "last_successful_update" + # Sensor types are defined like: Name, units, icon SENSOR_TYPES = { "temperature": ["Temperature", "°C", "mdi:thermometer"], @@ -59,6 +65,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_MEDIAN, default=DEFAULT_MEDIAN): cv.positive_int, vol.Optional(CONF_FORCE_UPDATE, default=DEFAULT_FORCE_UPDATE): cv.boolean, vol.Optional(CONF_ADAPTER, default=DEFAULT_ADAPTER): cv.string, + vol.Optional( + CONF_GO_UNAVAILABLE_TIMEOUT, default=DEFAULT_GO_UNAVAILABLE_TIMEOUT + ): cv.time_period, } ) @@ -78,6 +87,8 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= force_update = config.get(CONF_FORCE_UPDATE) median = config.get(CONF_MEDIAN) + go_unavailable_timeout = config.get(CONF_GO_UNAVAILABLE_TIMEOUT) + devs = [] for parameter in config[CONF_MONITORED_CONDITIONS]: @@ -90,7 +101,16 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= name = f"{prefix} {name}" devs.append( - MiFloraSensor(poller, parameter, name, unit, icon, force_update, median) + MiFloraSensor( + poller, + parameter, + name, + unit, + icon, + force_update, + median, + go_unavailable_timeout, + ) ) async_add_entities(devs) @@ -99,7 +119,17 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class MiFloraSensor(Entity): """Implementing the MiFlora sensor.""" - def __init__(self, poller, parameter, name, unit, icon, force_update, median): + def __init__( + self, + poller, + parameter, + name, + unit, + icon, + force_update, + median, + go_unavailable_timeout, + ): """Initialize the sensor.""" self.poller = poller self.parameter = parameter @@ -107,9 +137,10 @@ class MiFloraSensor(Entity): self._icon = icon self._name = name self._state = None - self._available = False self.data = [] self._force_update = force_update + self.go_unavailable_timeout = go_unavailable_timeout + self.last_successful_update = dt_util.utc_from_timestamp(0) # Median is used to filter out outliers. median of 3 will filter # single outliers, while median of 5 will filter double outliers # Use median_count = 1 if no filtering is required. @@ -136,8 +167,16 @@ class MiFloraSensor(Entity): @property def available(self): - """Return True if entity is available.""" - return self._available + """Return True if did update since 2h.""" + return self.last_successful_update > ( + dt_util.utcnow() - self.go_unavailable_timeout + ) + + @property + def device_state_attributes(self): + """Return the state attributes of the device.""" + attr = {ATTR_LAST_SUCCESSFUL_UPDATE: self.last_successful_update} + return attr @property def unit_of_measurement(self): @@ -165,13 +204,12 @@ class MiFloraSensor(Entity): data = self.poller.parameter_value(self.parameter) except (OSError, BluetoothBackendException) as err: _LOGGER.info("Polling error %s: %s", type(err).__name__, err) - self._available = False return if data is not None: _LOGGER.debug("%s = %s", self.name, data) - self._available = True self.data.append(data) + self.last_successful_update = dt_util.utcnow() else: _LOGGER.info("Did not receive any data from Mi Flora sensor %s", self.name) # Remove old data from median list or set sensor value to None From c79b3df73f7dc4ed28dfb2d1dda90d554350ea2a Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sun, 22 Mar 2020 07:35:58 -0700 Subject: [PATCH 191/431] Handle generic_thermostat state unavailable (#32852) * fix sensor unavailable error * fix climate.py --- .../components/generic_thermostat/climate.py | 8 +++- .../generic_thermostat/test_climate.py | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/generic_thermostat/climate.py b/homeassistant/components/generic_thermostat/climate.py index 8714ddcfbe6..a7ddcc08314 100644 --- a/homeassistant/components/generic_thermostat/climate.py +++ b/homeassistant/components/generic_thermostat/climate.py @@ -30,6 +30,7 @@ from homeassistant.const import ( SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON, + STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import DOMAIN as HA_DOMAIN, callback @@ -197,7 +198,10 @@ class GenericThermostat(ClimateDevice, RestoreEntity): def _async_startup(event): """Init on startup.""" sensor_state = self.hass.states.get(self.sensor_entity_id) - if sensor_state and sensor_state.state != STATE_UNKNOWN: + if sensor_state and sensor_state.state not in ( + STATE_UNAVAILABLE, + STATE_UNKNOWN, + ): self._async_update_temp(sensor_state) self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_startup) @@ -352,7 +356,7 @@ class GenericThermostat(ClimateDevice, RestoreEntity): async def _async_sensor_changed(self, entity_id, old_state, new_state): """Handle temperature changes.""" - if new_state is None: + if new_state is None or new_state.state in (STATE_UNAVAILABLE, STATE_UNKNOWN): return self._async_update_temp(new_state) diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index 776d8f39f69..264146a6fda 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -22,6 +22,8 @@ from homeassistant.const import ( SERVICE_TURN_ON, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, + STATE_UNKNOWN, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) @@ -271,6 +273,44 @@ async def test_sensor_bad_value(hass, setup_comp_2): assert temp == state.attributes.get("current_temperature") +async def test_sensor_unknown(hass): + """Test when target sensor is Unknown.""" + hass.states.async_set("sensor.unknown", STATE_UNKNOWN) + assert await async_setup_component( + hass, + "climate", + { + "climate": { + "platform": "generic_thermostat", + "name": "unknown", + "heater": ENT_SWITCH, + "target_sensor": "sensor.unknown", + } + }, + ) + state = hass.states.get("climate.unknown") + assert state.attributes.get("current_temperature") is None + + +async def test_sensor_unavailable(hass): + """Test when target sensor is Unavailable.""" + hass.states.async_set("sensor.unavailable", STATE_UNAVAILABLE) + assert await async_setup_component( + hass, + "climate", + { + "climate": { + "platform": "generic_thermostat", + "name": "unavailable", + "heater": ENT_SWITCH, + "target_sensor": "sensor.unavailable", + } + }, + ) + state = hass.states.get("climate.unavailable") + assert state.attributes.get("current_temperature") is None + + async def test_set_target_temp_heater_on(hass, setup_comp_2): """Test if target temperature turn heater on.""" calls = _setup_switch(hass, False) From 912cda4e6f0d418b4bac25f0a3c7e36cabd26d4b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 22 Mar 2020 16:37:33 +0100 Subject: [PATCH 192/431] [skip ci] add rc into build --- azure-pipelines-wheels.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 81d76708edc..816e64d29bf 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -5,6 +5,7 @@ trigger: branches: include: - dev + - rc paths: include: - requirements_all.txt From ca3a22b5a3e173f12fee0d87272bcec278ee100a Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Sun, 22 Mar 2020 12:34:00 -0400 Subject: [PATCH 193/431] Filter out duplicate app names from vizio's source list (#33051) * filter out additional app config names from pyvizios app list * add test * change where filtering app list occurs * fix test * fix mistake in change * hopefully final test fix * fix test scenario that unknowingly broke with test change --- homeassistant/components/vizio/media_player.py | 6 +++++- tests/components/vizio/const.py | 3 +-- tests/components/vizio/test_media_player.py | 11 +++++++---- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index a46a4c9a2d1..d97a82ca144 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -335,7 +335,11 @@ class VizioDevice(MediaPlayerDevice): if _input not in INPUT_APPS ], *self._available_apps, - *self._get_additional_app_names(), + *[ + app + for app in self._get_additional_app_names() + if app not in self._available_apps + ], ] return self._available_inputs diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 2cb9103c4d9..25abd01d53b 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -70,10 +70,9 @@ INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] CURRENT_APP = "Hulu" APP_LIST = ["Hulu", "Netflix"] INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"] -CUSTOM_APP_NAME = "APP3" CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10} ADDITIONAL_APP_CONFIG = { - "name": CUSTOM_APP_NAME, + "name": CURRENT_APP, CONF_CONFIG: CUSTOM_CONFIG, } diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index 68366e8e98b..ebeef1661ed 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -58,7 +58,6 @@ from .const import ( APP_LIST, CURRENT_APP, CURRENT_INPUT, - CUSTOM_APP_NAME, CUSTOM_CONFIG, ENTITY_ID, INPUT_LIST, @@ -180,11 +179,15 @@ async def _test_setup_with_apps( + [ app["name"] for app in device_config[CONF_APPS][CONF_ADDITIONAL_CONFIGS] + if app["name"] not in APP_LIST ] ) else: list_to_test = list(INPUT_LIST_WITH_APPS + APP_LIST) + if CONF_ADDITIONAL_CONFIGS in device_config.get(CONF_APPS, {}): + assert attr["source_list"].count(CURRENT_APP) == 1 + for app_to_remove in INPUT_APPS: if app_to_remove in list_to_test: list_to_test.remove(app_to_remove) @@ -471,14 +474,14 @@ async def test_setup_with_apps_additional_apps_config( hass, "launch_app", SERVICE_SELECT_SOURCE, - {ATTR_INPUT_SOURCE: CURRENT_APP}, - CURRENT_APP, + {ATTR_INPUT_SOURCE: "Netflix"}, + "Netflix", ) await _test_service( hass, "launch_app_config", SERVICE_SELECT_SOURCE, - {ATTR_INPUT_SOURCE: CUSTOM_APP_NAME}, + {ATTR_INPUT_SOURCE: CURRENT_APP}, **CUSTOM_CONFIG, ) From 2a3d688923ea70b7064d8ee43a8647893978ad50 Mon Sep 17 00:00:00 2001 From: Jason Swails Date: Sun, 22 Mar 2020 12:35:01 -0400 Subject: [PATCH 194/431] General code cleanups for lutron_caseta component (#33144) * General code cleanups for lutron_caseta component * black * Update homeassistant/components/lutron_caseta/__init__.py Co-Authored-By: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/lutron_caseta/__init__.py | 6 ++---- homeassistant/components/lutron_caseta/cover.py | 8 ++++---- homeassistant/components/lutron_caseta/light.py | 8 ++++---- homeassistant/components/lutron_caseta/scene.py | 8 ++++---- homeassistant/components/lutron_caseta/switch.py | 8 ++++---- 5 files changed, 18 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index aaac06a6bd5..a3e384fd77b 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -81,9 +81,7 @@ class LutronCasetaDevice(Entity): async def async_added_to_hass(self): """Register callbacks.""" - self._smartbridge.add_subscriber( - self.device_id, self.async_schedule_update_ha_state - ) + self._smartbridge.add_subscriber(self.device_id, self.async_write_ha_state) @property def device_id(self): @@ -108,7 +106,7 @@ class LutronCasetaDevice(Entity): @property def device_state_attributes(self): """Return the state attributes.""" - attr = {"Device ID": self.device_id, "Zone ID": self._device["zone"]} + attr = {"device_id": self.device_id, "zone_id": self._device["zone"]} return attr @property diff --git a/homeassistant/components/lutron_caseta/cover.py b/homeassistant/components/lutron_caseta/cover.py index afd669153e0..60c723b7b42 100644 --- a/homeassistant/components/lutron_caseta/cover.py +++ b/homeassistant/components/lutron_caseta/cover.py @@ -17,14 +17,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta shades as a cover device.""" - devs = [] + entities = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] cover_devices = bridge.get_devices_by_domain(DOMAIN) for cover_device in cover_devices: - dev = LutronCasetaCover(cover_device, bridge) - devs.append(dev) + entity = LutronCasetaCover(cover_device, bridge) + entities.append(entity) - async_add_entities(devs, True) + async_add_entities(entities, True) class LutronCasetaCover(LutronCasetaDevice, CoverDevice): diff --git a/homeassistant/components/lutron_caseta/light.py b/homeassistant/components/lutron_caseta/light.py index 53de8b66311..ba4342ecfce 100644 --- a/homeassistant/components/lutron_caseta/light.py +++ b/homeassistant/components/lutron_caseta/light.py @@ -25,14 +25,14 @@ def to_hass_level(level): async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta lights.""" - devs = [] + entities = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] light_devices = bridge.get_devices_by_domain(DOMAIN) for light_device in light_devices: - dev = LutronCasetaLight(light_device, bridge) - devs.append(dev) + entity = LutronCasetaLight(light_device, bridge) + entities.append(entity) - async_add_entities(devs, True) + async_add_entities(entities, True) class LutronCasetaLight(LutronCasetaDevice, Light): diff --git a/homeassistant/components/lutron_caseta/scene.py b/homeassistant/components/lutron_caseta/scene.py index abdbcaa03cd..593f58f5274 100644 --- a/homeassistant/components/lutron_caseta/scene.py +++ b/homeassistant/components/lutron_caseta/scene.py @@ -10,14 +10,14 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up the Lutron Caseta lights.""" - devs = [] + entities = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] scenes = bridge.get_scenes() for scene in scenes: - dev = LutronCasetaScene(scenes[scene], bridge) - devs.append(dev) + entity = LutronCasetaScene(scenes[scene], bridge) + entities.append(entity) - async_add_entities(devs, True) + async_add_entities(entities, True) class LutronCasetaScene(Scene): diff --git a/homeassistant/components/lutron_caseta/switch.py b/homeassistant/components/lutron_caseta/switch.py index f6eb846ecfb..23cd1db8f79 100644 --- a/homeassistant/components/lutron_caseta/switch.py +++ b/homeassistant/components/lutron_caseta/switch.py @@ -10,15 +10,15 @@ _LOGGER = logging.getLogger(__name__) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up Lutron switch.""" - devs = [] + entities = [] bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] switch_devices = bridge.get_devices_by_domain(DOMAIN) for switch_device in switch_devices: - dev = LutronCasetaLight(switch_device, bridge) - devs.append(dev) + entity = LutronCasetaLight(switch_device, bridge) + entities.append(entity) - async_add_entities(devs, True) + async_add_entities(entities, True) return True From 52ac7285a7eef92ba6bf4ac458518886051c9595 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Sun, 22 Mar 2020 19:40:25 +0100 Subject: [PATCH 195/431] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 816e64d29bf..a01e81789ab 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -71,9 +71,5 @@ jobs: sed -i "s|# py_noaa|py_noaa|g" ${requirement_file} sed -i "s|# bme680|bme680|g" ${requirement_file} sed -i "s|# python-gammu|python-gammu|g" ${requirement_file} - - if [[ "$(buildArch)" =~ arm ]]; then - sed -i "s|# VL53L1X|VL53L1X|g" ${requirement_file} - fi done displayName: 'Prepare requirements files for Hass.io' From 8d2e72cdf6e48112140c259b60ae5899b37020cc Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Sun, 22 Mar 2020 20:25:31 +0100 Subject: [PATCH 196/431] Add pvpc electricity prices integration (#32092) * Add new integration: pvpc_hourly_pricing to add a sensor with the current hourly price of electricity in Spain. Configuration is done by selecting one of the 3 reference tariffs, with 1, 2, or 3 billing periods. * Features config flow, entity registry, RestoreEntity, options flow to change tariff, manual yaml config as integration or sensor platform * Cloud polling sensor with minimal API calls (3/hour at random times) and smart retry; fully async * Only 1 state change / hour (only when the price changes) * At evening, try to download published tomorrow prices, to always store prices info for a window of [3, 27] hours in the future. * Include useful state attributes to program automations to be run at best electric prices. * Add spanish and english translations. * Requires `xmltodict` to parse official xml file with hourly prices for each day. * Update requirements and add to codeowners * Avoid passing in hass as a parameter to the entity Instead, create time change listeners in async_added_to_hass and call async_generate_entity_id before async_add_entities * Fix lint issues * Add tests for config & options flow * Add tests for manual yaml config with entity definition as integration and also as a sensor platform * Fix placement of PLATFORM_SCHEMA and update generated config_flows * Store prices internally linked to UTC timestamps - to deal with days with DST changes - and work with different local timezones * Add availability to sensor to 'expire' the sensor if there is no connection available and current hour is not in the stored prices. Also, turn off logging and retrying if prices can't be downloaded repeatedly, by flagging `data_source_available` as False, so there is no log-flood mess. * Add more tests - to cover behavior in DST changes and complete coverage of sensor logic - to cover abort config flow * fix linter * Better handling of sensor availability and minor enhancements - Emmit 1 error if data source is marked as unavailable (after some retries), and be silent until cloud access is recovered, then emmit 1 warning. - Follow standard of camel_case keys in attributes * Mock aiosession to not access real API, store fixture data - Store a set of daily xml files to test sensor logic for all situations - Mock time and session to run tests with stored API responses - Add availability test to simulate a lost + recovery of cloud access, checking that logging is reasonable: 1 error to flag the continued disconnection + 1 warning in recovery. * Change API endpoint to retrieve JSON data and remove xmltodict from reqs. It seems that this endpoint is more reliable than the XML. * Adapt tests to new API endpoint * Translate tariff labels to plain English and sync the default timeout value for all ways of configuration. * Relax logging levels to meet silver requirements - 1 warning when becoming unavailable, another warning when recovered. - Warnings for unexpected TimeoutError or ClientError - Move the rest to debug level, leaving info for HA internals Also reduce number of API calls from 3 to 2 calls/hour. * Fix requirements * Mod tests to work with timezone Atlantic/Canary and fix state attributes for timezones != reference, by using 3 price prefixes: 'price_last_day_XXh', 'price_next_day_XXh' and 'price_XXh', all generated with local time (backend timezone) * Try to fix CI tests * Externalize pvpc data and simplify sensor.py * add new `aiopvpc` to requirements * Remove data parsing and price logic from here * Replace some constant properties with class variables * Simplify tests for pvpc_hourly_pricing * Fix updater for options flow * Updater always reloads * `tariff` value comes 1st from entry.options, 2nd from entry.data * Fix lint * Bump aiopvpc * Remove options flow and platform setup - Remove PLATFORM_SCHEMA and async_setup_platform - Generate config_entry.unique_id with tariff instead of entity_id, in flow step. - Remove TariffSelectorConfigFlow - Adapt tests to maintain full coverage * Fix docstring on test and rename SENSOR_SCHEMA to SINGLE_SENSOR_SCHEMA to avoid confusion * Remove timeout manual config, fix entry.options usage, simplify unique_id * Simplify tests - No need for a test_setup now, as platform setup is removed and integration setup is already used in `test_availability` - Simplified `_process_time_step`: only one async_fire(EVENT_TIME_CHANGED)/hour * Fix possible duplicated update when source is not available. * Do not access State last_changed for log messages * Do not update until entity is added to hass and call to async_update after 1st download or when recovering access, so async_write_ha_state is not called twice on those. * minor changes * Rename method to select current price and make it a callback --- CODEOWNERS | 1 + .../pvpc_hourly_pricing/.translations/en.json | 18 + .../pvpc_hourly_pricing/.translations/es.json | 18 + .../pvpc_hourly_pricing/__init__.py | 56 ++ .../pvpc_hourly_pricing/config_flow.py | 27 + .../components/pvpc_hourly_pricing/const.py | 8 + .../pvpc_hourly_pricing/manifest.json | 10 + .../components/pvpc_hourly_pricing/sensor.py | 169 ++++ .../pvpc_hourly_pricing/strings.json | 18 + homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + .../pvpc_hourly_pricing/__init__.py | 1 + .../pvpc_hourly_pricing/conftest.py | 56 ++ .../pvpc_hourly_pricing/test_config_flow.py | 79 ++ .../pvpc_hourly_pricing/test_sensor_logic.py | 85 ++ .../PVPC_CURV_DD_2019_10_26.json | 820 +++++++++++++++++ .../PVPC_CURV_DD_2019_10_27.json | 854 ++++++++++++++++++ .../PVPC_CURV_DD_2019_10_29.json | 820 +++++++++++++++++ 19 files changed, 3047 insertions(+) create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/en.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/es.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/__init__.py create mode 100644 homeassistant/components/pvpc_hourly_pricing/config_flow.py create mode 100644 homeassistant/components/pvpc_hourly_pricing/const.py create mode 100644 homeassistant/components/pvpc_hourly_pricing/manifest.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/sensor.py create mode 100644 homeassistant/components/pvpc_hourly_pricing/strings.json create mode 100644 tests/components/pvpc_hourly_pricing/__init__.py create mode 100644 tests/components/pvpc_hourly_pricing/conftest.py create mode 100644 tests/components/pvpc_hourly_pricing/test_config_flow.py create mode 100644 tests/components/pvpc_hourly_pricing/test_sensor_logic.py create mode 100644 tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_26.json create mode 100644 tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_27.json create mode 100644 tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_29.json diff --git a/CODEOWNERS b/CODEOWNERS index f0efe63ada2..f185059c999 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -285,6 +285,7 @@ homeassistant/components/ps4/* @ktnrg45 homeassistant/components/ptvsd/* @swamp-ig homeassistant/components/push/* @dgomes homeassistant/components/pvoutput/* @fabaff +homeassistant/components/pvpc_hourly_pricing/* @azogue homeassistant/components/qld_bushfire/* @exxamalte homeassistant/components/qnap/* @colinodell homeassistant/components/quantum_gateway/* @cisasteelersfan diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/en.json b/homeassistant/components/pvpc_hourly_pricing/.translations/en.json new file mode 100644 index 00000000000..86aaf15c0f1 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/en.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that tariff" + }, + "step": { + "user": { + "data": { + "name": "Sensor Name", + "tariff": "Contracted tariff (1, 2, or 3 periods)" + }, + "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)", + "title": "Tariff selection" + } + }, + "title": "Hourly price of electricity in Spain (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/es.json b/homeassistant/components/pvpc_hourly_pricing/.translations/es.json new file mode 100644 index 00000000000..746edf3043d --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "La integraci\u00f3n ya est\u00e1 configurada con un sensor existente con esa tarifa" + }, + "step": { + "user": { + "data": { + "name": "Nombre del sensor", + "tariff": "Tarifa contratada (1, 2, o 3 periodos)" + }, + "description": "Este sensor utiliza la API oficial de REE para obtener el [precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\\u00f1a.\nPara instrucciones detalladas consulte la [documentación de la integración](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSeleccione la tarifa contratada en base al número de periodos de facturación al día:\n- 1 periodo: normal\n- 2 periodos: discriminación (tarifa nocturna)\n- 3 periodos: coche eléctrico (tarifa nocturna de 3 periodos)", + "title": "Selecci\u00f3n de tarifa" + } + }, + "title": "Precio horario de la electricidad en Espa\u00f1a (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/__init__.py b/homeassistant/components/pvpc_hourly_pricing/__init__.py new file mode 100644 index 00000000000..5930da52313 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/__init__.py @@ -0,0 +1,56 @@ +"""The pvpc_hourly_pricing integration to collect Spain official electric prices.""" +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant +import homeassistant.helpers.config_validation as cv + +from .const import ATTR_TARIFF, DEFAULT_NAME, DEFAULT_TARIFF, DOMAIN, PLATFORM, TARIFFS + +UI_CONFIG_SCHEMA = vol.Schema( + { + vol.Required(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(ATTR_TARIFF, default=DEFAULT_TARIFF): vol.In(TARIFFS), + } +) +CONFIG_SCHEMA = vol.Schema( + {DOMAIN: cv.ensure_list(UI_CONFIG_SCHEMA)}, extra=vol.ALLOW_EXTRA +) + + +async def async_setup(hass: HomeAssistant, config: dict): + """ + Set up the electricity price sensor from configuration.yaml. + + ```yaml + pvpc_hourly_pricing: + - name: PVPC manual ve + tariff: electric_car + - name: PVPC manual nocturna + tariff: discrimination + timeout: 3 + ``` + """ + for conf in config.get(DOMAIN, []): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, data=conf, context={"source": config_entries.SOURCE_IMPORT} + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Set up pvpc hourly pricing from a config entry.""" + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, PLATFORM) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: config_entries.ConfigEntry): + """Unload a config entry.""" + return await hass.config_entries.async_forward_entry_unload(entry, PLATFORM) diff --git a/homeassistant/components/pvpc_hourly_pricing/config_flow.py b/homeassistant/components/pvpc_hourly_pricing/config_flow.py new file mode 100644 index 00000000000..10591e5b82c --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/config_flow.py @@ -0,0 +1,27 @@ +"""Config flow for pvpc_hourly_pricing.""" +from homeassistant import config_entries + +from . import CONF_NAME, UI_CONFIG_SCHEMA +from .const import ATTR_TARIFF, DOMAIN + +_DOMAIN_NAME = DOMAIN + + +class TariffSelectorConfigFlow(config_entries.ConfigFlow, domain=_DOMAIN_NAME): + """Handle a config flow for `pvpc_hourly_pricing` to select the tariff.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is not None: + await self.async_set_unique_id(user_input[ATTR_TARIFF]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) + + return self.async_show_form(step_id="user", data_schema=UI_CONFIG_SCHEMA) + + async def async_step_import(self, import_info): + """Handle import from config file.""" + return await self.async_step_user(import_info) diff --git a/homeassistant/components/pvpc_hourly_pricing/const.py b/homeassistant/components/pvpc_hourly_pricing/const.py new file mode 100644 index 00000000000..d75ad9fe35c --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/const.py @@ -0,0 +1,8 @@ +"""Constant values for pvpc_hourly_pricing.""" +from aiopvpc import TARIFFS + +DOMAIN = "pvpc_hourly_pricing" +PLATFORM = "sensor" +ATTR_TARIFF = "tariff" +DEFAULT_NAME = "PVPC" +DEFAULT_TARIFF = TARIFFS[1] diff --git a/homeassistant/components/pvpc_hourly_pricing/manifest.json b/homeassistant/components/pvpc_hourly_pricing/manifest.json new file mode 100644 index 00000000000..a2f6ec12941 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "pvpc_hourly_pricing", + "name": "Spain electricity hourly pricing (PVPC)", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/pvpc_hourly_pricing", + "requirements": ["aiopvpc==1.0.2"], + "dependencies": [], + "codeowners": ["@azogue"], + "quality_scale": "platinum" +} diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py new file mode 100644 index 00000000000..ff0b01f9ae4 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -0,0 +1,169 @@ +""" +Sensor to collect the reference daily prices of electricity ('PVPC') in Spain. + +For more details about this platform, please refer to the documentation at +https://www.home-assistant.io/integrations/pvpc_hourly_pricing/ +""" +from datetime import timedelta +import logging +from random import randint +from typing import Optional + +from aiopvpc import PVPCData + +from homeassistant import config_entries +from homeassistant.const import CONF_NAME +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.event import ( + async_track_point_in_time, + async_track_time_change, +) +from homeassistant.helpers.restore_state import RestoreEntity +import homeassistant.util.dt as dt_util + +from .const import ATTR_TARIFF + +_LOGGER = logging.getLogger(__name__) + +ATTR_PRICE = "price" +ICON = "mdi:currency-eur" +UNIT = "€/kWh" + +_DEFAULT_TIMEOUT = 10 + + +async def async_setup_entry( + hass: HomeAssistant, config_entry: config_entries.ConfigEntry, async_add_entities +): + """Set up the electricity price sensor from config_entry.""" + name = config_entry.data[CONF_NAME] + pvpc_data_handler = PVPCData( + tariff=config_entry.data[ATTR_TARIFF], + local_timezone=hass.config.time_zone, + websession=async_get_clientsession(hass), + logger=_LOGGER, + timeout=_DEFAULT_TIMEOUT, + ) + async_add_entities( + [ElecPriceSensor(name, config_entry.unique_id, pvpc_data_handler)], False + ) + + +class ElecPriceSensor(RestoreEntity): + """Class to hold the prices of electricity as a sensor.""" + + unit_of_measurement = UNIT + icon = ICON + should_poll = False + + def __init__(self, name, unique_id, pvpc_data_handler): + """Initialize the sensor object.""" + self._name = name + self._unique_id = unique_id + self._pvpc_data = pvpc_data_handler + self._num_retries = 0 + + self._hourly_tracker = None + self._price_tracker = None + + async def async_will_remove_from_hass(self) -> None: + """Cancel listeners for sensor updates.""" + self._hourly_tracker() + self._price_tracker() + + 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: + self._pvpc_data.state = state.state + + # Update 'state' value in hour changes + self._hourly_tracker = async_track_time_change( + self.hass, self.update_current_price, second=[0], minute=[0] + ) + # Update prices at random time, 2 times/hour (don't want to upset API) + random_minute = randint(1, 29) + mins_update = [random_minute, random_minute + 30] + self._price_tracker = async_track_time_change( + self.hass, self.async_update_prices, second=[0], minute=mins_update, + ) + _LOGGER.debug( + "Setup of price sensor %s (%s) with tariff '%s', " + "updating prices each hour at %s min", + self.name, + self.entity_id, + self._pvpc_data.tariff, + mins_update, + ) + await self.async_update_prices(dt_util.utcnow()) + self.update_current_price(dt_util.utcnow()) + + @property + def unique_id(self) -> Optional[str]: + """Return a unique ID.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._pvpc_data.state + + @property + def available(self) -> bool: + """Return True if entity is available.""" + return self._pvpc_data.state_available + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return self._pvpc_data.attributes + + @callback + def update_current_price(self, now): + """Update the sensor state, by selecting the current price for this hour.""" + self._pvpc_data.process_state_and_attributes(now) + self.async_write_ha_state() + + async def async_update_prices(self, now): + """Update electricity prices from the ESIOS API.""" + prices = await self._pvpc_data.async_update_prices(now) + if not prices and self._pvpc_data.source_available: + self._num_retries += 1 + if self._num_retries > 2: + _LOGGER.warning( + "%s: repeated bad data update, mark component as unavailable source", + self.entity_id, + ) + self._pvpc_data.source_available = False + return + + retry_delay = 2 * self._pvpc_data.timeout + _LOGGER.debug( + "%s: Bad update[retry:%d], will try again in %d s", + self.entity_id, + self._num_retries, + retry_delay, + ) + async_track_point_in_time( + self.hass, + self.async_update_prices, + dt_util.now() + timedelta(seconds=retry_delay), + ) + return + + if not prices: + _LOGGER.debug("%s: data source is not yet available", self.entity_id) + return + + self._num_retries = 0 + if not self._pvpc_data.source_available: + self._pvpc_data.source_available = True + _LOGGER.warning("%s: component has recovered data access", self.entity_id) + self.update_current_price(now) diff --git a/homeassistant/components/pvpc_hourly_pricing/strings.json b/homeassistant/components/pvpc_hourly_pricing/strings.json new file mode 100644 index 00000000000..bff5dc2e68f --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/strings.json @@ -0,0 +1,18 @@ +{ + "config": { + "title": "Hourly price of electricity in Spain (PVPC)", + "step": { + "user": { + "title": "Tariff selection", + "description": "This sensor uses official API to get [hourly pricing of electricity (PVPC)](https://www.esios.ree.es/es/pvpc) in Spain.\nFor more precise explanation visit the [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelect the contracted rate based on the number of billing periods per day:\n- 1 period: normal\n- 2 periods: discrimination (nightly rate)\n- 3 periods: electric car (nightly rate of 3 periods)", + "data": { + "name": "Sensor Name", + "tariff": "Contracted tariff (1, 2, or 3 periods)" + } + } + }, + "abort": { + "already_configured": "Integration is already configured with an existing sensor with that tariff" + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index e318def042e..9e9084a9349 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -85,6 +85,7 @@ FLOWS = [ "point", "powerwall", "ps4", + "pvpc_hourly_pricing", "rachio", "rainmachine", "ring", diff --git a/requirements_all.txt b/requirements_all.txt index af8ca7bc562..9bb7cd9f6c3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -196,6 +196,9 @@ aionotion==1.1.0 # homeassistant.components.hunterdouglas_powerview aiopvapi==1.6.14 +# homeassistant.components.pvpc_hourly_pricing +aiopvpc==1.0.2 + # homeassistant.components.webostv aiopylgtv==0.3.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index bcf9a2fea82..db69dbe2ec2 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -80,6 +80,9 @@ aiohue==2.0.0 # homeassistant.components.notion aionotion==1.1.0 +# homeassistant.components.pvpc_hourly_pricing +aiopvpc==1.0.2 + # homeassistant.components.webostv aiopylgtv==0.3.3 diff --git a/tests/components/pvpc_hourly_pricing/__init__.py b/tests/components/pvpc_hourly_pricing/__init__.py new file mode 100644 index 00000000000..f36b721bc11 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/__init__.py @@ -0,0 +1 @@ +"""Tests for the pvpc_hourly_pricing integration.""" diff --git a/tests/components/pvpc_hourly_pricing/conftest.py b/tests/components/pvpc_hourly_pricing/conftest.py new file mode 100644 index 00000000000..a2cfefb1200 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/conftest.py @@ -0,0 +1,56 @@ +"""Tests for the pvpc_hourly_pricing integration.""" +import pytest + +from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN +from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT + +from tests.common import load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker + +FIXTURE_JSON_DATA_2019_10_26 = "PVPC_CURV_DD_2019_10_26.json" +FIXTURE_JSON_DATA_2019_10_27 = "PVPC_CURV_DD_2019_10_27.json" +FIXTURE_JSON_DATA_2019_10_29 = "PVPC_CURV_DD_2019_10_29.json" + + +def check_valid_state(state, tariff: str, value=None, key_attr=None): + """Ensure that sensor has a valid state and attributes.""" + assert state + assert state.attributes[ATTR_UNIT_OF_MEASUREMENT] == "€/kWh" + try: + _ = float(state.state) + # safety margins for current electricity price (it shouldn't be out of [0, 0.2]) + assert -0.1 < float(state.state) < 0.3 + assert state.attributes[ATTR_TARIFF] == tariff + except ValueError: + pass + + if value is not None and isinstance(value, str): + assert state.state == value + elif value is not None: + assert abs(float(state.state) - value) < 1e-6 + if key_attr is not None: + assert abs(float(state.state) - state.attributes[key_attr]) < 1e-6 + + +@pytest.fixture +def pvpc_aioclient_mock(aioclient_mock: AiohttpClientMocker): + """Create a mock config entry.""" + aioclient_mock.get( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-26", + text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_26}"), + ) + aioclient_mock.get( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-27", + text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_27}"), + ) + # missing day + aioclient_mock.get( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-28", + text='{"message":"No values for specified archive"}', + ) + aioclient_mock.get( + "https://api.esios.ree.es/archives/70/download_json?locale=es&date=2019-10-29", + text=load_fixture(f"{DOMAIN}/{FIXTURE_JSON_DATA_2019_10_29}"), + ) + + return aioclient_mock diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py new file mode 100644 index 00000000000..fbbe87fee5f --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -0,0 +1,79 @@ +"""Tests for the pvpc_hourly_pricing config_flow.""" +from datetime import datetime +from unittest.mock import patch + +from pytz import timezone + +from homeassistant import data_entry_flow +from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN +from homeassistant.const import CONF_NAME +from homeassistant.helpers import entity_registry + +from .conftest import check_valid_state + +from tests.common import date_util +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_flow(hass, pvpc_aioclient_mock: AiohttpClientMocker): + """ + Test config flow for pvpc_hourly_pricing. + + - Create a new entry with tariff "normal" + - Check state and attributes + - Check abort when trying to config another with same tariff + - Check removal and add again to check state restoration + """ + hass.config.time_zone = timezone("Europe/Madrid") + mock_data = {"return_time": datetime(2019, 10, 26, 14, 0, tzinfo=date_util.UTC)} + + def mock_now(): + return mock_data["return_time"] + + with patch("homeassistant.util.dt.utcnow", new=mock_now): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff="normal") + assert pvpc_aioclient_mock.call_count == 1 + + # Check abort when configuring another with same tariff + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert pvpc_aioclient_mock.call_count == 1 + + # Check removal + registry = await entity_registry.async_get_registry(hass) + registry_entity = registry.async_get("sensor.test") + assert await hass.config_entries.async_remove(registry_entity.config_entry_id) + + # and add it again with UI + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], {CONF_NAME: "test", ATTR_TARIFF: "normal"} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + state = hass.states.get("sensor.test") + check_valid_state(state, tariff="normal") + assert pvpc_aioclient_mock.call_count == 2 diff --git a/tests/components/pvpc_hourly_pricing/test_sensor_logic.py b/tests/components/pvpc_hourly_pricing/test_sensor_logic.py new file mode 100644 index 00000000000..c6ae6fa57c2 --- /dev/null +++ b/tests/components/pvpc_hourly_pricing/test_sensor_logic.py @@ -0,0 +1,85 @@ +"""Tests for the pvpc_hourly_pricing component.""" +from datetime import datetime, timedelta +import logging +from unittest.mock import patch + +from pytz import timezone + +from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN +from homeassistant.const import CONF_NAME +from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED + +from .conftest import check_valid_state + +from tests.common import async_setup_component, date_util +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def _process_time_step( + hass, mock_data, key_state=None, value=None, tariff="discrimination", delta_min=60 +): + state = hass.states.get("sensor.test_dst") + check_valid_state(state, tariff=tariff, value=value, key_attr=key_state) + + mock_data["return_time"] += timedelta(minutes=delta_min) + hass.bus.async_fire(EVENT_TIME_CHANGED, {ATTR_NOW: mock_data["return_time"]}) + await hass.async_block_till_done() + return state + + +async def test_availability(hass, caplog, pvpc_aioclient_mock: AiohttpClientMocker): + """Test sensor availability and handling of cloud access.""" + hass.config.time_zone = timezone("Europe/Madrid") + config = {DOMAIN: [{CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}]} + mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)} + + def mock_now(): + return mock_data["return_time"] + + with patch("homeassistant.util.dt.utcnow", new=mock_now): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + caplog.clear() + assert pvpc_aioclient_mock.call_count == 2 + + await _process_time_step(hass, mock_data, "price_21h", 0.13896) + await _process_time_step(hass, mock_data, "price_22h", 0.06893) + assert pvpc_aioclient_mock.call_count == 4 + await _process_time_step(hass, mock_data, "price_23h", 0.06935) + assert pvpc_aioclient_mock.call_count == 5 + + # sensor has no more prices, state is "unavailable" from now on + await _process_time_step(hass, mock_data, value="unavailable") + await _process_time_step(hass, mock_data, value="unavailable") + num_errors = sum( + 1 for x in caplog.get_records("call") if x.levelno == logging.ERROR + ) + num_warnings = sum( + 1 for x in caplog.get_records("call") if x.levelno == logging.WARNING + ) + assert num_warnings == 1 + assert num_errors == 0 + assert pvpc_aioclient_mock.call_count == 7 + + # check that it is silent until it becomes available again + caplog.clear() + with caplog.at_level(logging.WARNING): + # silent mode + for _ in range(21): + await _process_time_step(hass, mock_data, value="unavailable") + assert pvpc_aioclient_mock.call_count == 28 + assert len(caplog.messages) == 0 + + # warning about data access recovered + await _process_time_step(hass, mock_data, value="unavailable") + assert pvpc_aioclient_mock.call_count == 29 + assert len(caplog.messages) == 1 + assert caplog.records[0].levelno == logging.WARNING + + # working ok again + await _process_time_step(hass, mock_data, "price_00h", value=0.06821) + assert pvpc_aioclient_mock.call_count == 30 + await _process_time_step(hass, mock_data, "price_01h", value=0.06627) + assert pvpc_aioclient_mock.call_count == 31 + assert len(caplog.messages) == 1 + assert caplog.records[0].levelno == logging.WARNING diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_26.json b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_26.json new file mode 100644 index 00000000000..dd8c73352d9 --- /dev/null +++ b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_26.json @@ -0,0 +1,820 @@ +{ + "PVPC": [ + { + "Dia": "26/10/2019", + "Hora": "00-01", + "GEN": "114,20", + "NOC": "65,17", + "VHC": "69,02", + "COFGEN": "0,000087148314000000", + "COFNOC": "0,000135978057000000", + "COFVHC": "0,000151138804000000", + "PMHGEN": "59,56", + "PMHNOC": "57,22", + "PMHVHC": "59,81", + "SAHGEN": "1,96", + "SAHNOC": "1,89", + "SAHVHC": "1,97", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,14", + "INTGEN": "0,93", + "INTNOC": "0,90", + "INTVHC": "0,94", + "PCAPGEN": "5,54", + "PCAPNOC": "0,93", + "PCAPVHC": "1,31", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,01", + "CCVNOC": "1,86", + "CCVVHC": "1,95" + }, + { + "Dia": "26/10/2019", + "Hora": "01-02", + "GEN": "111,01", + "NOC": "62,10", + "VHC": "59,03", + "COFGEN": "0,000072922194000000", + "COFNOC": "0,000124822445000000", + "COFVHC": "0,000160597191000000", + "PMHGEN": "56,23", + "PMHNOC": "54,03", + "PMHVHC": "52,62", + "SAHGEN": "2,14", + "SAHNOC": "2,05", + "SAHVHC": "2,00", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,56", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,96", + "CCVNOC": "1,82", + "CCVVHC": "1,77" + }, + { + "Dia": "26/10/2019", + "Hora": "02-03", + "GEN": "105,17", + "NOC": "56,48", + "VHC": "53,56", + "COFGEN": "0,000064100056000000", + "COFNOC": "0,000117356595000000", + "COFVHC": "0,000158787037000000", + "PMHGEN": "50,26", + "PMHNOC": "48,29", + "PMHVHC": "47,03", + "SAHGEN": "2,35", + "SAHNOC": "2,26", + "SAHVHC": "2,20", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,55", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,87", + "CCVNOC": "1,73", + "CCVVHC": "1,68" + }, + { + "Dia": "26/10/2019", + "Hora": "03-04", + "GEN": "102,45", + "NOC": "53,87", + "VHC": "51,02", + "COFGEN": "0,000059549798000000", + "COFNOC": "0,000113408113000000", + "COFVHC": "0,000152391581000000", + "PMHGEN": "47,42", + "PMHNOC": "45,57", + "PMHVHC": "44,38", + "SAHGEN": "2,51", + "SAHNOC": "2,41", + "SAHVHC": "2,35", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,56", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,83", + "CCVNOC": "1,69", + "CCVVHC": "1,65" + }, + { + "Dia": "26/10/2019", + "Hora": "04-05", + "GEN": "102,15", + "NOC": "53,58", + "VHC": "50,73", + "COFGEN": "0,000057296575000000", + "COFNOC": "0,000111308472000000", + "COFVHC": "0,000145270809000000", + "PMHGEN": "47,05", + "PMHNOC": "45,21", + "PMHVHC": "44,03", + "SAHGEN": "2,58", + "SAHNOC": "2,48", + "SAHVHC": "2,41", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,56", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,83", + "CCVNOC": "1,69", + "CCVVHC": "1,64" + }, + { + "Dia": "26/10/2019", + "Hora": "05-06", + "GEN": "101,62", + "NOC": "53,13", + "VHC": "50,34", + "COFGEN": "0,000057285870000000", + "COFNOC": "0,000111061995000000", + "COFVHC": "0,000141535570000000", + "PMHGEN": "46,55", + "PMHNOC": "44,76", + "PMHVHC": "43,63", + "SAHGEN": "2,60", + "SAHNOC": "2,50", + "SAHVHC": "2,43", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,90", + "INTVHC": "0,87", + "PCAPGEN": "5,54", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,82", + "CCVNOC": "1,68", + "CCVVHC": "1,64" + }, + { + "Dia": "26/10/2019", + "Hora": "06-07", + "GEN": "102,36", + "NOC": "53,90", + "VHC": "51,08", + "COFGEN": "0,000060011439000000", + "COFNOC": "0,000113191071000000", + "COFVHC": "0,000139395926000000", + "PMHGEN": "46,58", + "PMHNOC": "44,82", + "PMHVHC": "43,69", + "SAHGEN": "3,32", + "SAHNOC": "3,20", + "SAHVHC": "3,12", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,89", + "INTVHC": "0,87", + "PCAPGEN": "5,51", + "PCAPNOC": "0,92", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,82", + "CCVNOC": "1,69", + "CCVVHC": "1,64" + }, + { + "Dia": "26/10/2019", + "Hora": "07-08", + "GEN": "106,73", + "NOC": "58,10", + "VHC": "61,55", + "COFGEN": "0,000067624746000000", + "COFNOC": "0,000113073036000000", + "COFVHC": "0,000130165590000000", + "PMHGEN": "50,24", + "PMHNOC": "48,34", + "PMHVHC": "50,45", + "SAHGEN": "3,98", + "SAHNOC": "3,83", + "SAHVHC": "4,00", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,89", + "INTVHC": "0,93", + "PCAPGEN": "5,50", + "PCAPNOC": "0,92", + "PCAPVHC": "1,30", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,89", + "CCVNOC": "1,75", + "CCVVHC": "1,83" + }, + { + "Dia": "26/10/2019", + "Hora": "08-09", + "GEN": "107,75", + "NOC": "59,43", + "VHC": "62,66", + "COFGEN": "0,000083194704000000", + "COFNOC": "0,000083589950000000", + "COFVHC": "0,000069841029000000", + "PMHGEN": "51,74", + "PMHNOC": "50,02", + "PMHVHC": "51,97", + "SAHGEN": "3,62", + "SAHNOC": "3,50", + "SAHVHC": "3,63", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,91", + "PCAPGEN": "5,40", + "PCAPNOC": "0,91", + "PCAPVHC": "1,27", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,89", + "CCVNOC": "1,76", + "CCVVHC": "1,83" + }, + { + "Dia": "26/10/2019", + "Hora": "09-10", + "GEN": "110,38", + "NOC": "62,09", + "VHC": "65,34", + "COFGEN": "0,000105869478000000", + "COFNOC": "0,000077963480000000", + "COFVHC": "0,000057355982000000", + "PMHGEN": "55,41", + "PMHNOC": "53,64", + "PMHVHC": "55,65", + "SAHGEN": "2,60", + "SAHNOC": "2,52", + "SAHVHC": "2,61", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,87", + "INTVHC": "0,91", + "PCAPGEN": "5,36", + "PCAPNOC": "0,90", + "PCAPVHC": "1,26", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,92", + "CCVNOC": "1,79", + "CCVVHC": "1,86" + }, + { + "Dia": "26/10/2019", + "Hora": "10-11", + "GEN": "108,10", + "NOC": "60,00", + "VHC": "63,02", + "COFGEN": "0,000121833263000000", + "COFNOC": "0,000085468800000000", + "COFVHC": "0,000063770407000000", + "PMHGEN": "53,39", + "PMHNOC": "51,77", + "PMHVHC": "53,58", + "SAHGEN": "2,42", + "SAHNOC": "2,34", + "SAHVHC": "2,42", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,87", + "INTVHC": "0,90", + "PCAPGEN": "5,32", + "PCAPNOC": "0,90", + "PCAPVHC": "1,25", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,88", + "CCVNOC": "1,76", + "CCVVHC": "1,82" + }, + { + "Dia": "26/10/2019", + "Hora": "11-12", + "GEN": "104,11", + "NOC": "56,20", + "VHC": "59,04", + "COFGEN": "0,000125947995000000", + "COFNOC": "0,000085228595000000", + "COFVHC": "0,000064070840000000", + "PMHGEN": "50,02", + "PMHNOC": "48,54", + "PMHVHC": "50,20", + "SAHGEN": "1,89", + "SAHNOC": "1,83", + "SAHVHC": "1,90", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,89", + "INTNOC": "0,87", + "INTVHC": "0,90", + "PCAPGEN": "5,31", + "PCAPNOC": "0,90", + "PCAPVHC": "1,25", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,81", + "CCVNOC": "1,70", + "CCVVHC": "1,76" + }, + { + "Dia": "26/10/2019", + "Hora": "12-13", + "GEN": "103,61", + "NOC": "55,65", + "VHC": "58,52", + "COFGEN": "0,000128302145000000", + "COFNOC": "0,000082279443000000", + "COFVHC": "0,000063904657000000", + "PMHGEN": "49,50", + "PMHNOC": "47,99", + "PMHVHC": "49,67", + "SAHGEN": "1,90", + "SAHNOC": "1,84", + "SAHVHC": "1,90", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,87", + "INTVHC": "0,90", + "PCAPGEN": "5,32", + "PCAPNOC": "0,90", + "PCAPVHC": "1,25", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,81", + "CCVNOC": "1,69", + "CCVVHC": "1,75" + }, + { + "Dia": "26/10/2019", + "Hora": "13-14", + "GEN": "104,03", + "NOC": "122,60", + "VHC": "122,60", + "COFGEN": "0,000134270665000000", + "COFNOC": "0,000080726428000000", + "COFVHC": "0,000063976543000000", + "PMHGEN": "49,98", + "PMHNOC": "50,33", + "PMHVHC": "50,33", + "SAHGEN": "1,85", + "SAHNOC": "1,87", + "SAHVHC": "1,87", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,89", + "INTNOC": "0,90", + "INTVHC": "0,90", + "PCAPGEN": "5,30", + "PCAPNOC": "5,50", + "PCAPVHC": "5,50", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,81", + "CCVNOC": "1,83", + "CCVVHC": "1,83" + }, + { + "Dia": "26/10/2019", + "Hora": "14-15", + "GEN": "103,44", + "NOC": "122,00", + "VHC": "122,00", + "COFGEN": "0,000130580837000000", + "COFNOC": "0,000079392022000000", + "COFVHC": "0,000064422150000000", + "PMHGEN": "49,25", + "PMHNOC": "49,60", + "PMHVHC": "49,60", + "SAHGEN": "1,97", + "SAHNOC": "1,98", + "SAHVHC": "1,98", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,90", + "INTVHC": "0,90", + "PCAPGEN": "5,32", + "PCAPNOC": "5,52", + "PCAPVHC": "5,52", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,81", + "CCVNOC": "1,82", + "CCVVHC": "1,82" + }, + { + "Dia": "26/10/2019", + "Hora": "15-16", + "GEN": "100,57", + "NOC": "119,16", + "VHC": "119,16", + "COFGEN": "0,000114850139000000", + "COFNOC": "0,000070924506000000", + "COFVHC": "0,000056150579000000", + "PMHGEN": "46,19", + "PMHNOC": "46,55", + "PMHVHC": "46,55", + "SAHGEN": "2,15", + "SAHNOC": "2,17", + "SAHVHC": "2,17", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,36", + "PCAPNOC": "5,57", + "PCAPVHC": "5,57", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,77", + "CCVNOC": "1,79", + "CCVVHC": "1,79" + }, + { + "Dia": "26/10/2019", + "Hora": "16-17", + "GEN": "99,90", + "NOC": "118,48", + "VHC": "118,48", + "COFGEN": "0,000105915899000000", + "COFNOC": "0,000065274280000000", + "COFVHC": "0,000051268616000000", + "PMHGEN": "45,44", + "PMHNOC": "45,80", + "PMHVHC": "45,80", + "SAHGEN": "2,25", + "SAHNOC": "2,27", + "SAHVHC": "2,27", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,35", + "PCAPNOC": "5,56", + "PCAPVHC": "5,56", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,76", + "CCVNOC": "1,78", + "CCVVHC": "1,78" + }, + { + "Dia": "26/10/2019", + "Hora": "17-18", + "GEN": "102,97", + "NOC": "121,53", + "VHC": "121,53", + "COFGEN": "0,000104178581000000", + "COFNOC": "0,000063611672000000", + "COFVHC": "0,000049947652000000", + "PMHGEN": "48,62", + "PMHNOC": "48,96", + "PMHVHC": "48,96", + "SAHGEN": "2,14", + "SAHNOC": "2,16", + "SAHVHC": "2,16", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,90", + "INTVHC": "0,90", + "PCAPGEN": "5,33", + "PCAPNOC": "5,53", + "PCAPVHC": "5,53", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,80", + "CCVNOC": "1,82", + "CCVVHC": "1,82" + }, + { + "Dia": "26/10/2019", + "Hora": "18-19", + "GEN": "107,71", + "NOC": "126,30", + "VHC": "126,30", + "COFGEN": "0,000106669089000000", + "COFNOC": "0,000070000350000000", + "COFVHC": "0,000061100876000000", + "PMHGEN": "53,37", + "PMHNOC": "53,74", + "PMHVHC": "53,74", + "SAHGEN": "2,05", + "SAHNOC": "2,06", + "SAHVHC": "2,06", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,90", + "INTVHC": "0,90", + "PCAPGEN": "5,33", + "PCAPNOC": "5,53", + "PCAPVHC": "5,53", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,87", + "CCVNOC": "1,89", + "CCVVHC": "1,89" + }, + { + "Dia": "26/10/2019", + "Hora": "19-20", + "GEN": "118,75", + "NOC": "137,49", + "VHC": "137,49", + "COFGEN": "0,000115010612000000", + "COFNOC": "0,000095780287000000", + "COFVHC": "0,000092687680000000", + "PMHGEN": "64,21", + "PMHNOC": "64,71", + "PMHVHC": "64,71", + "SAHGEN": "2,07", + "SAHNOC": "2,08", + "SAHVHC": "2,08", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,35", + "PCAPNOC": "5,55", + "PCAPVHC": "5,55", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,04", + "CCVNOC": "2,06", + "CCVVHC": "2,06" + }, + { + "Dia": "26/10/2019", + "Hora": "20-21", + "GEN": "124,00", + "NOC": "142,78", + "VHC": "142,78", + "COFGEN": "0,000129085428000000", + "COFNOC": "0,000144302922000000", + "COFVHC": "0,000185612441000000", + "PMHGEN": "69,13", + "PMHNOC": "69,67", + "PMHVHC": "69,67", + "SAHGEN": "2,30", + "SAHNOC": "2,32", + "SAHVHC": "2,32", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,36", + "PCAPNOC": "5,56", + "PCAPVHC": "5,56", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,12", + "CCVNOC": "2,14", + "CCVVHC": "2,14" + }, + { + "Dia": "26/10/2019", + "Hora": "21-22", + "GEN": "124,16", + "NOC": "143,00", + "VHC": "143,00", + "COFGEN": "0,000133109692000000", + "COFNOC": "0,000151101318000000", + "COFVHC": "0,000197574745000000", + "PMHGEN": "68,50", + "PMHNOC": "69,09", + "PMHVHC": "69,09", + "SAHGEN": "3,05", + "SAHNOC": "3,07", + "SAHVHC": "3,07", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,38", + "PCAPNOC": "5,60", + "PCAPVHC": "5,60", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,13", + "CCVNOC": "2,15", + "CCVVHC": "2,15" + }, + { + "Dia": "26/10/2019", + "Hora": "22-23", + "GEN": "120,30", + "NOC": "139,04", + "VHC": "139,04", + "COFGEN": "0,000120157209000000", + "COFNOC": "0,000148137882000000", + "COFVHC": "0,000194906294000000", + "PMHGEN": "64,33", + "PMHNOC": "64,82", + "PMHVHC": "64,82", + "SAHGEN": "3,38", + "SAHNOC": "3,41", + "SAHVHC": "3,41", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,92", + "INTVHC": "0,92", + "PCAPGEN": "5,42", + "PCAPNOC": "5,63", + "PCAPVHC": "5,63", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,08", + "CCVNOC": "2,10", + "CCVVHC": "2,10" + }, + { + "Dia": "26/10/2019", + "Hora": "23-24", + "GEN": "118,05", + "NOC": "69,05", + "VHC": "72,93", + "COFGEN": "0,000103870556000000", + "COFNOC": "0,000146233245000000", + "COFVHC": "0,000182184931000000", + "PMHGEN": "61,54", + "PMHNOC": "59,25", + "PMHVHC": "61,80", + "SAHGEN": "3,85", + "SAHNOC": "3,71", + "SAHVHC": "3,87", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,92", + "INTNOC": "0,89", + "INTVHC": "0,93", + "PCAPGEN": "5,49", + "PCAPNOC": "0,92", + "PCAPVHC": "1,29", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,05", + "CCVNOC": "1,91", + "CCVVHC": "2,00" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_27.json b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_27.json new file mode 100644 index 00000000000..66afc5a91d3 --- /dev/null +++ b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_27.json @@ -0,0 +1,854 @@ +{ + "PVPC": [ + { + "Dia": "27/10/2019", + "Hora": "00-01", + "GEN": "115,15", + "NOC": "65,95", + "VHC": "69,94", + "COFGEN": "0,000083408754000000", + "COFNOC": "0,000125204015000000", + "COFVHC": "0,000143740251000000", + "PMHGEN": "59,13", + "PMHNOC": "56,72", + "PMHVHC": "59,37", + "SAHGEN": "3,28", + "SAHNOC": "3,14", + "SAHVHC": "3,29", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,14", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,94", + "PCAPGEN": "5,58", + "PCAPNOC": "0,93", + "PCAPVHC": "1,32", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,03", + "CCVNOC": "1,88", + "CCVVHC": "1,97" + }, + { + "Dia": "27/10/2019", + "Hora": "01-02", + "GEN": "109,63", + "NOC": "60,60", + "VHC": "57,48", + "COFGEN": "0,000069962863000000", + "COFNOC": "0,000114629494000000", + "COFVHC": "0,000147622130000000", + "PMHGEN": "53,21", + "PMHNOC": "51,01", + "PMHVHC": "49,61", + "SAHGEN": "3,72", + "SAHNOC": "3,57", + "SAHVHC": "3,47", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,95", + "INTNOC": "0,91", + "INTVHC": "0,88", + "PCAPGEN": "5,61", + "PCAPNOC": "0,94", + "PCAPVHC": "0,73", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,95", + "CCVNOC": "1,80", + "CCVVHC": "1,75" + }, + { + "Dia": "27/10/2019", + "Hora": "02-03", + "GEN": "108,41", + "NOC": "59,38", + "VHC": "56,29", + "COFGEN": "0,000065978330000000", + "COFNOC": "0,000111216294000000", + "COFVHC": "0,000145651145000000", + "PMHGEN": "52,09", + "PMHNOC": "49,90", + "PMHVHC": "48,53", + "SAHGEN": "3,62", + "SAHNOC": "3,47", + "SAHVHC": "3,37", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,95", + "INTNOC": "0,91", + "INTVHC": "0,88", + "PCAPGEN": "5,63", + "PCAPNOC": "0,94", + "PCAPVHC": "0,73", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,93", + "CCVNOC": "1,79", + "CCVVHC": "1,73" + }, + { + "Dia": "27/10/2019", + "Hora": "03-04", + "GEN": "108,22", + "NOC": "59,31", + "VHC": "56,27", + "COFGEN": "0,000061999708000000", + "COFNOC": "0,000107809474000000", + "COFVHC": "0,000143671560000000", + "PMHGEN": "51,88", + "PMHNOC": "49,78", + "PMHVHC": "48,45", + "SAHGEN": "3,68", + "SAHNOC": "3,53", + "SAHVHC": "3,44", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,59", + "PCAPNOC": "0,93", + "PCAPVHC": "0,73", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,93", + "CCVNOC": "1,78", + "CCVVHC": "1,73" + }, + { + "Dia": "27/10/2019", + "Hora": "04-05", + "GEN": "107,03", + "NOC": "58,16", + "VHC": "55,10", + "COFGEN": "0,000057358428000000", + "COFNOC": "0,000103595831000000", + "COFVHC": "0,000139122535000000", + "PMHGEN": "50,53", + "PMHNOC": "48,48", + "PMHVHC": "47,15", + "SAHGEN": "3,85", + "SAHNOC": "3,69", + "SAHVHC": "3,59", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,91", + "INTVHC": "0,88", + "PCAPGEN": "5,60", + "PCAPNOC": "0,93", + "PCAPVHC": "0,73", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,91", + "CCVNOC": "1,76", + "CCVVHC": "1,71" + }, + { + "Dia": "27/10/2019", + "Hora": "05-06", + "GEN": "104,79", + "NOC": "56,01", + "VHC": "53,06", + "COFGEN": "0,000055060063000000", + "COFNOC": "0,000101732765000000", + "COFVHC": "0,000134441142000000", + "PMHGEN": "48,28", + "PMHNOC": "46,32", + "PMHVHC": "45,08", + "SAHGEN": "3,91", + "SAHNOC": "3,75", + "SAHVHC": "3,65", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,59", + "PCAPNOC": "0,93", + "PCAPVHC": "0,73", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,87", + "CCVNOC": "1,73", + "CCVVHC": "1,68" + }, + { + "Dia": "27/10/2019", + "Hora": "06-07", + "GEN": "104,56", + "NOC": "55,85", + "VHC": "52,94", + "COFGEN": "0,000054511300000000", + "COFNOC": "0,000101250808000000", + "COFVHC": "0,000131206727000000", + "PMHGEN": "48,10", + "PMHNOC": "46,18", + "PMHVHC": "44,98", + "SAHGEN": "3,90", + "SAHNOC": "3,74", + "SAHVHC": "3,65", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,57", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,87", + "CCVNOC": "1,73", + "CCVVHC": "1,68" + }, + { + "Dia": "27/10/2019", + "Hora": "07-08", + "GEN": "107,72", + "NOC": "58,93", + "VHC": "55,95", + "COFGEN": "0,000056191283000000", + "COFNOC": "0,000102978398000000", + "COFVHC": "0,000130073563000000", + "PMHGEN": "50,23", + "PMHNOC": "48,26", + "PMHVHC": "47,01", + "SAHGEN": "4,89", + "SAHNOC": "4,70", + "SAHVHC": "4,57", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,56", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,91", + "CCVNOC": "1,77", + "CCVVHC": "1,72" + }, + { + "Dia": "27/10/2019", + "Hora": "08-09", + "GEN": "107,80", + "NOC": "59,29", + "VHC": "62,70", + "COFGEN": "0,000060083432000000", + "COFNOC": "0,000100348617000000", + "COFVHC": "0,000118460190000000", + "PMHGEN": "50,94", + "PMHNOC": "49,13", + "PMHVHC": "51,20", + "SAHGEN": "4,38", + "SAHNOC": "4,23", + "SAHVHC": "4,40", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,92", + "INTNOC": "0,89", + "INTVHC": "0,93", + "PCAPGEN": "5,47", + "PCAPNOC": "0,92", + "PCAPVHC": "1,29", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,90", + "CCVNOC": "1,76", + "CCVVHC": "1,84" + }, + { + "Dia": "27/10/2019", + "Hora": "09-10", + "GEN": "106,74", + "NOC": "58,40", + "VHC": "61,63", + "COFGEN": "0,000070236674000000", + "COFNOC": "0,000071273888000000", + "COFVHC": "0,000062511624000000", + "PMHGEN": "50,00", + "PMHNOC": "48,29", + "PMHVHC": "50,22", + "SAHGEN": "4,34", + "SAHNOC": "4,20", + "SAHVHC": "4,36", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,92", + "PCAPGEN": "5,42", + "PCAPNOC": "0,91", + "PCAPVHC": "1,28", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,87", + "CCVNOC": "1,74", + "CCVVHC": "1,82" + }, + { + "Dia": "27/10/2019", + "Hora": "10-11", + "GEN": "106,13", + "NOC": "57,81", + "VHC": "61,02", + "COFGEN": "0,000089379429000000", + "COFNOC": "0,000066131351000000", + "COFVHC": "0,000053107930000000", + "PMHGEN": "50,32", + "PMHNOC": "48,60", + "PMHVHC": "50,54", + "SAHGEN": "3,43", + "SAHNOC": "3,31", + "SAHVHC": "3,44", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,92", + "PCAPGEN": "5,42", + "PCAPNOC": "0,91", + "PCAPVHC": "1,28", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,86", + "CCVNOC": "1,73", + "CCVVHC": "1,81" + }, + { + "Dia": "27/10/2019", + "Hora": "11-12", + "GEN": "105,00", + "NOC": "56,78", + "VHC": "59,91", + "COFGEN": "0,000106229062000000", + "COFNOC": "0,000075658481000000", + "COFVHC": "0,000058816566000000", + "PMHGEN": "50,34", + "PMHNOC": "48,65", + "PMHVHC": "50,56", + "SAHGEN": "2,33", + "SAHNOC": "2,25", + "SAHVHC": "2,34", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,91", + "PCAPGEN": "5,39", + "PCAPNOC": "0,91", + "PCAPVHC": "1,27", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,84", + "CCVNOC": "1,72", + "CCVVHC": "1,79" + }, + { + "Dia": "27/10/2019", + "Hora": "12-13", + "GEN": "105,07", + "NOC": "56,79", + "VHC": "59,92", + "COFGEN": "0,000113739886000000", + "COFNOC": "0,000079251893000000", + "COFVHC": "0,000061868784000000", + "PMHGEN": "50,41", + "PMHNOC": "48,69", + "PMHVHC": "50,59", + "SAHGEN": "2,31", + "SAHNOC": "2,23", + "SAHVHC": "2,32", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,91", + "PCAPGEN": "5,40", + "PCAPNOC": "0,91", + "PCAPVHC": "1,27", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,85", + "CCVNOC": "1,72", + "CCVVHC": "1,79" + }, + { + "Dia": "27/10/2019", + "Hora": "13-14", + "GEN": "104,67", + "NOC": "123,29", + "VHC": "59,59", + "COFGEN": "0,000116885572000000", + "COFNOC": "0,000077561607000000", + "COFVHC": "0,000061189779000000", + "PMHGEN": "50,08", + "PMHNOC": "50,47", + "PMHVHC": "50,30", + "SAHGEN": "2,29", + "SAHNOC": "2,31", + "SAHVHC": "2,30", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,37", + "PCAPNOC": "5,57", + "PCAPVHC": "1,27", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "2,88", + "CCVGEN": "1,83", + "CCVNOC": "1,85", + "CCVVHC": "1,78" + }, + { + "Dia": "27/10/2019", + "Hora": "14-15", + "GEN": "107,41", + "NOC": "126,05", + "VHC": "126,05", + "COFGEN": "0,000122253070000000", + "COFNOC": "0,000076034460000000", + "COFVHC": "0,000059795888000000", + "PMHGEN": "52,87", + "PMHNOC": "53,28", + "PMHVHC": "53,28", + "SAHGEN": "2,20", + "SAHNOC": "2,22", + "SAHVHC": "2,22", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,37", + "PCAPNOC": "5,57", + "PCAPVHC": "5,57", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,87", + "CCVNOC": "1,89", + "CCVVHC": "1,89" + }, + { + "Dia": "27/10/2019", + "Hora": "15-16", + "GEN": "108,36", + "NOC": "127,06", + "VHC": "127,06", + "COFGEN": "0,000120316270000000", + "COFNOC": "0,000073732639000000", + "COFVHC": "0,000059483320000000", + "PMHGEN": "53,68", + "PMHNOC": "54,14", + "PMHVHC": "54,14", + "SAHGEN": "2,29", + "SAHNOC": "2,31", + "SAHVHC": "2,31", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,92", + "INTVHC": "0,92", + "PCAPGEN": "5,39", + "PCAPNOC": "5,60", + "PCAPVHC": "5,60", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,89", + "CCVNOC": "1,91", + "CCVVHC": "1,91" + }, + { + "Dia": "27/10/2019", + "Hora": "16-17", + "GEN": "106,15", + "NOC": "124,78", + "VHC": "124,78", + "COFGEN": "0,000106276301000000", + "COFNOC": "0,000065442255000000", + "COFVHC": "0,000053614900000000", + "PMHGEN": "51,38", + "PMHNOC": "51,78", + "PMHVHC": "51,78", + "SAHGEN": "2,40", + "SAHNOC": "2,42", + "SAHVHC": "2,42", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,92", + "INTVHC": "0,92", + "PCAPGEN": "5,40", + "PCAPNOC": "5,61", + "PCAPVHC": "5,61", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,86", + "CCVNOC": "1,88", + "CCVVHC": "1,88" + }, + { + "Dia": "27/10/2019", + "Hora": "17-18", + "GEN": "105,09", + "NOC": "123,72", + "VHC": "123,72", + "COFGEN": "0,000098092024000000", + "COFNOC": "0,000060340481000000", + "COFVHC": "0,000050280869000000", + "PMHGEN": "51,35", + "PMHNOC": "51,75", + "PMHVHC": "51,75", + "SAHGEN": "1,40", + "SAHNOC": "1,41", + "SAHVHC": "1,41", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,92", + "INTVHC": "0,92", + "PCAPGEN": "5,40", + "PCAPNOC": "5,61", + "PCAPVHC": "5,61", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,85", + "CCVNOC": "1,86", + "CCVVHC": "1,86" + }, + { + "Dia": "27/10/2019", + "Hora": "18-19", + "GEN": "108,12", + "NOC": "126,77", + "VHC": "126,77", + "COFGEN": "0,000095857172000000", + "COFNOC": "0,000058545227000000", + "COFVHC": "0,000049936767000000", + "PMHGEN": "54,41", + "PMHNOC": "54,83", + "PMHVHC": "54,83", + "SAHGEN": "1,35", + "SAHNOC": "1,36", + "SAHVHC": "1,36", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,38", + "PCAPNOC": "5,59", + "PCAPVHC": "5,59", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,89", + "CCVNOC": "1,91", + "CCVVHC": "1,91" + }, + { + "Dia": "27/10/2019", + "Hora": "19-20", + "GEN": "112,76", + "NOC": "131,51", + "VHC": "131,51", + "COFGEN": "0,000099686581000000", + "COFNOC": "0,000063674261000000", + "COFVHC": "0,000057884599000000", + "PMHGEN": "58,53", + "PMHNOC": "59,03", + "PMHVHC": "59,03", + "SAHGEN": "1,77", + "SAHNOC": "1,79", + "SAHVHC": "1,79", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,92", + "INTVHC": "0,92", + "PCAPGEN": "5,40", + "PCAPNOC": "5,62", + "PCAPVHC": "5,62", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,96", + "CCVNOC": "1,98", + "CCVVHC": "1,98" + }, + { + "Dia": "27/10/2019", + "Hora": "20-21", + "GEN": "118,69", + "NOC": "137,48", + "VHC": "137,48", + "COFGEN": "0,000111025948000000", + "COFNOC": "0,000087846097000000", + "COFVHC": "0,000084304207000000", + "PMHGEN": "64,79", + "PMHNOC": "65,35", + "PMHVHC": "65,35", + "SAHGEN": "1,34", + "SAHNOC": "1,35", + "SAHVHC": "1,35", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,92", + "INTVHC": "0,92", + "PCAPGEN": "5,40", + "PCAPNOC": "5,62", + "PCAPVHC": "5,62", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,05", + "CCVNOC": "2,07", + "CCVVHC": "2,07" + }, + { + "Dia": "27/10/2019", + "Hora": "21-22", + "GEN": "121,19", + "NOC": "139,94", + "VHC": "139,94", + "COFGEN": "0,000129356812000000", + "COFNOC": "0,000137580750000000", + "COFVHC": "0,000175068439000000", + "PMHGEN": "66,00", + "PMHNOC": "66,51", + "PMHVHC": "66,51", + "SAHGEN": "2,64", + "SAHNOC": "2,66", + "SAHVHC": "2,66", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,38", + "PCAPNOC": "5,58", + "PCAPVHC": "5,58", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,08", + "CCVNOC": "2,10", + "CCVVHC": "2,10" + }, + { + "Dia": "27/10/2019", + "Hora": "22-23", + "GEN": "120,21", + "NOC": "138,96", + "VHC": "138,96", + "COFGEN": "0,000132818174000000", + "COFNOC": "0,000143862321000000", + "COFVHC": "0,000185393247000000", + "PMHGEN": "65,72", + "PMHNOC": "66,23", + "PMHVHC": "66,23", + "SAHGEN": "1,94", + "SAHNOC": "1,96", + "SAHVHC": "1,96", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,91", + "INTVHC": "0,91", + "PCAPGEN": "5,38", + "PCAPNOC": "5,59", + "PCAPVHC": "5,59", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,07", + "CCVNOC": "2,09", + "CCVVHC": "2,09" + }, + { + "Dia": "27/10/2019", + "Hora": "23-24", + "GEN": "117,85", + "NOC": "68,93", + "VHC": "136,63", + "COFGEN": "0,000117725347000000", + "COFNOC": "0,000138623638000000", + "COFVHC": "0,000180725170000000", + "PMHGEN": "62,92", + "PMHNOC": "60,64", + "PMHVHC": "63,46", + "SAHGEN": "2,28", + "SAHNOC": "2,20", + "SAHVHC": "2,30", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,92", + "INTNOC": "0,89", + "INTVHC": "0,93", + "PCAPGEN": "5,48", + "PCAPNOC": "0,92", + "PCAPVHC": "5,69", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "62,01", + "CCVGEN": "2,05", + "CCVNOC": "1,91", + "CCVVHC": "2,07" + }, + { + "Dia": "27/10/2019", + "Hora": "24-25", + "GEN": "118,42", + "NOC": "69,35", + "VHC": "73,34", + "COFGEN": "0,000097485259000000", + "COFNOC": "0,000133828173000000", + "COFVHC": "0,000166082424000000", + "PMHGEN": "63,21", + "PMHNOC": "60,82", + "PMHVHC": "63,52", + "SAHGEN": "2,51", + "SAHNOC": "2,41", + "SAHVHC": "2,52", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,90", + "INTVHC": "0,94", + "PCAPGEN": "5,52", + "PCAPNOC": "0,92", + "PCAPVHC": "1,30", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,07", + "CCVNOC": "1,92", + "CCVVHC": "2,01" + } + ] +} \ No newline at end of file diff --git a/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_29.json b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_29.json new file mode 100644 index 00000000000..631e77f5e76 --- /dev/null +++ b/tests/fixtures/pvpc_hourly_pricing/PVPC_CURV_DD_2019_10_29.json @@ -0,0 +1,820 @@ +{ + "PVPC": [ + { + "Dia": "29/10/2019", + "Hora": "00-01", + "GEN": "117,17", + "NOC": "68,21", + "VHC": "72,10", + "COFGEN": "0,000077051997000000", + "COFNOC": "0,000124733002000000", + "COFVHC": "0,000143780107000000", + "PMHGEN": "62,55", + "PMHNOC": "60,23", + "PMHVHC": "62,86", + "SAHGEN": "1,96", + "SAHNOC": "1,89", + "SAHVHC": "1,97", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,89", + "INTVHC": "0,93", + "PCAPGEN": "5,50", + "PCAPNOC": "0,92", + "PCAPVHC": "1,30", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,04", + "CCVNOC": "1,90", + "CCVVHC": "1,99" + }, + { + "Dia": "29/10/2019", + "Hora": "01-02", + "GEN": "115,34", + "NOC": "66,27", + "VHC": "63,14", + "COFGEN": "0,000063669626000000", + "COFNOC": "0,000113703738000000", + "COFVHC": "0,000153709241000000", + "PMHGEN": "60,54", + "PMHNOC": "58,17", + "PMHVHC": "56,70", + "SAHGEN": "2,11", + "SAHNOC": "2,02", + "SAHVHC": "1,97", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,90", + "INTVHC": "0,87", + "PCAPGEN": "5,54", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "2,02", + "CCVNOC": "1,88", + "CCVVHC": "1,83" + }, + { + "Dia": "29/10/2019", + "Hora": "02-03", + "GEN": "112,37", + "NOC": "63,40", + "VHC": "60,25", + "COFGEN": "0,000057299719000000", + "COFNOC": "0,000107847932000000", + "COFVHC": "0,000151346355000000", + "PMHGEN": "57,42", + "PMHNOC": "55,17", + "PMHVHC": "53,69", + "SAHGEN": "2,27", + "SAHNOC": "2,18", + "SAHVHC": "2,12", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,57", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,98", + "CCVNOC": "1,84", + "CCVVHC": "1,79" + }, + { + "Dia": "29/10/2019", + "Hora": "03-04", + "GEN": "111,32", + "NOC": "62,39", + "VHC": "59,27", + "COFGEN": "0,000054631496000000", + "COFNOC": "0,000105135123000000", + "COFVHC": "0,000145712713000000", + "PMHGEN": "55,92", + "PMHNOC": "53,73", + "PMHVHC": "52,29", + "SAHGEN": "2,74", + "SAHNOC": "2,63", + "SAHVHC": "2,56", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,57", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,97", + "CCVNOC": "1,82", + "CCVVHC": "1,77" + }, + { + "Dia": "29/10/2019", + "Hora": "04-05", + "GEN": "111,08", + "NOC": "62,17", + "VHC": "59,04", + "COFGEN": "0,000053587732000000", + "COFNOC": "0,000103791403000000", + "COFVHC": "0,000139739507000000", + "PMHGEN": "55,64", + "PMHNOC": "53,46", + "PMHVHC": "52,03", + "SAHGEN": "2,79", + "SAHNOC": "2,68", + "SAHVHC": "2,61", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,14", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,94", + "INTNOC": "0,90", + "INTVHC": "0,88", + "PCAPGEN": "5,56", + "PCAPNOC": "0,93", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,96", + "CCVNOC": "1,82", + "CCVVHC": "1,77" + }, + { + "Dia": "29/10/2019", + "Hora": "05-06", + "GEN": "113,57", + "NOC": "64,62", + "VHC": "61,53", + "COFGEN": "0,000054978965000000", + "COFNOC": "0,000104220226000000", + "COFVHC": "0,000135044065000000", + "PMHGEN": "58,23", + "PMHNOC": "55,99", + "PMHVHC": "54,57", + "SAHGEN": "2,69", + "SAHNOC": "2,58", + "SAHVHC": "2,52", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,93", + "INTNOC": "0,90", + "INTVHC": "0,87", + "PCAPGEN": "5,53", + "PCAPNOC": "0,92", + "PCAPVHC": "0,72", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "2,00", + "CCVNOC": "1,85", + "CCVVHC": "1,80" + }, + { + "Dia": "29/10/2019", + "Hora": "06-07", + "GEN": "113,48", + "NOC": "64,78", + "VHC": "61,84", + "COFGEN": "0,000063808739000000", + "COFNOC": "0,000109956697000000", + "COFVHC": "0,000134904594000000", + "PMHGEN": "58,13", + "PMHNOC": "56,06", + "PMHVHC": "54,78", + "SAHGEN": "2,80", + "SAHNOC": "2,70", + "SAHVHC": "2,64", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,12", + "INTGEN": "0,92", + "INTNOC": "0,89", + "INTVHC": "0,87", + "PCAPGEN": "5,45", + "PCAPNOC": "0,91", + "PCAPVHC": "0,71", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "0,89", + "CCVGEN": "1,98", + "CCVNOC": "1,84", + "CCVVHC": "1,80" + }, + { + "Dia": "29/10/2019", + "Hora": "07-08", + "GEN": "118,40", + "NOC": "69,72", + "VHC": "73,36", + "COFGEN": "0,000086957107000000", + "COFNOC": "0,000119021762000000", + "COFVHC": "0,000131848949000000", + "PMHGEN": "63,73", + "PMHNOC": "61,60", + "PMHVHC": "64,00", + "SAHGEN": "2,12", + "SAHNOC": "2,05", + "SAHVHC": "2,13", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,91", + "PCAPGEN": "5,40", + "PCAPNOC": "0,91", + "PCAPVHC": "1,27", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,05", + "CCVNOC": "1,91", + "CCVVHC": "1,99" + }, + { + "Dia": "29/10/2019", + "Hora": "08-09", + "GEN": "117,71", + "NOC": "69,47", + "VHC": "72,71", + "COFGEN": "0,000103659543000000", + "COFNOC": "0,000093080441000000", + "COFVHC": "0,000078478538000000", + "PMHGEN": "63,59", + "PMHNOC": "61,75", + "PMHVHC": "63,81", + "SAHGEN": "1,76", + "SAHNOC": "1,71", + "SAHVHC": "1,77", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,12", + "FOSVHC": "0,13", + "INTGEN": "0,89", + "INTNOC": "0,86", + "INTVHC": "0,89", + "PCAPGEN": "5,27", + "PCAPNOC": "0,89", + "PCAPVHC": "1,24", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "2,01", + "CCVNOC": "1,89", + "CCVVHC": "1,96" + }, + { + "Dia": "29/10/2019", + "Hora": "09-10", + "GEN": "115,84", + "NOC": "67,79", + "VHC": "70,80", + "COFGEN": "0,000109607743000000", + "COFNOC": "0,000077907419000000", + "COFVHC": "0,000061476325000000", + "PMHGEN": "62,27", + "PMHNOC": "60,57", + "PMHVHC": "62,44", + "SAHGEN": "1,29", + "SAHNOC": "1,25", + "SAHVHC": "1,29", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,12", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,86", + "INTVHC": "0,88", + "PCAPGEN": "5,23", + "PCAPNOC": "0,88", + "PCAPVHC": "1,23", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,98", + "CCVNOC": "1,86", + "CCVVHC": "1,92" + }, + { + "Dia": "29/10/2019", + "Hora": "10-11", + "GEN": "114,70", + "NOC": "66,74", + "VHC": "69,67", + "COFGEN": "0,000115808394000000", + "COFNOC": "0,000078426619000000", + "COFVHC": "0,000062221967000000", + "PMHGEN": "61,05", + "PMHNOC": "59,42", + "PMHVHC": "61,21", + "SAHGEN": "1,41", + "SAHNOC": "1,37", + "SAHVHC": "1,41", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,12", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,86", + "INTVHC": "0,88", + "PCAPGEN": "5,22", + "PCAPNOC": "0,88", + "PCAPVHC": "1,23", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,96", + "CCVNOC": "1,84", + "CCVVHC": "1,90" + }, + { + "Dia": "29/10/2019", + "Hora": "11-12", + "GEN": "114,45", + "NOC": "66,51", + "VHC": "69,43", + "COFGEN": "0,000117753360000000", + "COFNOC": "0,000076432674000000", + "COFVHC": "0,000061112533000000", + "PMHGEN": "60,85", + "PMHNOC": "59,23", + "PMHVHC": "61,01", + "SAHGEN": "1,37", + "SAHNOC": "1,34", + "SAHVHC": "1,38", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,12", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,85", + "INTVHC": "0,88", + "PCAPGEN": "5,21", + "PCAPNOC": "0,88", + "PCAPVHC": "1,23", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,95", + "CCVNOC": "1,84", + "CCVVHC": "1,90" + }, + { + "Dia": "29/10/2019", + "Hora": "12-13", + "GEN": "114,46", + "NOC": "133,04", + "VHC": "69,45", + "COFGEN": "0,000121492044000000", + "COFNOC": "0,000074703573000000", + "COFVHC": "0,000061457855000000", + "PMHGEN": "60,95", + "PMHNOC": "61,33", + "PMHVHC": "61,11", + "SAHGEN": "1,30", + "SAHNOC": "1,31", + "SAHVHC": "1,31", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,88", + "INTVHC": "0,88", + "PCAPGEN": "5,20", + "PCAPNOC": "5,39", + "PCAPVHC": "1,22", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "2,88", + "CCVGEN": "1,95", + "CCVNOC": "1,97", + "CCVVHC": "1,90" + }, + { + "Dia": "29/10/2019", + "Hora": "13-14", + "GEN": "113,37", + "NOC": "131,94", + "VHC": "131,94", + "COFGEN": "0,000126490319000000", + "COFNOC": "0,000074777760000000", + "COFVHC": "0,000060760068000000", + "PMHGEN": "59,86", + "PMHNOC": "60,24", + "PMHVHC": "60,24", + "SAHGEN": "1,32", + "SAHNOC": "1,33", + "SAHVHC": "1,33", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,87", + "INTNOC": "0,88", + "INTVHC": "0,88", + "PCAPGEN": "5,19", + "PCAPNOC": "5,38", + "PCAPVHC": "5,38", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,93", + "CCVNOC": "1,95", + "CCVVHC": "1,95" + }, + { + "Dia": "29/10/2019", + "Hora": "14-15", + "GEN": "112,88", + "NOC": "131,46", + "VHC": "131,46", + "COFGEN": "0,000120771211000000", + "COFNOC": "0,000072095790000000", + "COFVHC": "0,000058765117000000", + "PMHGEN": "59,31", + "PMHNOC": "59,68", + "PMHVHC": "59,68", + "SAHGEN": "1,37", + "SAHNOC": "1,38", + "SAHVHC": "1,38", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,88", + "INTVHC": "0,88", + "PCAPGEN": "5,21", + "PCAPNOC": "5,40", + "PCAPVHC": "5,40", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,93", + "CCVNOC": "1,94", + "CCVVHC": "1,94" + }, + { + "Dia": "29/10/2019", + "Hora": "15-16", + "GEN": "115,75", + "NOC": "134,41", + "VHC": "134,41", + "COFGEN": "0,000110808247000000", + "COFNOC": "0,000066006577000000", + "COFVHC": "0,000053763013000000", + "PMHGEN": "62,14", + "PMHNOC": "62,58", + "PMHVHC": "62,58", + "SAHGEN": "1,34", + "SAHNOC": "1,35", + "SAHVHC": "1,35", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,89", + "INTVHC": "0,89", + "PCAPGEN": "5,23", + "PCAPNOC": "5,42", + "PCAPVHC": "5,42", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "1,98", + "CCVNOC": "1,99", + "CCVVHC": "1,99" + }, + { + "Dia": "29/10/2019", + "Hora": "16-17", + "GEN": "118,08", + "NOC": "136,75", + "VHC": "136,75", + "COFGEN": "0,000107924950000000", + "COFNOC": "0,000063090606000000", + "COFVHC": "0,000052115396000000", + "PMHGEN": "64,48", + "PMHNOC": "64,93", + "PMHVHC": "64,93", + "SAHGEN": "1,31", + "SAHNOC": "1,32", + "SAHVHC": "1,32", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,89", + "INTVHC": "0,89", + "PCAPGEN": "5,22", + "PCAPNOC": "5,42", + "PCAPVHC": "5,42", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,01", + "CCVNOC": "2,03", + "CCVVHC": "2,03" + }, + { + "Dia": "29/10/2019", + "Hora": "17-18", + "GEN": "121,32", + "NOC": "139,95", + "VHC": "139,95", + "COFGEN": "0,000111993776000000", + "COFNOC": "0,000063840323000000", + "COFVHC": "0,000053264660000000", + "PMHGEN": "67,88", + "PMHNOC": "68,30", + "PMHVHC": "68,30", + "SAHGEN": "1,10", + "SAHNOC": "1,11", + "SAHVHC": "1,11", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,88", + "INTVHC": "0,88", + "PCAPGEN": "5,22", + "PCAPNOC": "5,41", + "PCAPVHC": "5,41", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,06", + "CCVNOC": "2,07", + "CCVVHC": "2,07" + }, + { + "Dia": "29/10/2019", + "Hora": "18-19", + "GEN": "126,19", + "NOC": "144,85", + "VHC": "144,85", + "COFGEN": "0,000117118878000000", + "COFNOC": "0,000072058416000000", + "COFVHC": "0,000066417528000000", + "PMHGEN": "69,04", + "PMHNOC": "69,47", + "PMHVHC": "69,47", + "SAHGEN": "4,73", + "SAHNOC": "4,76", + "SAHVHC": "4,76", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,89", + "INTVHC": "0,89", + "PCAPGEN": "5,22", + "PCAPNOC": "5,42", + "PCAPVHC": "5,42", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,13", + "CCVNOC": "2,15", + "CCVVHC": "2,15" + }, + { + "Dia": "29/10/2019", + "Hora": "19-20", + "GEN": "125,34", + "NOC": "144,06", + "VHC": "144,06", + "COFGEN": "0,000128443388000000", + "COFNOC": "0,000098772457000000", + "COFVHC": "0,000100678475000000", + "PMHGEN": "68,61", + "PMHNOC": "69,09", + "PMHVHC": "69,09", + "SAHGEN": "4,31", + "SAHNOC": "4,34", + "SAHVHC": "4,34", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,88", + "INTNOC": "0,89", + "INTVHC": "0,89", + "PCAPGEN": "5,24", + "PCAPNOC": "5,43", + "PCAPVHC": "5,43", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,12", + "CCVNOC": "2,14", + "CCVVHC": "2,14" + }, + { + "Dia": "29/10/2019", + "Hora": "20-21", + "GEN": "120,62", + "NOC": "139,31", + "VHC": "139,31", + "COFGEN": "0,000144847952000000", + "COFNOC": "0,000148736569000000", + "COFVHC": "0,000192706770000000", + "PMHGEN": "67,11", + "PMHNOC": "67,58", + "PMHVHC": "67,58", + "SAHGEN": "1,12", + "SAHNOC": "1,12", + "SAHVHC": "1,12", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,89", + "INTNOC": "0,89", + "INTVHC": "0,89", + "PCAPGEN": "5,27", + "PCAPNOC": "5,47", + "PCAPVHC": "5,47", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,06", + "CCVNOC": "2,07", + "CCVVHC": "2,07" + }, + { + "Dia": "29/10/2019", + "Hora": "21-22", + "GEN": "120,67", + "NOC": "139,36", + "VHC": "139,36", + "COFGEN": "0,000143400205000000", + "COFNOC": "0,000153448551000000", + "COFVHC": "0,000201113372000000", + "PMHGEN": "66,43", + "PMHNOC": "66,90", + "PMHVHC": "66,90", + "SAHGEN": "1,80", + "SAHNOC": "1,81", + "SAHVHC": "1,81", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,89", + "INTNOC": "0,90", + "INTVHC": "0,90", + "PCAPGEN": "5,30", + "PCAPNOC": "5,50", + "PCAPVHC": "5,50", + "TEUGEN": "44,03", + "TEUNOC": "62,01", + "TEUVHC": "62,01", + "CCVGEN": "2,06", + "CCVNOC": "2,08", + "CCVVHC": "2,08" + }, + { + "Dia": "29/10/2019", + "Hora": "22-23", + "GEN": "117,80", + "NOC": "69,35", + "VHC": "136,53", + "COFGEN": "0,000122948482000000", + "COFNOC": "0,000146077289000000", + "COFVHC": "0,000194614149000000", + "PMHGEN": "63,25", + "PMHNOC": "61,28", + "PMHVHC": "63,75", + "SAHGEN": "2,10", + "SAHNOC": "2,03", + "SAHVHC": "2,11", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,90", + "INTNOC": "0,87", + "INTVHC": "0,91", + "PCAPGEN": "5,34", + "PCAPNOC": "0,90", + "PCAPVHC": "5,54", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "62,01", + "CCVGEN": "2,03", + "CCVNOC": "1,90", + "CCVVHC": "2,04" + }, + { + "Dia": "29/10/2019", + "Hora": "23-24", + "GEN": "111,95", + "NOC": "63,48", + "VHC": "66,87", + "COFGEN": "0,000098841799000000", + "COFNOC": "0,000139677463000000", + "COFVHC": "0,000176886301000000", + "PMHGEN": "56,97", + "PMHNOC": "55,07", + "PMHVHC": "57,22", + "SAHGEN": "2,52", + "SAHNOC": "2,44", + "SAHVHC": "2,53", + "FOMGEN": "0,03", + "FOMNOC": "0,03", + "FOMVHC": "0,03", + "FOSGEN": "0,13", + "FOSNOC": "0,13", + "FOSVHC": "0,13", + "INTGEN": "0,91", + "INTNOC": "0,88", + "INTVHC": "0,91", + "PCAPGEN": "5,40", + "PCAPNOC": "0,91", + "PCAPVHC": "1,27", + "TEUGEN": "44,03", + "TEUNOC": "2,22", + "TEUVHC": "2,88", + "CCVGEN": "1,95", + "CCVNOC": "1,82", + "CCVVHC": "1,89" + } + ] +} \ No newline at end of file From 0249daef2eac4d6acbc3a1cb8d3501667728548f Mon Sep 17 00:00:00 2001 From: Quentame Date: Sun, 22 Mar 2020 21:05:27 +0100 Subject: [PATCH 197/431] Bump iCloud to 0.9.6.1 (#33161) --- homeassistant/components/icloud/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index b4ef46cfbaf..fd970ce4441 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,7 +3,7 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.5"], + "requirements": ["pyicloud==0.9.6.1"], "dependencies": [], "codeowners": ["@Quentame"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9bb7cd9f6c3..927c8d35c13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1316,7 +1316,7 @@ pyhomeworks==0.0.6 pyialarm==0.3 # homeassistant.components.icloud -pyicloud==0.9.5 +pyicloud==0.9.6.1 # homeassistant.components.intesishome pyintesishome==1.7.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index db69dbe2ec2..b671e7bd660 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -503,7 +503,7 @@ pyheos==0.6.0 pyhomematic==0.1.65 # homeassistant.components.icloud -pyicloud==0.9.5 +pyicloud==0.9.6.1 # homeassistant.components.ipma pyipma==2.0.5 From f0472f2dc26257693f339464339cc00dc74a3277 Mon Sep 17 00:00:00 2001 From: Matt Snyder Date: Sun, 22 Mar 2020 15:43:45 -0500 Subject: [PATCH 198/431] Fix QVR Pro connection error and add port option (#33070) * Allow port specification. Handle connection error gracefully. * Allow port specification. Handle connection error gracefully. * Alias exception. Requested changes. --- homeassistant/components/qvr_pro/__init__.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/qvr_pro/__init__.py b/homeassistant/components/qvr_pro/__init__.py index f2840d49299..3e10191e48b 100644 --- a/homeassistant/components/qvr_pro/__init__.py +++ b/homeassistant/components/qvr_pro/__init__.py @@ -4,10 +4,11 @@ import logging from pyqvrpro import Client from pyqvrpro.client import AuthenticationError, InsufficientPermissionsError +from requests.exceptions import ConnectionError as RequestsConnectionError import voluptuous as vol from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN -from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME import homeassistant.helpers.config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -29,6 +30,7 @@ CONFIG_SCHEMA = vol.Schema( vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_EXCLUDE_CHANNELS, default=[]): vol.All( cv.ensure_list_csv, [cv.positive_int] ), @@ -49,10 +51,11 @@ def setup(hass, config): user = conf[CONF_USERNAME] password = conf[CONF_PASSWORD] host = conf[CONF_HOST] + port = conf.get(CONF_PORT) excluded_channels = conf[CONF_EXCLUDE_CHANNELS] try: - qvrpro = Client(user, password, host) + qvrpro = Client(user, password, host, port=port) channel_resp = qvrpro.get_channel_list() @@ -62,6 +65,9 @@ def setup(hass, config): except AuthenticationError: _LOGGER.error("Authentication failed") return False + except RequestsConnectionError: + _LOGGER.error("Error connecting to QVR server") + return False channels = [] From ab8c50895ef3cbe93c6c6dac9e230141c3343ec8 Mon Sep 17 00:00:00 2001 From: Ivan Dyedov Date: Sun, 22 Mar 2020 13:58:35 -0700 Subject: [PATCH 199/431] Add fahrenheit support in miflora sensor (#33136) * add support for fahrenheit in miflora sensor * fix error introduced when resolving merge conflict * fix formatting --- homeassistant/components/miflora/sensor.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/miflora/sensor.py b/homeassistant/components/miflora/sensor.py index 776e2151a7e..9d564c6536a 100644 --- a/homeassistant/components/miflora/sensor.py +++ b/homeassistant/components/miflora/sensor.py @@ -16,12 +16,14 @@ from homeassistant.const import ( CONF_NAME, CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START, + TEMP_FAHRENHEIT, UNIT_PERCENTAGE, ) from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity import homeassistant.util.dt as dt_util +from homeassistant.util.temperature import celsius_to_fahrenheit try: import bluepy.btle # noqa: F401 pylint: disable=unused-import @@ -93,7 +95,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= for parameter in config[CONF_MONITORED_CONDITIONS]: name = SENSOR_TYPES[parameter][0] - unit = SENSOR_TYPES[parameter][1] + unit = ( + hass.config.units.temperature_unit + if parameter == "temperature" + else SENSOR_TYPES[parameter][1] + ) icon = SENSOR_TYPES[parameter][2] prefix = config.get(CONF_NAME) @@ -208,6 +214,8 @@ class MiFloraSensor(Entity): if data is not None: _LOGGER.debug("%s = %s", self.name, data) + if self._unit == TEMP_FAHRENHEIT: + data = celsius_to_fahrenheit(data) self.data.append(data) self.last_successful_update = dt_util.utcnow() else: From e344c2ea6405d563899f70b2b98f7125b8f0ce34 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2020 18:28:55 -0500 Subject: [PATCH 200/431] Add gate support to myq, fix bouncy updates (#33124) * Add gate support to myq, fix bouncy updates Switch to DataUpdateCoordinator, previously we would hit the myq api every 60 seconds per device. If you have access to 20 garage doors on the account it means we would have previously tried to update 20 times per minutes. * switch to async_call_later --- homeassistant/components/myq/__init__.py | 14 +++- homeassistant/components/myq/const.py | 31 +++++++- homeassistant/components/myq/cover.py | 94 +++++++++++++++++++++--- 3 files changed, 124 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/myq/__init__.py b/homeassistant/components/myq/__init__.py index 51ad9fb48f0..fc1d374fe43 100644 --- a/homeassistant/components/myq/__init__.py +++ b/homeassistant/components/myq/__init__.py @@ -1,5 +1,6 @@ """The MyQ integration.""" import asyncio +from datetime import timedelta import logging import pymyq @@ -10,8 +11,9 @@ from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import aiohttp_client +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DOMAIN, PLATFORMS +from .const import DOMAIN, MYQ_COORDINATOR, MYQ_GATEWAY, PLATFORMS, UPDATE_INTERVAL _LOGGER = logging.getLogger(__name__) @@ -38,7 +40,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except MyQError: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = myq + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="myq devices", + update_method=myq.update_device_info, + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + hass.data[DOMAIN][entry.entry_id] = {MYQ_GATEWAY: myq, MYQ_COORDINATOR: coordinator} for component in PLATFORMS: hass.async_create_task( diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index 260811e54ce..dcae53bd080 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -1,4 +1,11 @@ """The MyQ integration.""" +from pymyq.device import ( + STATE_CLOSED as MYQ_STATE_CLOSED, + STATE_CLOSING as MYQ_STATE_CLOSING, + STATE_OPEN as MYQ_STATE_OPEN, + STATE_OPENING as MYQ_STATE_OPENING, +) + from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_OPENING DOMAIN = "myq" @@ -10,9 +17,25 @@ MYQ_DEVICE_TYPE_GATE = "gate" MYQ_DEVICE_STATE = "state" MYQ_DEVICE_STATE_ONLINE = "online" + MYQ_TO_HASS = { - "closed": STATE_CLOSED, - "closing": STATE_CLOSING, - "open": STATE_OPEN, - "opening": STATE_OPENING, + MYQ_STATE_CLOSED: STATE_CLOSED, + MYQ_STATE_CLOSING: STATE_CLOSING, + MYQ_STATE_OPEN: STATE_OPEN, + MYQ_STATE_OPENING: STATE_OPENING, } + +MYQ_GATEWAY = "myq_gateway" +MYQ_COORDINATOR = "coordinator" + +# myq has some ratelimits in place +# and 61 seemed to be work every time +UPDATE_INTERVAL = 61 + +# Estimated time it takes myq to start transition from one +# state to the next. +TRANSITION_START_DURATION = 7 + +# Estimated time it takes myq to complete a transition +# from one state to another +TRANSITION_COMPLETE_DURATION = 37 diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 0df61b4d5db..21eca6179dd 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -1,9 +1,12 @@ """Support for MyQ-Enabled Garage Doors.""" import logging +import time import voluptuous as vol from homeassistant.components.cover import ( + DEVICE_CLASS_GARAGE, + DEVICE_CLASS_GATE, PLATFORM_SCHEMA, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -18,9 +21,22 @@ from homeassistant.const import ( STATE_CLOSING, STATE_OPENING, ) +from homeassistant.core import callback from homeassistant.helpers import config_validation as cv +from homeassistant.helpers.event import async_call_later -from .const import DOMAIN, MYQ_DEVICE_STATE, MYQ_DEVICE_STATE_ONLINE, MYQ_TO_HASS +from .const import ( + DOMAIN, + MYQ_COORDINATOR, + MYQ_DEVICE_STATE, + MYQ_DEVICE_STATE_ONLINE, + MYQ_DEVICE_TYPE, + MYQ_DEVICE_TYPE_GATE, + MYQ_GATEWAY, + MYQ_TO_HASS, + TRANSITION_COMPLETE_DURATION, + TRANSITION_START_DURATION, +) _LOGGER = logging.getLogger(__name__) @@ -53,21 +69,32 @@ 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 mysq covers.""" - myq = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([MyQDevice(device) for device in myq.covers.values()], True) + data = hass.data[DOMAIN][config_entry.entry_id] + myq = data[MYQ_GATEWAY] + coordinator = data[MYQ_COORDINATOR] + + async_add_entities( + [MyQDevice(coordinator, device) for device in myq.covers.values()], True + ) class MyQDevice(CoverDevice): """Representation of a MyQ cover.""" - def __init__(self, device): + def __init__(self, coordinator, device): """Initialize with API object, device id.""" + self._coordinator = coordinator self._device = device + self._last_action_timestamp = 0 + self._scheduled_transition_update = None @property def device_class(self): """Define this cover as a garage door.""" - return "garage" + device_type = self._device.device_json.get(MYQ_DEVICE_TYPE) + if device_type is not None and device_type == MYQ_DEVICE_TYPE_GATE: + return DEVICE_CLASS_GATE + return DEVICE_CLASS_GARAGE @property def name(self): @@ -77,6 +104,9 @@ class MyQDevice(CoverDevice): @property def available(self): """Return if the device is online.""" + if not self._coordinator.last_update_success: + return False + # Not all devices report online so assume True if its missing return self._device.device_json[MYQ_DEVICE_STATE].get( MYQ_DEVICE_STATE_ONLINE, True @@ -109,19 +139,41 @@ class MyQDevice(CoverDevice): async def async_close_cover(self, **kwargs): """Issue close command to cover.""" + self._last_action_timestamp = time.time() await self._device.close() - # Writes closing state - self.async_write_ha_state() + self._async_schedule_update_for_transition() async def async_open_cover(self, **kwargs): """Issue open command to cover.""" + self._last_action_timestamp = time.time() await self._device.open() - # Writes opening state + self._async_schedule_update_for_transition() + + @callback + def _async_schedule_update_for_transition(self): self.async_write_ha_state() + # Cancel any previous updates + if self._scheduled_transition_update: + self._scheduled_transition_update() + + # Schedule an update for when we expect the transition + # to be completed so the garage door or gate does not + # seem like its closing or opening for a long time + self._scheduled_transition_update = async_call_later( + self.hass, + TRANSITION_COMPLETE_DURATION, + self._async_complete_schedule_update, + ) + + async def _async_complete_schedule_update(self, _): + """Update status of the cover via coordinator.""" + self._scheduled_transition_update = None + await self._coordinator.async_request_refresh() + async def async_update(self): """Update status of cover.""" - await self._device.update() + await self._coordinator.async_request_refresh() @property def device_info(self): @@ -135,3 +187,27 @@ class MyQDevice(CoverDevice): if self._device.parent_device_id: device_info["via_device"] = (DOMAIN, self._device.parent_device_id) return device_info + + @callback + def _async_consume_update(self): + if time.time() - self._last_action_timestamp <= TRANSITION_START_DURATION: + # If we just started a transition we need + # to prevent a bouncy state + return + + self.async_write_ha_state() + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._coordinator.async_add_listener(self._async_consume_update) + + async def async_will_remove_from_hass(self): + """Undo subscription.""" + self._coordinator.async_remove_listener(self._async_consume_update) + if self._scheduled_transition_update: + self._scheduled_transition_update() From 6e6ad94df61dfdb3043d4ab98398ca132c315a2e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 23 Mar 2020 00:39:37 +0100 Subject: [PATCH 201/431] Integrate dockerbuild (#33168) * Integrate dockerbuild * cleanup --- .dockerignore | 6 ++++++ Dockerfile | 17 +++++++++++++++++ azure-pipelines-release.yml | 8 +++----- build.json | 14 ++++++++++++++ rootfs/etc/services.d/home-assistant/finish | 5 +++++ rootfs/etc/services.d/home-assistant/run | 7 +++++++ 6 files changed, 52 insertions(+), 5 deletions(-) create mode 100644 Dockerfile create mode 100644 build.json create mode 100644 rootfs/etc/services.d/home-assistant/finish create mode 100644 rootfs/etc/services.d/home-assistant/run diff --git a/.dockerignore b/.dockerignore index 3d8c32cfb92..8144367ede1 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,9 +2,15 @@ .git .github config +docs + +# Development +.devcontainer +.vscode # Test related files .tox +tests # Other virtualization methods venv diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000000..8853314ae80 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,17 @@ +ARG BUILD_FROM +FROM ${BUILD_FROM}:6.1.0 + +WORKDIR /usr/src + +## Setup Home Assistant +COPY . homeassistant/ +RUN pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + -r homeassistant/requirements_all.txt -c homeassistant/homeassistant/package_constraints.txt \ + && pip3 install --no-cache-dir --no-index --only-binary=:all: --find-links "${WHEELS_LINKS}" \ + -e ./homeassistant \ + && python3 -m compileall homeassistant/homeassistant + +# Home Assistant S6-Overlay +COPY rootfs / + +WORKDIR /config diff --git a/azure-pipelines-release.yml b/azure-pipelines-release.yml index 34827897749..7bf8e3ddfb2 100644 --- a/azure-pipelines-release.yml +++ b/azure-pipelines-release.yml @@ -14,7 +14,7 @@ schedules: always: true variables: - name: versionBuilder - value: '6.9' + value: '7.2.0' - group: docker - group: github - group: twine @@ -108,11 +108,9 @@ stages: docker run --rm --privileged \ -v ~/.docker:/root/.docker:rw \ -v /run/docker.sock:/run/docker.sock:rw \ - -v $(pwd):/homeassistant:ro \ + -v $(pwd):/data:ro \ homeassistant/amd64-builder:$(versionBuilder) \ - --homeassistant $(homeassistantRelease) "--$(buildArch)" \ - -r https://github.com/home-assistant/hassio-homeassistant \ - -t generic --docker-hub homeassistant + --generic $(homeassistantRelease) "--$(buildArch)" -t /data \ docker run --rm --privileged \ -v ~/.docker:/root/.docker \ diff --git a/build.json b/build.json new file mode 100644 index 00000000000..c61a693af1c --- /dev/null +++ b/build.json @@ -0,0 +1,14 @@ +{ + "image": "homeassistant/{arch}-homeassistant", + "build_from": { + "aarch64": "homeassistant/aarch64-homeassistant-base:7.0.1", + "armhf": "homeassistant/armhf-homeassistant-base:7.0.1", + "armv7": "homeassistant/armv7-homeassistant-base:7.0.1", + "amd64": "homeassistant/amd64-homeassistant-base:7.0.1", + "i386": "homeassistant/i386-homeassistant-base:7.0.1" + }, + "labels": { + "io.hass.type": "core" + }, + "version_tag": true +} diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish new file mode 100644 index 00000000000..84b7abcab8b --- /dev/null +++ b/rootfs/etc/services.d/home-assistant/finish @@ -0,0 +1,5 @@ +#!/usr/bin/execlineb -S0 +# ============================================================================== +# Take down the S6 supervision tree when Home Assistant fails +# ============================================================================== +s6-svscanctl -t /var/run/s6/services \ No newline at end of file diff --git a/rootfs/etc/services.d/home-assistant/run b/rootfs/etc/services.d/home-assistant/run new file mode 100644 index 00000000000..a153db56b61 --- /dev/null +++ b/rootfs/etc/services.d/home-assistant/run @@ -0,0 +1,7 @@ +#!/usr/bin/with-contenv bashio +# ============================================================================== +# Start Home Assistant service +# ============================================================================== +cd /config || bashio::exit.nok "Can't find config folder!" + +exec python3 -m homeassistant --config /config From d3f9408650da467ce0dbde0656837d269054566e Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 23 Mar 2020 01:00:00 +0100 Subject: [PATCH 202/431] Fix dockerfile --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 8853314ae80..647c2b8ac07 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ ARG BUILD_FROM -FROM ${BUILD_FROM}:6.1.0 +FROM ${BUILD_FROM} WORKDIR /usr/src From 087b672449251f484431e247e89a21fc08216dc0 Mon Sep 17 00:00:00 2001 From: Alan Tse Date: Sun, 22 Mar 2020 20:33:55 -0700 Subject: [PATCH 203/431] Add enable_wake_on_start option to Tesla (#33035) --- .../components/tesla/.translations/en.json | 51 ++++++++++--------- homeassistant/components/tesla/__init__.py | 13 ++++- homeassistant/components/tesla/config_flow.py | 16 +++++- homeassistant/components/tesla/const.py | 2 + homeassistant/components/tesla/strings.json | 5 +- tests/components/tesla/test_config_flow.py | 41 +++++++++++++-- 6 files changed, 93 insertions(+), 35 deletions(-) diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json index 8c43f28e04e..3c8017a7d76 100644 --- a/homeassistant/components/tesla/.translations/en.json +++ b/homeassistant/components/tesla/.translations/en.json @@ -1,30 +1,31 @@ { - "config": { - "error": { - "connection_error": "Error connecting; check network and retry", - "identifier_exists": "Email already registered", - "invalid_credentials": "Invalid credentials", - "unknown_error": "Unknown error, please report log info" - }, - "step": { - "user": { - "data": { - "password": "Password", - "username": "Email Address" - }, - "description": "Please enter your information.", - "title": "Tesla - Configuration" - } - }, - "title": "Tesla" + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Seconds between scans" - } - } + "step": { + "user": { + "data": { + "username": "Email Address", + "password": "Password" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Seconds between scans", + "enable_wake_on_start": "Force cars awake on startup" } + } } + } } \ No newline at end of file diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index df0664b8f4c..2d08b48e0af 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -28,8 +28,10 @@ from .config_flow import ( validate_input, ) from .const import ( + CONF_WAKE_ON_START, DATA_LISTENER, DEFAULT_SCAN_INTERVAL, + DEFAULT_WAKE_ON_START, DOMAIN, ICONS, MIN_SCAN_INTERVAL, @@ -71,7 +73,10 @@ async def async_setup(hass, base_config): def _update_entry(email, data=None, options=None): data = data or {} - options = options or {CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL} + options = options or { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_WAKE_ON_START: DEFAULT_WAKE_ON_START, + } for entry in hass.config_entries.async_entries(DOMAIN): if email != entry.title: continue @@ -131,7 +136,11 @@ async def async_setup_entry(hass, config_entry): CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), ) - (refresh_token, access_token) = await controller.connect() + (refresh_token, access_token) = await controller.connect( + wake_if_asleep=config_entry.options.get( + CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START + ) + ) except TeslaException as ex: _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False diff --git a/homeassistant/components/tesla/config_flow.py b/homeassistant/components/tesla/config_flow.py index c719807da9f..b8407653d1b 100644 --- a/homeassistant/components/tesla/config_flow.py +++ b/homeassistant/components/tesla/config_flow.py @@ -15,7 +15,13 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, MIN_SCAN_INTERVAL +from .const import ( + CONF_WAKE_ON_START, + DEFAULT_SCAN_INTERVAL, + DEFAULT_WAKE_ON_START, + DOMAIN, + MIN_SCAN_INTERVAL, +) _LOGGER = logging.getLogger(__name__) @@ -103,7 +109,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): default=self.config_entry.options.get( CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL ), - ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)) + ): vol.All(cv.positive_int, vol.Clamp(min=MIN_SCAN_INTERVAL)), + vol.Optional( + CONF_WAKE_ON_START, + default=self.config_entry.options.get( + CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START + ), + ): bool, } ) return self.async_show_form(step_id="init", data_schema=data_schema) diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index 54cb7a2e071..d7930c01fe8 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -1,7 +1,9 @@ """Const file for Tesla cars.""" +CONF_WAKE_ON_START = "enable_wake_on_start" DOMAIN = "tesla" DATA_LISTENER = "listener" DEFAULT_SCAN_INTERVAL = 660 +DEFAULT_WAKE_ON_START = False MIN_SCAN_INTERVAL = 60 TESLA_COMPONENTS = [ "sensor", diff --git a/homeassistant/components/tesla/strings.json b/homeassistant/components/tesla/strings.json index 831406a0d63..3c8017a7d76 100644 --- a/homeassistant/components/tesla/strings.json +++ b/homeassistant/components/tesla/strings.json @@ -22,9 +22,10 @@ "step": { "init": { "data": { - "scan_interval": "Seconds between scans" + "scan_interval": "Seconds between scans", + "enable_wake_on_start": "Force cars awake on startup" } } } } -} +} \ No newline at end of file diff --git a/tests/components/tesla/test_config_flow.py b/tests/components/tesla/test_config_flow.py index 477583f23fb..00e7ba78cc1 100644 --- a/tests/components/tesla/test_config_flow.py +++ b/tests/components/tesla/test_config_flow.py @@ -4,7 +4,13 @@ from unittest.mock import patch from teslajsonpy import TeslaException from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.tesla.const import DOMAIN, MIN_SCAN_INTERVAL +from homeassistant.components.tesla.const import ( + CONF_WAKE_ON_START, + DEFAULT_SCAN_INTERVAL, + DEFAULT_WAKE_ON_START, + DOMAIN, + MIN_SCAN_INTERVAL, +) from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_PASSWORD, @@ -137,10 +143,34 @@ async def test_option_flow(hass): assert result["step_id"] == "init" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={CONF_SCAN_INTERVAL: 350} + result["flow_id"], + user_input={CONF_SCAN_INTERVAL: 350, CONF_WAKE_ON_START: True}, ) assert result["type"] == "create_entry" - assert result["data"] == {CONF_SCAN_INTERVAL: 350} + assert result["data"] == { + CONF_SCAN_INTERVAL: 350, + CONF_WAKE_ON_START: True, + } + + +async def test_option_flow_defaults(hass): + """Test config flow options.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, options=None) + 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" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={} + ) + assert result["type"] == "create_entry" + assert result["data"] == { + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_WAKE_ON_START: DEFAULT_WAKE_ON_START, + } async def test_option_flow_input_floor(hass): @@ -157,4 +187,7 @@ async def test_option_flow_input_floor(hass): result["flow_id"], user_input={CONF_SCAN_INTERVAL: 1} ) assert result["type"] == "create_entry" - assert result["data"] == {CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL} + assert result["data"] == { + CONF_SCAN_INTERVAL: MIN_SCAN_INTERVAL, + CONF_WAKE_ON_START: DEFAULT_WAKE_ON_START, + } From 0b2a8bf79a7eee472fd78ed28cdbe5e6072f69b6 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2020 23:24:49 -0500 Subject: [PATCH 204/431] Make harmony handle IP address changes (#33173) If the IP address of the harmony hub changed it would not be rediscovered. We now connect to get the unique id and then update config entries with the correct ip if it is already setup. --- homeassistant/components/harmony/__init__.py | 9 ++- .../components/harmony/config_flow.py | 67 ++++++++++++------- homeassistant/components/harmony/remote.py | 6 +- tests/components/harmony/test_config_flow.py | 29 +++++--- 4 files changed, 70 insertions(+), 41 deletions(-) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index c0fddec09cc..f7140bdb400 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -37,11 +37,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") try: - device = HarmonyRemote(name, address, activity, harmony_conf_file, delay_secs) - await device.connect() + device = HarmonyRemote( + name, entry.unique_id, address, activity, harmony_conf_file, delay_secs + ) + connected_ok = await device.connect() except (asyncio.TimeoutError, ValueError, AttributeError): raise ConfigEntryNotReady + if not connected_ok: + raise ConfigEntryNotReady + hass.data[DOMAIN][entry.entry_id] = device entry.add_update_listener(_update_listener) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 8f9e672c9d9..ff7b47d6010 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -26,24 +26,32 @@ DATA_SCHEMA = vol.Schema( ) +async def get_harmony_client_if_available(hass: core.HomeAssistant, ip_address): + """Connect to a harmony hub and fetch info.""" + harmony = HarmonyAPI(ip_address=ip_address) + + try: + if not await harmony.connect(): + await harmony.close() + return None + except harmony_exceptions.TimeOut: + return None + + await harmony.close() + + return harmony + + async def validate_input(hass: core.HomeAssistant, data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - harmony = HarmonyAPI(ip_address=data[CONF_HOST]) - - _LOGGER.debug("harmony:%s", harmony) - - try: - if not await harmony.connect(): - await harmony.close() - raise CannotConnect - except harmony_exceptions.TimeOut: + harmony = await get_harmony_client_if_available(hass, data[CONF_HOST]) + if not harmony: raise CannotConnect unique_id = find_unique_id_for_remote(harmony) - await harmony.close() # As a last resort we get the name from the harmony client # in the event a name was not provided. harmony.name is @@ -74,7 +82,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - info = await validate_input(self.hass, user_input) + validated = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -82,7 +90,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" if "base" not in errors: - return await self._async_create_entry_from_valid_input(info, user_input) + await self.async_set_unique_id(validated[UNIQUE_ID]) + self._abort_if_unique_id_configured() + return await self._async_create_entry_from_valid_input( + validated, user_input + ) # Return form return self.async_show_form( @@ -104,8 +116,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: friendly_name, } - if self._host_already_configured(self.harmony_config): - return self.async_abort(reason="already_configured") + harmony = await get_harmony_client_if_available( + self.hass, self.harmony_config[CONF_HOST] + ) + + if harmony: + unique_id = find_unique_id_for_remote(harmony) + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.harmony_config[CONF_HOST]} + ) + self.harmony_config[UNIQUE_ID] = unique_id return await self.async_step_link() @@ -114,16 +135,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - try: - info = await validate_input(self.hass, self.harmony_config) - except CannotConnect: - errors["base"] = "cannot_connect" - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - errors["base"] = "unknown" - - if "base" not in errors: - return await self._async_create_entry_from_valid_input(info, user_input) + # Everything was validated in async_step_ssdp + # all we do now is create. + return await self._async_create_entry_from_valid_input( + self.harmony_config, {} + ) return self.async_show_form( step_id="link", @@ -146,8 +162,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def _async_create_entry_from_valid_input(self, validated, user_input): """Single path to create the config entry from validated input.""" - await self.async_set_unique_id(validated[UNIQUE_ID]) - self._abort_if_unique_id_configured() + data = {CONF_NAME: validated[CONF_NAME], CONF_HOST: validated[CONF_HOST]} # Options from yaml are preserved, we will pull them out when # we setup the config entry diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 47f7c7f974e..7d23e15a4e7 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -34,7 +34,6 @@ from .const import ( SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, ) -from .util import find_unique_id_for_remote _LOGGER = logging.getLogger(__name__) @@ -130,7 +129,7 @@ def register_services(hass): class HarmonyRemote(remote.RemoteDevice): """Remote representation used to control a Harmony device.""" - def __init__(self, name, host, activity, out_path, delay_secs): + def __init__(self, name, unique_id, host, activity, out_path, delay_secs): """Initialize HarmonyRemote class.""" self._name = name self.host = host @@ -141,6 +140,7 @@ class HarmonyRemote(remote.RemoteDevice): self._config_path = out_path self.delay_secs = delay_secs self._available = False + self._unique_id = unique_id self._undo_dispatch_subscription = None @property @@ -217,7 +217,7 @@ class HarmonyRemote(remote.RemoteDevice): @property def unique_id(self): """Return the unique id.""" - return find_unique_id_for_remote(self._client) + return self._unique_id @property def name(self): diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 4791b4e8d4c..18c0825b6a1 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -91,19 +91,28 @@ async def test_form_ssdp(hass): """Test we get the form with ssdp source.""" await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_SSDP}, - data={ - "friendlyName": "Harmony Hub", - "ssdp_location": "http://192.168.209.238:8088/description", - }, - ) + harmonyapi = _get_mock_harmonyapi(connect=True) + + with patch( + "homeassistant.components.harmony.config_flow.HarmonyAPI", + return_value=harmonyapi, + ): + 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", + }, + ) assert result["type"] == "form" assert result["step_id"] == "link" assert result["errors"] == {} + assert result["description_placeholders"] == { + "host": "Harmony Hub", + "name": "192.168.1.12", + } - harmonyapi = _get_mock_harmonyapi(connect=True) with patch( "homeassistant.components.harmony.config_flow.HarmonyAPI", return_value=harmonyapi, @@ -117,7 +126,7 @@ async def test_form_ssdp(hass): assert result2["type"] == "create_entry" assert result2["title"] == "Harmony Hub" assert result2["data"] == { - "host": "192.168.209.238", + "host": "192.168.1.12", "name": "Harmony Hub", } await hass.async_block_till_done() From fa60e9b03b1770b8b95c87463fc56d6c7f8bbdff Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 22 Mar 2020 23:41:59 -0500 Subject: [PATCH 205/431] Improve sense timeout exception handling (#33127) asyncio.Timeout needs to be trapped as well --- homeassistant/components/sense/__init__.py | 3 ++- homeassistant/components/sense/config_flow.py | 10 +++------- homeassistant/components/sense/const.py | 7 +++++++ homeassistant/components/sense/sensor.py | 5 ++--- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index d7887f7ab01..13452c97088 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -26,6 +26,7 @@ from .const import ( SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, + SENSE_TIMEOUT_EXCEPTIONS, ) _LOGGER = logging.getLogger(__name__) @@ -101,7 +102,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except SenseAuthenticationException: _LOGGER.error("Could not authenticate with sense server") return False - except SenseAPITimeoutException: + except SENSE_TIMEOUT_EXCEPTIONS: raise ConfigEntryNotReady sense_devices_data = SenseDevicesData() diff --git a/homeassistant/components/sense/config_flow.py b/homeassistant/components/sense/config_flow.py index 68bbb9ed932..7a5b04229a1 100644 --- a/homeassistant/components/sense/config_flow.py +++ b/homeassistant/components/sense/config_flow.py @@ -1,17 +1,13 @@ """Config flow for Sense integration.""" import logging -from sense_energy import ( - ASyncSenseable, - SenseAPITimeoutException, - SenseAuthenticationException, -) +from sense_energy import ASyncSenseable, SenseAuthenticationException import voluptuous as vol from homeassistant import config_entries, core from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_TIMEOUT -from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT +from .const import ACTIVE_UPDATE_RATE, DEFAULT_TIMEOUT, SENSE_TIMEOUT_EXCEPTIONS from .const import DOMAIN # pylint:disable=unused-import; pylint:disable=unused-import @@ -55,7 +51,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, user_input) await self.async_set_unique_id(user_input[CONF_EMAIL]) return self.async_create_entry(title=info["title"], data=user_input) - except SenseAPITimeoutException: + except SENSE_TIMEOUT_EXCEPTIONS: errors["base"] = "cannot_connect" except SenseAuthenticationException: errors["base"] = "invalid_auth" diff --git a/homeassistant/components/sense/const.py b/homeassistant/components/sense/const.py index 619956903f2..882c3c9d79f 100644 --- a/homeassistant/components/sense/const.py +++ b/homeassistant/components/sense/const.py @@ -1,4 +1,9 @@ """Constants for monitoring a Sense energy sensor.""" + +import asyncio + +from sense_energy import SenseAPITimeoutException + DOMAIN = "sense" DEFAULT_TIMEOUT = 10 ACTIVE_UPDATE_RATE = 60 @@ -18,6 +23,8 @@ PRODUCTION_ID = "production" ICON = "mdi:flash" +SENSE_TIMEOUT_EXCEPTIONS = (asyncio.TimeoutError, SenseAPITimeoutException) + MDI_ICONS = { "ac": "air-conditioner", "aquarium": "fish", diff --git a/homeassistant/components/sense/sensor.py b/homeassistant/components/sense/sensor.py index 6fe7b59c46c..06cfb90d2b5 100644 --- a/homeassistant/components/sense/sensor.py +++ b/homeassistant/components/sense/sensor.py @@ -2,8 +2,6 @@ from datetime import timedelta import logging -from sense_energy import SenseAPITimeoutException - from homeassistant.const import DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, POWER_WATT from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -24,6 +22,7 @@ from .const import ( SENSE_DEVICE_UPDATE, SENSE_DEVICES_DATA, SENSE_DISCOVERED_DEVICES_DATA, + SENSE_TIMEOUT_EXCEPTIONS, ) MIN_TIME_BETWEEN_DAILY_UPDATES = timedelta(seconds=300) @@ -256,7 +255,7 @@ class SenseTrendsSensor(Entity): try: await self.update_sensor() - except SenseAPITimeoutException: + except SENSE_TIMEOUT_EXCEPTIONS: _LOGGER.error("Timeout retrieving data") return From b09a9fc81a3ba9c1f2204899a38dc9c7dfb39935 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2020 00:29:45 -0500 Subject: [PATCH 206/431] Add config flow for Nuheat (#32885) * Modernize nuheat for new climate platform * Home Assistant state now mirrors the state displayed at mynewheat.com * Remove off mode as the device does not implement and setting was not implemented anyways * Implement missing set_hvac_mode for nuheat * Now shows as unavailable when offline * Add a unique id (serial number) * Fix hvac_mode as it was really implementing hvac_action * Presets now map to the open api spec published at https://api.mynuheat.com/swagger/ * ThermostatModel: scheduleMode * Revert test cleanup as it leaves files behind. Its going to be more invasive to modernize the tests so it will have to come in a new pr * Config flow for nuheat * codeowners * Add an import test as well * remove debug --- CODEOWNERS | 1 + .../components/nuheat/.translations/en.json | 25 ++ homeassistant/components/nuheat/__init__.py | 94 +++++- homeassistant/components/nuheat/climate.py | 67 ++--- .../components/nuheat/config_flow.py | 104 +++++++ homeassistant/components/nuheat/const.py | 9 + homeassistant/components/nuheat/manifest.json | 13 +- homeassistant/components/nuheat/services.yaml | 6 - homeassistant/components/nuheat/strings.json | 25 ++ homeassistant/generated/config_flows.py | 1 + tests/components/nuheat/mocks.py | 121 ++++++++ tests/components/nuheat/test_climate.py | 283 +++++++----------- tests/components/nuheat/test_config_flow.py | 163 ++++++++++ tests/components/nuheat/test_init.py | 44 +-- 14 files changed, 683 insertions(+), 273 deletions(-) create mode 100644 homeassistant/components/nuheat/.translations/en.json create mode 100644 homeassistant/components/nuheat/config_flow.py create mode 100644 homeassistant/components/nuheat/const.py delete mode 100644 homeassistant/components/nuheat/services.yaml create mode 100644 homeassistant/components/nuheat/strings.json create mode 100644 tests/components/nuheat/mocks.py create mode 100644 tests/components/nuheat/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index f185059c999..556ce10b18e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -253,6 +253,7 @@ homeassistant/components/notify/* @home-assistant/core homeassistant/components/notion/* @bachya homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte +homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pvizeli homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla diff --git a/homeassistant/components/nuheat/.translations/en.json b/homeassistant/components/nuheat/.translations/en.json new file mode 100644 index 00000000000..4bfbb8ef62a --- /dev/null +++ b/homeassistant/components/nuheat/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config" : { + "error" : { + "unknown" : "Unexpected error", + "cannot_connect" : "Failed to connect, please try again", + "invalid_auth" : "Invalid authentication", + "invalid_thermostat" : "The thermostat serial number is invalid." + }, + "title" : "NuHeat", + "abort" : { + "already_configured" : "The thermostat is already configured" + }, + "step" : { + "user" : { + "title" : "Connect to the NuHeat", + "description": "You will need to obtain your thermostat’s numeric serial number or ID by logging into https://MyNuHeat.com and selecting your thermostat(s).", + "data" : { + "username" : "Username", + "password" : "Password", + "serial_number" : "Serial number of the thermostat." + } + } + } + } +} diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index 88e10270d18..ff90bb26530 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -1,16 +1,21 @@ """Support for NuHeat thermostats.""" +import asyncio import logging import nuheat +import requests import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv + +from .const import CONF_SERIAL_NUMBER, DOMAIN, PLATFORMS _LOGGER = logging.getLogger(__name__) -DOMAIN = "nuheat" - CONFIG_SCHEMA = vol.Schema( { DOMAIN: vol.Schema( @@ -27,16 +32,81 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): - """Set up the NuHeat thermostat component.""" - conf = config[DOMAIN] - username = conf.get(CONF_USERNAME) - password = conf.get(CONF_PASSWORD) - devices = conf.get(CONF_DEVICES) +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the NuHeat component.""" + hass.data.setdefault(DOMAIN, {}) + conf = config.get(DOMAIN) + if not conf: + return True + + for serial_number in conf[CONF_DEVICES]: + # Since the api currently doesn't permit fetching the serial numbers + # and they have to be specified we create a separate config entry for + # each serial number. This won't increase the number of http + # requests as each thermostat has to be updated anyways. + # This also allows us to validate that the entered valid serial + # numbers and do not end up with a config entry where half of the + # devices work. + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={ + CONF_USERNAME: conf[CONF_USERNAME], + CONF_PASSWORD: conf[CONF_PASSWORD], + CONF_SERIAL_NUMBER: serial_number, + }, + ) + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up NuHeat from a config entry.""" + + conf = entry.data + + username = conf[CONF_USERNAME] + password = conf[CONF_PASSWORD] + serial_number = conf[CONF_SERIAL_NUMBER] api = nuheat.NuHeat(username, password) - api.authenticate() - hass.data[DOMAIN] = (api, devices) - discovery.load_platform(hass, "climate", DOMAIN, {}, config) + try: + await hass.async_add_executor_job(api.authenticate) + except requests.exceptions.Timeout: + raise ConfigEntryNotReady + except requests.exceptions.HTTPError as ex: + if ex.request.status_code > 400 and ex.request.status_code < 500: + _LOGGER.error("Failed to login to nuheat: %s", ex) + return False + raise ConfigEntryNotReady + except Exception as ex: # pylint: disable=broad-except + _LOGGER.error("Failed to login to nuheat: %s", ex) + return False + + hass.data[DOMAIN][entry.entry_id] = (api, serial_number) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index c13e2ab257f..dbd4eed8efb 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD -import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -14,16 +13,10 @@ from homeassistant.components.climate.const import ( SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.const import ( - ATTR_ENTITY_ID, - ATTR_TEMPERATURE, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, -) -import homeassistant.helpers.config_validation as cv +from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.util import Throttle -from . import DOMAIN +from .const import DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -49,55 +42,29 @@ SCHEDULE_MODE_TO_PRESET_MODE_MAP = { value: key for key, value in PRESET_MODE_TO_SCHEDULE_MODE_MAP.items() } -SERVICE_RESUME_PROGRAM = "resume_program" - -RESUME_PROGRAM_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) - SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the NuHeat thermostat(s).""" - if discovery_info is None: - return + api, serial_number = hass.data[DOMAIN][config_entry.entry_id] temperature_unit = hass.config.units.temperature_unit - api, serial_numbers = hass.data[DOMAIN] - thermostats = [ - NuHeatThermostat(api, serial_number, temperature_unit) - for serial_number in serial_numbers - ] - add_entities(thermostats, True) + thermostat = await hass.async_add_executor_job(api.get_thermostat, serial_number) + entity = NuHeatThermostat(thermostat, temperature_unit) - def resume_program_set_service(service): - """Resume the program on the target thermostats.""" - entity_id = service.data.get(ATTR_ENTITY_ID) - if entity_id: - target_thermostats = [ - device for device in thermostats if device.entity_id in entity_id - ] - else: - target_thermostats = thermostats + # No longer need a service as set_hvac_mode to auto does this + # since climate 1.0 has been implemented - for thermostat in target_thermostats: - thermostat.resume_program() - - thermostat.schedule_update_ha_state(True) - - hass.services.register( - DOMAIN, - SERVICE_RESUME_PROGRAM, - resume_program_set_service, - schema=RESUME_PROGRAM_SCHEMA, - ) + async_add_entities([entity], True) class NuHeatThermostat(ClimateDevice): """Representation of a NuHeat Thermostat.""" - def __init__(self, api, serial_number, temperature_unit): + def __init__(self, thermostat, temperature_unit): """Initialize the thermostat.""" - self._thermostat = api.get_thermostat(serial_number) + self._thermostat = thermostat self._temperature_unit = temperature_unit self._force_update = False @@ -140,8 +107,9 @@ class NuHeatThermostat(ClimateDevice): def set_hvac_mode(self, hvac_mode): """Set the system mode.""" + # This is the same as what res if hvac_mode == HVAC_MODE_AUTO: - self._thermostat.schedule_mode = SCHEDULE_RUN + self._thermostat.resume_schedule() elif hvac_mode == HVAC_MODE_HEAT: self._thermostat.schedule_mode = SCHEDULE_HOLD @@ -251,3 +219,12 @@ class NuHeatThermostat(ClimateDevice): def _throttled_update(self, **kwargs): """Get the latest state from the thermostat with a throttle.""" self._thermostat.get_data() + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._thermostat.serial_number)}, + "name": self._thermostat.room, + "manufacturer": MANUFACTURER, + } diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py new file mode 100644 index 00000000000..082cb899ec5 --- /dev/null +++ b/homeassistant/components/nuheat/config_flow.py @@ -0,0 +1,104 @@ +"""Config flow for NuHeat integration.""" +import logging + +import nuheat +import requests.exceptions +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .const import CONF_SERIAL_NUMBER +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Required(CONF_SERIAL_NUMBER): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + api = nuheat.NuHeat(data[CONF_USERNAME], data[CONF_PASSWORD]) + + try: + await hass.async_add_executor_job(api.authenticate) + except requests.exceptions.Timeout: + raise CannotConnect + except requests.exceptions.HTTPError as ex: + if ex.request.status_code > 400 and ex.request.status_code < 500: + raise InvalidAuth + raise CannotConnect + # + # The underlying module throws a generic exception on login failure + # + except Exception: # pylint: disable=broad-except + raise InvalidAuth + + try: + thermostat = await hass.async_add_executor_job( + api.get_thermostat, data[CONF_SERIAL_NUMBER] + ) + except requests.exceptions.HTTPError: + raise InvalidThermostat + + return {"title": thermostat.room, "serial_number": thermostat.serial_number} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for NuHeat.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except InvalidThermostat: + errors["base"] = "invalid_thermostat" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(info["serial_number"]) + 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 + ) + + async def async_step_import(self, user_input): + """Handle import.""" + await self.async_set_unique_id(user_input[CONF_SERIAL_NUMBER]) + self._abort_if_unique_id_configured() + + return await self.async_step_user(user_input) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + +class InvalidThermostat(exceptions.HomeAssistantError): + """Error to indicate there is invalid thermostat.""" diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py new file mode 100644 index 00000000000..e9465d69275 --- /dev/null +++ b/homeassistant/components/nuheat/const.py @@ -0,0 +1,9 @@ +"""Constants for NuHeat thermostats.""" + +DOMAIN = "nuheat" + +PLATFORMS = ["climate"] + +CONF_SERIAL_NUMBER = "serial_number" + +MANUFACTURER = "NuHeat" diff --git a/homeassistant/components/nuheat/manifest.json b/homeassistant/components/nuheat/manifest.json index fa011443245..ef78870854c 100644 --- a/homeassistant/components/nuheat/manifest.json +++ b/homeassistant/components/nuheat/manifest.json @@ -1,8 +1,9 @@ { - "domain": "nuheat", - "name": "NuHeat", - "documentation": "https://www.home-assistant.io/integrations/nuheat", - "requirements": ["nuheat==0.3.0"], - "dependencies": [], - "codeowners": [] + "domain": "nuheat", + "name": "NuHeat", + "documentation": "https://www.home-assistant.io/integrations/nuheat", + "requirements": ["nuheat==0.3.0"], + "dependencies": [], + "codeowners": ["@bdraco"], + "config_flow": true } diff --git a/homeassistant/components/nuheat/services.yaml b/homeassistant/components/nuheat/services.yaml deleted file mode 100644 index 6639fcd9898..00000000000 --- a/homeassistant/components/nuheat/services.yaml +++ /dev/null @@ -1,6 +0,0 @@ -resume_program: - description: Resume the programmed schedule. - fields: - entity_id: - description: Name(s) of entities to change. - example: 'climate.kitchen' diff --git a/homeassistant/components/nuheat/strings.json b/homeassistant/components/nuheat/strings.json new file mode 100644 index 00000000000..4bfbb8ef62a --- /dev/null +++ b/homeassistant/components/nuheat/strings.json @@ -0,0 +1,25 @@ +{ + "config" : { + "error" : { + "unknown" : "Unexpected error", + "cannot_connect" : "Failed to connect, please try again", + "invalid_auth" : "Invalid authentication", + "invalid_thermostat" : "The thermostat serial number is invalid." + }, + "title" : "NuHeat", + "abort" : { + "already_configured" : "The thermostat is already configured" + }, + "step" : { + "user" : { + "title" : "Connect to the NuHeat", + "description": "You will need to obtain your thermostat’s numeric serial number or ID by logging into https://MyNuHeat.com and selecting your thermostat(s).", + "data" : { + "username" : "Username", + "password" : "Password", + "serial_number" : "Serial number of the thermostat." + } + } + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 9e9084a9349..cc36af05da4 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -77,6 +77,7 @@ FLOWS = [ "netatmo", "nexia", "notion", + "nuheat", "opentherm_gw", "openuv", "owntracks", diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py new file mode 100644 index 00000000000..7b7c9d1ac06 --- /dev/null +++ b/tests/components/nuheat/mocks.py @@ -0,0 +1,121 @@ +"""The test for the NuHeat thermostat module.""" +from asynctest.mock import MagicMock, Mock +from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD + +from homeassistant.components.nuheat.const import DOMAIN +from homeassistant.const import CONF_DEVICES, CONF_PASSWORD, CONF_USERNAME + + +def _get_mock_thermostat_run(): + serial_number = "12345" + thermostat = Mock( + serial_number=serial_number, + room="Master bathroom", + online=True, + heating=True, + temperature=2222, + celsius=22, + fahrenheit=72, + max_celsius=69, + max_fahrenheit=157, + min_celsius=5, + min_fahrenheit=41, + schedule_mode=SCHEDULE_RUN, + target_celsius=22, + target_fahrenheit=72, + ) + + thermostat.get_data = Mock() + thermostat.resume_schedule = Mock() + thermostat.schedule_mode = Mock() + return thermostat + + +def _get_mock_thermostat_schedule_hold_unavailable(): + serial_number = "876" + thermostat = Mock( + serial_number=serial_number, + room="Guest bathroom", + online=False, + heating=False, + temperature=12, + celsius=12, + fahrenheit=102, + max_celsius=99, + max_fahrenheit=357, + min_celsius=9, + min_fahrenheit=21, + schedule_mode=SCHEDULE_HOLD, + target_celsius=23, + target_fahrenheit=79, + ) + + thermostat.get_data = Mock() + thermostat.resume_schedule = Mock() + thermostat.schedule_mode = Mock() + return thermostat + + +def _get_mock_thermostat_schedule_hold_available(): + serial_number = "876" + thermostat = Mock( + serial_number=serial_number, + room="Available bathroom", + online=True, + heating=False, + temperature=12, + celsius=12, + fahrenheit=102, + max_celsius=99, + max_fahrenheit=357, + min_celsius=9, + min_fahrenheit=21, + schedule_mode=SCHEDULE_HOLD, + target_celsius=23, + target_fahrenheit=79, + ) + + thermostat.get_data = Mock() + thermostat.resume_schedule = Mock() + thermostat.schedule_mode = Mock() + return thermostat + + +def _get_mock_thermostat_schedule_temporary_hold(): + serial_number = "999" + thermostat = Mock( + serial_number=serial_number, + room="Temp bathroom", + online=True, + heating=False, + temperature=14, + celsius=13, + fahrenheit=202, + max_celsius=39, + max_fahrenheit=357, + min_celsius=3, + min_fahrenheit=31, + schedule_mode=SCHEDULE_TEMPORARY_HOLD, + target_celsius=43, + target_fahrenheit=99, + ) + + thermostat.get_data = Mock() + thermostat.resume_schedule = Mock() + thermostat.schedule_mode = Mock() + return thermostat + + +def _get_mock_nuheat(authenticate=None, get_thermostat=None): + nuheat_mock = MagicMock() + type(nuheat_mock).authenticate = MagicMock() + type(nuheat_mock).get_thermostat = MagicMock(return_value=get_thermostat) + + return nuheat_mock + + +def _mock_get_config(): + """Return a default nuheat config.""" + return { + DOMAIN: {CONF_USERNAME: "me", CONF_PASSWORD: "secret", CONF_DEVICES: [12345]} + } diff --git a/tests/components/nuheat/test_climate.py b/tests/components/nuheat/test_climate.py index af16e3ffc26..7bf52026ef9 100644 --- a/tests/components/nuheat/test_climate.py +++ b/tests/components/nuheat/test_climate.py @@ -1,194 +1,133 @@ """The test for the NuHeat thermostat module.""" -import unittest -from unittest.mock import Mock, patch +from asynctest.mock import patch -from homeassistant.components.climate.const import ( - HVAC_MODE_AUTO, - HVAC_MODE_HEAT, - SUPPORT_PRESET_MODE, - SUPPORT_TARGET_TEMPERATURE, +from homeassistant.components.nuheat.const import DOMAIN +from homeassistant.setup import async_setup_component + +from .mocks import ( + _get_mock_nuheat, + _get_mock_thermostat_run, + _get_mock_thermostat_schedule_hold_available, + _get_mock_thermostat_schedule_hold_unavailable, + _get_mock_thermostat_schedule_temporary_hold, + _mock_get_config, ) -import homeassistant.components.nuheat.climate as nuheat -from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT - -from tests.common import get_test_home_assistant - -SCHEDULE_HOLD = 3 -SCHEDULE_RUN = 1 -SCHEDULE_TEMPORARY_HOLD = 2 -class TestNuHeat(unittest.TestCase): - """Tests for NuHeat climate.""" +async def test_climate_thermostat_run(hass): + """Test a thermostat with the schedule running.""" + mock_thermostat = _get_mock_thermostat_run() + mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) - # pylint: disable=protected-access, no-self-use + with patch( + "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + ): + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() - def setUp(self): # pylint: disable=invalid-name - """Set up test variables.""" - serial_number = "12345" - temperature_unit = "F" + state = hass.states.get("climate.master_bathroom") + assert state.state == "auto" + expected_attributes = { + "current_temperature": 22.2, + "friendly_name": "Master bathroom", + "hvac_action": "heating", + "hvac_modes": ["auto", "heat"], + "max_temp": 69.4, + "min_temp": 5.0, + "preset_mode": "Run Schedule", + "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], + "supported_features": 17, + "temperature": 22.2, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) - thermostat = Mock( - serial_number=serial_number, - room="Master bathroom", - online=True, - heating=True, - temperature=2222, - celsius=22, - fahrenheit=72, - max_celsius=69, - max_fahrenheit=157, - min_celsius=5, - min_fahrenheit=41, - schedule_mode=SCHEDULE_RUN, - target_celsius=22, - target_fahrenheit=72, - ) - thermostat.get_data = Mock() - thermostat.resume_schedule = Mock() +async def test_climate_thermostat_schedule_hold_unavailable(hass): + """Test a thermostat with the schedule hold that is offline.""" + mock_thermostat = _get_mock_thermostat_schedule_hold_unavailable() + mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) - self.api = Mock() - self.api.get_thermostat.return_value = thermostat + with patch( + "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + ): + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() - self.hass = get_test_home_assistant() - self.thermostat = nuheat.NuHeatThermostat( - self.api, serial_number, temperature_unit - ) + state = hass.states.get("climate.guest_bathroom") - def tearDown(self): # pylint: disable=invalid-name - """Stop hass.""" - self.hass.stop() + assert state.state == "unavailable" + expected_attributes = { + "friendly_name": "Guest bathroom", + "hvac_modes": ["auto", "heat"], + "max_temp": 180.6, + "min_temp": -6.1, + "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], + "supported_features": 17, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) - @patch("homeassistant.components.nuheat.climate.NuHeatThermostat") - def test_setup_platform(self, mocked_thermostat): - """Test setup_platform.""" - mocked_thermostat.return_value = self.thermostat - thermostat = mocked_thermostat(self.api, "12345", "F") - thermostats = [thermostat] - self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) +async def test_climate_thermostat_schedule_hold_available(hass): + """Test a thermostat with the schedule hold that is online.""" + mock_thermostat = _get_mock_thermostat_schedule_hold_available() + mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) - config = {} - add_entities = Mock() - discovery_info = {} + with patch( + "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + ): + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() - nuheat.setup_platform(self.hass, config, add_entities, discovery_info) - add_entities.assert_called_once_with(thermostats, True) + state = hass.states.get("climate.available_bathroom") - @patch("homeassistant.components.nuheat.climate.NuHeatThermostat") - def test_resume_program_service(self, mocked_thermostat): - """Test resume program service.""" - mocked_thermostat.return_value = self.thermostat - thermostat = mocked_thermostat(self.api, "12345", "F") - thermostat.resume_program = Mock() - thermostat.schedule_update_ha_state = Mock() - thermostat.entity_id = "climate.master_bathroom" + assert state.state == "auto" + expected_attributes = { + "current_temperature": 38.9, + "friendly_name": "Available bathroom", + "hvac_action": "idle", + "hvac_modes": ["auto", "heat"], + "max_temp": 180.6, + "min_temp": -6.1, + "preset_mode": "Run Schedule", + "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], + "supported_features": 17, + "temperature": 26.1, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) - self.hass.data[nuheat.DOMAIN] = (self.api, ["12345"]) - nuheat.setup_platform(self.hass, {}, Mock(), {}) - # Explicit entity - self.hass.services.call( - nuheat.DOMAIN, - nuheat.SERVICE_RESUME_PROGRAM, - {"entity_id": "climate.master_bathroom"}, - True, - ) +async def test_climate_thermostat_schedule_temporary_hold(hass): + """Test a thermostat with the temporary schedule hold that is online.""" + mock_thermostat = _get_mock_thermostat_schedule_temporary_hold() + mock_nuheat = _get_mock_nuheat(get_thermostat=mock_thermostat) - thermostat.resume_program.assert_called_with() - thermostat.schedule_update_ha_state.assert_called_with(True) + with patch( + "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + ): + assert await async_setup_component(hass, DOMAIN, _mock_get_config()) + await hass.async_block_till_done() - thermostat.resume_program.reset_mock() - thermostat.schedule_update_ha_state.reset_mock() + state = hass.states.get("climate.temp_bathroom") - # All entities - self.hass.services.call(nuheat.DOMAIN, nuheat.SERVICE_RESUME_PROGRAM, {}, True) - - thermostat.resume_program.assert_called_with() - thermostat.schedule_update_ha_state.assert_called_with(True) - - def test_name(self): - """Test name property.""" - assert self.thermostat.name == "Master bathroom" - - def test_supported_features(self): - """Test name property.""" - features = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE - assert self.thermostat.supported_features == features - - def test_temperature_unit(self): - """Test temperature unit.""" - assert self.thermostat.temperature_unit == TEMP_FAHRENHEIT - self.thermostat._temperature_unit = "C" - assert self.thermostat.temperature_unit == TEMP_CELSIUS - - def test_current_temperature(self): - """Test current temperature.""" - assert self.thermostat.current_temperature == 72 - self.thermostat._temperature_unit = "C" - assert self.thermostat.current_temperature == 22 - - def test_current_operation(self): - """Test requested mode.""" - assert self.thermostat.hvac_mode == HVAC_MODE_AUTO - - def test_min_temp(self): - """Test min temp.""" - assert self.thermostat.min_temp == 41 - self.thermostat._temperature_unit = "C" - assert self.thermostat.min_temp == 5 - - def test_max_temp(self): - """Test max temp.""" - assert self.thermostat.max_temp == 157 - self.thermostat._temperature_unit = "C" - assert self.thermostat.max_temp == 69 - - def test_target_temperature(self): - """Test target temperature.""" - assert self.thermostat.target_temperature == 72 - self.thermostat._temperature_unit = "C" - assert self.thermostat.target_temperature == 22 - - def test_operation_list(self): - """Test the operation list.""" - assert self.thermostat.hvac_modes == [HVAC_MODE_AUTO, HVAC_MODE_HEAT] - - def test_resume_program(self): - """Test resume schedule.""" - self.thermostat.resume_program() - self.thermostat._thermostat.resume_schedule.assert_called_once_with() - assert self.thermostat._force_update - - def test_set_temperature(self): - """Test set temperature.""" - self.thermostat.set_temperature(temperature=85) - assert self.thermostat._thermostat.target_fahrenheit == 85 - assert self.thermostat._force_update - - self.thermostat._temperature_unit = "C" - self.thermostat.set_temperature(temperature=23) - assert self.thermostat._thermostat.target_celsius == 23 - assert self.thermostat._force_update - - @patch.object(nuheat.NuHeatThermostat, "_throttled_update") - def test_update_without_throttle(self, throttled_update): - """Test update without throttle.""" - self.thermostat._force_update = True - self.thermostat.update() - throttled_update.assert_called_once_with(no_throttle=True) - assert not self.thermostat._force_update - - @patch.object(nuheat.NuHeatThermostat, "_throttled_update") - def test_update_with_throttle(self, throttled_update): - """Test update with throttle.""" - self.thermostat._force_update = False - self.thermostat.update() - throttled_update.assert_called_once_with() - assert not self.thermostat._force_update - - def test_throttled_update(self): - """Test update with throttle.""" - self.thermostat._throttled_update() - self.thermostat._thermostat.get_data.assert_called_once_with() + assert state.state == "auto" + expected_attributes = { + "current_temperature": 94.4, + "friendly_name": "Temp bathroom", + "hvac_action": "idle", + "hvac_modes": ["auto", "heat"], + "max_temp": 180.6, + "min_temp": -0.6, + "preset_mode": "Run Schedule", + "preset_modes": ["Run Schedule", "Temporary Hold", "Permanent Hold"], + "supported_features": 17, + "temperature": 37.2, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py new file mode 100644 index 00000000000..95987404e44 --- /dev/null +++ b/tests/components/nuheat/test_config_flow.py @@ -0,0 +1,163 @@ +"""Test the NuHeat config flow.""" +from asynctest import patch +import requests + +from homeassistant import config_entries, setup +from homeassistant.components.nuheat.const import CONF_SERIAL_NUMBER, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from .mocks import _get_mock_thermostat_run + + +async def test_form_user(hass): + """Test we get the form with user source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_thermostat = _get_mock_thermostat_run() + + with patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + return_value=True, + ), patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", + return_value=mock_thermostat, + ), patch( + "homeassistant.components.nuheat.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nuheat.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "Master bathroom" + assert result2["data"] == { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_thermostat = _get_mock_thermostat_run() + + with patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + return_value=True, + ), patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", + return_value=mock_thermostat, + ), patch( + "homeassistant.components.nuheat.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nuheat.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "Master bathroom" + assert result["data"] == { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_invalid_thermostat(hass): + """Test we handle invalid thermostats.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + return_value=True, + ), patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", + side_effect=requests.exceptions.HTTPError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_thermostat"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + side_effect=requests.exceptions.Timeout, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} diff --git a/tests/components/nuheat/test_init.py b/tests/components/nuheat/test_init.py index 90a209fd897..01128610462 100644 --- a/tests/components/nuheat/test_init.py +++ b/tests/components/nuheat/test_init.py @@ -1,43 +1,23 @@ """NuHeat component tests.""" -import unittest from unittest.mock import patch -from homeassistant.components import nuheat +from homeassistant.components.nuheat.const import DOMAIN +from homeassistant.setup import async_setup_component -from tests.common import MockDependency, get_test_home_assistant +from .mocks import _get_mock_nuheat VALID_CONFIG = { "nuheat": {"username": "warm", "password": "feet", "devices": "thermostat123"} } +INVALID_CONFIG = {"nuheat": {"username": "warm", "password": "feet"}} -class TestNuHeat(unittest.TestCase): - """Test the NuHeat component.""" +async def test_init_success(hass): + """Test that we can setup with valid config.""" + mock_nuheat = _get_mock_nuheat() - def setUp(self): # pylint: disable=invalid-name - """Initialize the values for this test class.""" - self.hass = get_test_home_assistant() - self.config = VALID_CONFIG - - def tearDown(self): # pylint: disable=invalid-name - """Teardown this test class. Stop hass.""" - self.hass.stop() - - @MockDependency("nuheat") - @patch("homeassistant.helpers.discovery.load_platform") - def test_setup(self, mocked_nuheat, mocked_load): - """Test setting up the NuHeat component.""" - with patch.object(nuheat, "nuheat", mocked_nuheat): - nuheat.setup(self.hass, self.config) - - mocked_nuheat.NuHeat.assert_called_with("warm", "feet") - assert nuheat.DOMAIN in self.hass.data - assert len(self.hass.data[nuheat.DOMAIN]) == 2 - assert isinstance( - self.hass.data[nuheat.DOMAIN][0], type(mocked_nuheat.NuHeat()) - ) - assert self.hass.data[nuheat.DOMAIN][1] == "thermostat123" - - mocked_load.assert_called_with( - self.hass, "climate", nuheat.DOMAIN, {}, self.config - ) + with patch( + "homeassistant.components.nuheat.nuheat.NuHeat", return_value=mock_nuheat, + ): + assert await async_setup_component(hass, DOMAIN, VALID_CONFIG) + await hass.async_block_till_done() From 49ebea2be3f45a4d34776d8f5c2a97b90497a57e Mon Sep 17 00:00:00 2001 From: Jason Swails Date: Mon, 23 Mar 2020 02:51:24 -0400 Subject: [PATCH 207/431] Add support for occupancy/vacancy groups in Lutron Caseta component (#33032) * Add support for Lutron Caseta occupancy/vacancy sensors This follows updates to pylutron-caseta to add support for these devices. This code works for me as a custom component in Home Assistant Core with pylutron-caseta 0.6.0 (currently unreleased). * black formatting * Update requirements_all.txt * Apply black formatting * Resolve some review comments * serial -> unique_id * Black formatting * Resolve linting errors * Add code owner... * Fix isort complaint * Fix remaining isort complaints * Update codeowners * Resolve outstanding review comments * Remove caseta_ --- CODEOWNERS | 1 + .../components/lutron_caseta/__init__.py | 2 +- .../components/lutron_caseta/binary_sensor.py | 56 +++++++++++++++++++ .../components/lutron_caseta/manifest.json | 4 +- requirements_all.txt | 2 +- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/lutron_caseta/binary_sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 556ce10b18e..5cc19809a47 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -211,6 +211,7 @@ homeassistant/components/luci/* @fbradyirl @mzdrale homeassistant/components/luftdaten/* @fabaff homeassistant/components/lupusec/* @majuss homeassistant/components/lutron/* @JonGilmore +homeassistant/components/lutron_caseta/* @swails homeassistant/components/mastodon/* @fabaff homeassistant/components/matrix/* @tinloaf homeassistant/components/mcp23017/* @jardiamj diff --git a/homeassistant/components/lutron_caseta/__init__.py b/homeassistant/components/lutron_caseta/__init__.py index a3e384fd77b..47df6a221dd 100644 --- a/homeassistant/components/lutron_caseta/__init__.py +++ b/homeassistant/components/lutron_caseta/__init__.py @@ -33,7 +33,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan"] +LUTRON_CASETA_COMPONENTS = ["light", "switch", "cover", "scene", "fan", "binary_sensor"] async def async_setup(hass, base_config): diff --git a/homeassistant/components/lutron_caseta/binary_sensor.py b/homeassistant/components/lutron_caseta/binary_sensor.py new file mode 100644 index 00000000000..871f3c28664 --- /dev/null +++ b/homeassistant/components/lutron_caseta/binary_sensor.py @@ -0,0 +1,56 @@ +"""Support for Lutron Caseta Occupancy/Vacancy Sensors.""" +from pylutron_caseta import OCCUPANCY_GROUP_OCCUPIED + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_OCCUPANCY, + BinarySensorDevice, +) + +from . import LUTRON_CASETA_SMARTBRIDGE, LutronCasetaDevice + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Lutron Caseta lights.""" + entities = [] + bridge = hass.data[LUTRON_CASETA_SMARTBRIDGE] + occupancy_groups = bridge.occupancy_groups + for occupancy_group in occupancy_groups.values(): + entity = LutronOccupancySensor(occupancy_group, bridge) + entities.append(entity) + + async_add_entities(entities, True) + + +class LutronOccupancySensor(LutronCasetaDevice, BinarySensorDevice): + """Representation of a Lutron occupancy group.""" + + @property + def device_class(self): + """Flag supported features.""" + return DEVICE_CLASS_OCCUPANCY + + @property + def is_on(self): + """Return the brightness of the light.""" + return self._device["status"] == OCCUPANCY_GROUP_OCCUPIED + + async def async_added_to_hass(self): + """Register callbacks.""" + self._smartbridge.add_occupancy_subscriber( + self.device_id, self.async_write_ha_state + ) + + @property + def device_id(self): + """Return the device ID used for calling pylutron_caseta.""" + return self._device["occupancy_group_id"] + + @property + def unique_id(self): + """Return a unique identifier.""" + return f"occupancygroup_{self.device_id}" + + @property + def device_state_attributes(self): + """Return the state attributes.""" + return {"device_id": self.device_id} diff --git a/homeassistant/components/lutron_caseta/manifest.json b/homeassistant/components/lutron_caseta/manifest.json index 3dd8c8fac2e..856bf285a16 100644 --- a/homeassistant/components/lutron_caseta/manifest.json +++ b/homeassistant/components/lutron_caseta/manifest.json @@ -2,7 +2,7 @@ "domain": "lutron_caseta", "name": "Lutron Caseta", "documentation": "https://www.home-assistant.io/integrations/lutron_caseta", - "requirements": ["pylutron-caseta==0.5.1"], + "requirements": ["pylutron-caseta==0.6.0"], "dependencies": [], - "codeowners": [] + "codeowners": ["@swails"] } diff --git a/requirements_all.txt b/requirements_all.txt index 927c8d35c13..e2f81dbcf20 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1364,7 +1364,7 @@ pylitejet==0.1 pyloopenergy==0.1.3 # homeassistant.components.lutron_caseta -pylutron-caseta==0.5.1 +pylutron-caseta==0.6.0 # homeassistant.components.lutron pylutron==0.2.5 From f9a7c641066c966d023242d4ab5a29ecd2bf7402 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2020 04:14:21 -0500 Subject: [PATCH 208/431] Config flow for doorbird (#33165) * Config flow for doorbird * Discoverable via zeroconf * Fix zeroconf test * add missing return * Add a test for legacy over ride url (will go away when refactored to cloud hooks) * Update homeassistant/components/doorbird/__init__.py Co-Authored-By: Paulus Schoutsen * without getting the hooks its not so useful * Update homeassistant/components/doorbird/config_flow.py Co-Authored-By: Paulus Schoutsen * fix copy pasta * remove identifiers since its in connections * self review fixes Co-authored-by: Paulus Schoutsen --- CODEOWNERS | 2 +- .../components/doorbird/.translations/en.json | 34 +++ homeassistant/components/doorbird/__init__.py | 208 +++++++++----- homeassistant/components/doorbird/camera.py | 96 ++++--- .../components/doorbird/config_flow.py | 156 +++++++++++ homeassistant/components/doorbird/const.py | 17 ++ homeassistant/components/doorbird/entity.py | 36 +++ .../components/doorbird/manifest.json | 15 +- .../components/doorbird/strings.json | 34 +++ homeassistant/components/doorbird/switch.py | 35 ++- homeassistant/components/doorbird/util.py | 19 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 3 +- requirements_test_all.txt | 3 + tests/components/doorbird/__init__.py | 1 + tests/components/doorbird/test_config_flow.py | 258 ++++++++++++++++++ tests/components/zeroconf/test_init.py | 5 +- 17 files changed, 802 insertions(+), 121 deletions(-) create mode 100644 homeassistant/components/doorbird/.translations/en.json create mode 100644 homeassistant/components/doorbird/config_flow.py create mode 100644 homeassistant/components/doorbird/const.py create mode 100644 homeassistant/components/doorbird/entity.py create mode 100644 homeassistant/components/doorbird/strings.json create mode 100644 homeassistant/components/doorbird/util.py create mode 100644 tests/components/doorbird/__init__.py create mode 100644 tests/components/doorbird/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 5cc19809a47..86ad7f5c2db 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -86,7 +86,7 @@ homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff homeassistant/components/directv/* @ctalkington homeassistant/components/discogs/* @thibmaek -homeassistant/components/doorbird/* @oblogic7 +homeassistant/components/doorbird/* @oblogic7 @bdraco homeassistant/components/dsmr_reader/* @depl0y homeassistant/components/dweet/* @fabaff homeassistant/components/dynalite/* @ziv1234 diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json new file mode 100644 index 00000000000..caf3177c681 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/en.json @@ -0,0 +1,34 @@ +{ + "options" : { + "step" : { + "init" : { + "data" : { + "events" : "Comma separated list of events." + }, + "description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } + } + }, + "config" : { + "step" : { + "user" : { + "title" : "Connect to the DoorBird", + "data" : { + "password" : "Password", + "host" : "Host (IP Address)", + "name" : "Device Name", + "username" : "Username" + } + } + }, + "abort" : { + "already_configured" : "This DoorBird is already configured" + }, + "title" : "DoorBird", + "error" : { + "invalid_auth" : "Invalid authentication", + "unknown" : "Unexpected error", + "cannot_connect" : "Failed to connect, please try again" + } + } +} diff --git a/homeassistant/components/doorbird/__init__.py b/homeassistant/components/doorbird/__init__.py index 049681a4aa6..f762a722f2f 100644 --- a/homeassistant/components/doorbird/__init__.py +++ b/homeassistant/components/doorbird/__init__.py @@ -1,5 +1,7 @@ """Support for DoorBird devices.""" +import asyncio import logging +import urllib from urllib.error import HTTPError from doorbirdpy import DoorBird @@ -7,6 +9,7 @@ import voluptuous as vol from homeassistant.components.http import HomeAssistantView from homeassistant.components.logbook import log_entry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -15,17 +18,19 @@ from homeassistant.const import ( CONF_TOKEN, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.util import dt as dt_util, slugify -_LOGGER = logging.getLogger(__name__) +from .const import CONF_EVENTS, DOMAIN, DOOR_STATION, DOOR_STATION_INFO, PLATFORMS +from .util import get_doorstation_by_token -DOMAIN = "doorbird" +_LOGGER = logging.getLogger(__name__) API_URL = f"/api/{DOMAIN}" CONF_CUSTOM_URL = "hass_url_override" -CONF_EVENTS = "events" RESET_DEVICE_FAVORITES = "doorbird_reset_favorites" @@ -51,72 +56,24 @@ CONFIG_SCHEMA = vol.Schema( ) -def setup(hass, config): +async def async_setup(hass: HomeAssistant, config: dict): """Set up the DoorBird component.""" + hass.data.setdefault(DOMAIN, {}) + # Provide an endpoint for the doorstations to call to trigger events hass.http.register_view(DoorBirdRequestView) - doorstations = [] + if DOMAIN in config and CONF_DEVICES in config[DOMAIN]: + for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): + if CONF_NAME not in doorstation_config: + doorstation_config[CONF_NAME] = f"DoorBird {index + 1}" - for index, doorstation_config in enumerate(config[DOMAIN][CONF_DEVICES]): - device_ip = doorstation_config.get(CONF_HOST) - username = doorstation_config.get(CONF_USERNAME) - password = doorstation_config.get(CONF_PASSWORD) - custom_url = doorstation_config.get(CONF_CUSTOM_URL) - events = doorstation_config.get(CONF_EVENTS) - token = doorstation_config.get(CONF_TOKEN) - name = doorstation_config.get(CONF_NAME) or f"DoorBird {index + 1}" - - try: - device = DoorBird(device_ip, username, password) - status = device.ready() - except OSError as oserr: - _LOGGER.error( - "Failed to setup doorbird at %s: %s; not retrying", device_ip, oserr - ) - continue - - if status[0]: - doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) - doorstations.append(doorstation) - _LOGGER.info( - 'Connected to DoorBird "%s" as %s@%s', - doorstation.name, - username, - device_ip, - ) - elif status[1] == 401: - _LOGGER.error( - "Authorization rejected by DoorBird for %s@%s", username, device_ip - ) - return False - else: - _LOGGER.error( - "Could not connect to DoorBird as %s@%s: Error %s", - username, - device_ip, - str(status[1]), - ) - return False - - # Subscribe to doorbell or motion events - if events: - try: - doorstation.register_events(hass) - except HTTPError: - hass.components.persistent_notification.create( - "Doorbird configuration failed. Please verify that API " - "Operator permission is enabled for the Doorbird user. " - "A restart will be required once permissions have been " - "verified.", - title="Doorbird Configuration Failure", - notification_id="doorbird_schedule_error", + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=doorstation_config, ) - - return False - - hass.data[DOMAIN] = doorstations + ) def _reset_device_favorites_handler(event): """Handle clearing favorites on device.""" @@ -129,6 +86,7 @@ def setup(hass, config): if doorstation is None: _LOGGER.error("Device not found for provided token.") + return # Clear webhooks favorites = doorstation.device.favorites() @@ -137,16 +95,126 @@ def setup(hass, config): for favorite_id in favorites[favorite_type]: doorstation.device.delete_favorite(favorite_type, favorite_id) - hass.bus.listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) + hass.bus.async_listen(RESET_DEVICE_FAVORITES, _reset_device_favorites_handler) return True -def get_doorstation_by_token(hass, token): - """Get doorstation by slug.""" - for doorstation in hass.data[DOMAIN]: - if token == doorstation.token: - return doorstation +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up DoorBird from a config entry.""" + + _async_import_options_from_data_if_missing(hass, entry) + + doorstation_config = entry.data + doorstation_options = entry.options + config_entry_id = entry.entry_id + + device_ip = doorstation_config[CONF_HOST] + username = doorstation_config[CONF_USERNAME] + password = doorstation_config[CONF_PASSWORD] + + device = DoorBird(device_ip, username, password) + try: + status = await hass.async_add_executor_job(device.ready) + info = await hass.async_add_executor_job(device.info) + except urllib.error.HTTPError as err: + if err.code == 401: + _LOGGER.error( + "Authorization rejected by DoorBird for %s@%s", username, device_ip + ) + return False + raise ConfigEntryNotReady + except OSError as oserr: + _LOGGER.error("Failed to setup doorbird at %s: %s", device_ip, oserr) + raise ConfigEntryNotReady + + if not status[0]: + _LOGGER.error( + "Could not connect to DoorBird as %s@%s: Error %s", + username, + device_ip, + str(status[1]), + ) + raise ConfigEntryNotReady + + token = doorstation_config.get(CONF_TOKEN, config_entry_id) + custom_url = doorstation_config.get(CONF_CUSTOM_URL) + name = doorstation_config.get(CONF_NAME) + events = doorstation_options.get(CONF_EVENTS, []) + doorstation = ConfiguredDoorBird(device, name, events, custom_url, token) + # Subscribe to doorbell or motion events + if not await _async_register_events(hass, doorstation): + raise ConfigEntryNotReady + + hass.data[DOMAIN][config_entry_id] = { + DOOR_STATION: doorstation, + DOOR_STATION_INFO: info, + } + + entry.add_update_listener(_update_listener) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def _async_register_events(hass, doorstation): + try: + await hass.async_add_executor_job(doorstation.register_events, hass) + except HTTPError: + hass.components.persistent_notification.create( + "Doorbird configuration failed. Please verify that API " + "Operator permission is enabled for the Doorbird user. " + "A restart will be required once permissions have been " + "verified.", + title="Doorbird Configuration Failure", + notification_id="doorbird_schedule_error", + ) + return False + + return True + + +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + config_entry_id = entry.entry_id + doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] + + doorstation.events = entry.options[CONF_EVENTS] + # Subscribe to doorbell or motion events + await _async_register_events(hass, doorstation) + + +@callback +def _async_import_options_from_data_if_missing(hass: HomeAssistant, entry: ConfigEntry): + options = dict(entry.options) + modified = False + for importable_option in [CONF_EVENTS]: + if importable_option not in entry.options and importable_option in entry.data: + options[importable_option] = entry.data[importable_option] + modified = True + + if modified: + hass.config_entries.async_update_entry(entry, options=options) class ConfiguredDoorBird: @@ -157,7 +225,7 @@ class ConfiguredDoorBird: self._name = name self._device = device self._custom_url = custom_url - self._events = events + self.events = events self._token = token @property @@ -189,7 +257,7 @@ class ConfiguredDoorBird: if self.custom_url is not None: hass_url = self.custom_url - for event in self._events: + for event in self.events: event = self._get_event_name(event) self._register_event(hass_url, event) diff --git a/homeassistant/components/doorbird/camera.py b/homeassistant/components/doorbird/camera.py index 4bf3a6e060f..bf999489589 100644 --- a/homeassistant/components/doorbird/camera.py +++ b/homeassistant/components/doorbird/camera.py @@ -10,46 +10,69 @@ from homeassistant.components.camera import SUPPORT_STREAM, Camera from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.util.dt as dt_util -from . import DOMAIN as DOORBIRD_DOMAIN +from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO +from .entity import DoorBirdEntity -_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=1) -_LAST_MOTION_INTERVAL = datetime.timedelta(minutes=1) -_LIVE_INTERVAL = datetime.timedelta(seconds=1) +_LAST_VISITOR_INTERVAL = datetime.timedelta(minutes=2) +_LAST_MOTION_INTERVAL = datetime.timedelta(seconds=30) +_LIVE_INTERVAL = datetime.timedelta(seconds=45) _LOGGER = logging.getLogger(__name__) -_TIMEOUT = 10 # seconds +_TIMEOUT = 15 # seconds -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the DoorBird camera platform.""" - for doorstation in hass.data[DOORBIRD_DOMAIN]: - device = doorstation.device - async_add_entities( - [ - DoorBirdCamera( - device.live_image_url, - f"{doorstation.name} Live", - _LIVE_INTERVAL, - device.rtsp_live_video_url, - ), - DoorBirdCamera( - device.history_image_url(1, "doorbell"), - f"{doorstation.name} Last Ring", - _LAST_VISITOR_INTERVAL, - ), - DoorBirdCamera( - device.history_image_url(1, "motionsensor"), - f"{doorstation.name} Last Motion", - _LAST_MOTION_INTERVAL, - ), - ] - ) + config_entry_id = config_entry.entry_id + doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] + doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO] + device = doorstation.device + + async_add_entities( + [ + DoorBirdCamera( + doorstation, + doorstation_info, + device.live_image_url, + "live", + f"{doorstation.name} Live", + _LIVE_INTERVAL, + device.rtsp_live_video_url, + ), + DoorBirdCamera( + doorstation, + doorstation_info, + device.history_image_url(1, "doorbell"), + "last_ring", + f"{doorstation.name} Last Ring", + _LAST_VISITOR_INTERVAL, + ), + DoorBirdCamera( + doorstation, + doorstation_info, + device.history_image_url(1, "motionsensor"), + "last_motion", + f"{doorstation.name} Last Motion", + _LAST_MOTION_INTERVAL, + ), + ] + ) -class DoorBirdCamera(Camera): +class DoorBirdCamera(DoorBirdEntity, Camera): """The camera on a DoorBird device.""" - def __init__(self, url, name, interval=None, stream_url=None): + def __init__( + self, + doorstation, + doorstation_info, + url, + camera_id, + name, + interval=None, + stream_url=None, + ): """Initialize the camera on a DoorBird device.""" + super().__init__(doorstation, doorstation_info) self._url = url self._stream_url = stream_url self._name = name @@ -57,12 +80,17 @@ class DoorBirdCamera(Camera): self._supported_features = SUPPORT_STREAM if self._stream_url else 0 self._interval = interval or datetime.timedelta self._last_update = datetime.datetime.min - super().__init__() + self._unique_id = f"{self._mac_addr}_{camera_id}" async def stream_source(self): """Return the stream source.""" return self._stream_url + @property + def unique_id(self): + """Camera Unique id.""" + return self._unique_id + @property def supported_features(self): """Return supported features.""" @@ -89,8 +117,10 @@ class DoorBirdCamera(Camera): self._last_update = now return self._last_image except asyncio.TimeoutError: - _LOGGER.error("Camera image timed out") + _LOGGER.error("DoorBird %s: Camera image timed out", self._name) return self._last_image except aiohttp.ClientError as error: - _LOGGER.error("Error getting camera image: %s", error) + _LOGGER.error( + "DoorBird %s: Error getting camera image: %s", self._name, error + ) return self._last_image diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py new file mode 100644 index 00000000000..37d46c23a9d --- /dev/null +++ b/homeassistant/components/doorbird/config_flow.py @@ -0,0 +1,156 @@ +"""Config flow for DoorBird integration.""" +import logging +import urllib + +from doorbirdpy import DoorBird +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import callback + +from .const import CONF_EVENTS, DOORBIRD_OUI +from .const import DOMAIN # pylint:disable=unused-import +from .util import get_mac_address_from_doorstation_info + +_LOGGER = logging.getLogger(__name__) + + +def _schema_with_defaults(host=None, name=None): + return vol.Schema( + { + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_NAME, default=name): str, + } + ) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + device = DoorBird(data[CONF_HOST], data[CONF_USERNAME], data[CONF_PASSWORD]) + try: + status = await hass.async_add_executor_job(device.ready) + info = await hass.async_add_executor_job(device.info) + except urllib.error.HTTPError as err: + if err.code == 401: + raise InvalidAuth + raise CannotConnect + except OSError: + raise CannotConnect + + if not status[0]: + raise CannotConnect + + mac_addr = get_mac_address_from_doorstation_info(info) + + # Return info that you want to store in the config entry. + return {"title": data[CONF_HOST], "mac_addr": mac_addr} + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for DoorBird.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the DoorBird config flow.""" + self.discovery_schema = {} + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(info["mac_addr"]) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=info["title"], data=user_input) + + 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): + """Prepare configuration for a discovered doorbird device.""" + macaddress = discovery_info["properties"]["macaddress"] + + if macaddress[:6] != DOORBIRD_OUI: + return self.async_abort(reason="not_doorbird_device") + + await self.async_set_unique_id(macaddress) + + self._abort_if_unique_id_configured( + updates={CONF_HOST: discovery_info[CONF_HOST]} + ) + + chop_ending = "._axis-video._tcp.local." + friendly_hostname = discovery_info["name"] + if friendly_hostname.endswith(chop_ending): + friendly_hostname = friendly_hostname[: -len(chop_ending)] + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + CONF_NAME: friendly_hostname, + CONF_HOST: discovery_info[CONF_HOST], + } + self.discovery_schema = _schema_with_defaults( + host=discovery_info[CONF_HOST], name=friendly_hostname + ) + + return await self.async_step_user() + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @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 doorbird.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """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: + events = [event.strip() for event in user_input[CONF_EVENTS].split(",")] + + return self.async_create_entry(title="", data={CONF_EVENTS: events}) + + current_events = self.config_entry.options.get(CONF_EVENTS, []) + + # We convert to a comma separated list for the UI + # since there really isn't anything better + options_schema = vol.Schema( + {vol.Optional(CONF_EVENTS, default=", ".join(current_events)): str} + ) + return self.async_show_form(step_id="init", data_schema=options_schema) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/doorbird/const.py b/homeassistant/components/doorbird/const.py new file mode 100644 index 00000000000..3b639fc8dca --- /dev/null +++ b/homeassistant/components/doorbird/const.py @@ -0,0 +1,17 @@ +"""The DoorBird integration constants.""" + + +DOMAIN = "doorbird" +PLATFORMS = ["switch", "camera"] +DOOR_STATION = "door_station" +DOOR_STATION_INFO = "door_station_info" +CONF_EVENTS = "events" +MANUFACTURER = "Bird Home Automation Group" +DOORBIRD_OUI = "1CCAE3" + +DOORBIRD_INFO_KEY_FIRMWARE = "FIRMWARE" +DOORBIRD_INFO_KEY_BUILD_NUMBER = "BUILD_NUMBER" +DOORBIRD_INFO_KEY_DEVICE_TYPE = "DEVICE-TYPE" +DOORBIRD_INFO_KEY_RELAYS = "RELAYS" +DOORBIRD_INFO_KEY_PRIMARY_MAC_ADDR = "PRIMARY_MAC_ADDR" +DOORBIRD_INFO_KEY_WIFI_MAC_ADDR = "WIFI_MAC_ADDR" diff --git a/homeassistant/components/doorbird/entity.py b/homeassistant/components/doorbird/entity.py new file mode 100644 index 00000000000..44cbb1f42de --- /dev/null +++ b/homeassistant/components/doorbird/entity.py @@ -0,0 +1,36 @@ +"""The DoorBird integration base entity.""" + +from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import Entity + +from .const import ( + DOORBIRD_INFO_KEY_BUILD_NUMBER, + DOORBIRD_INFO_KEY_DEVICE_TYPE, + DOORBIRD_INFO_KEY_FIRMWARE, + MANUFACTURER, +) +from .util import get_mac_address_from_doorstation_info + + +class DoorBirdEntity(Entity): + """Base class for doorbird entities.""" + + def __init__(self, doorstation, doorstation_info): + """Initialize the entity.""" + super().__init__() + self._doorstation_info = doorstation_info + self._doorstation = doorstation + self._mac_addr = get_mac_address_from_doorstation_info(doorstation_info) + + @property + def device_info(self): + """Doorbird device info.""" + firmware = self._doorstation_info[DOORBIRD_INFO_KEY_FIRMWARE] + firmware_build = self._doorstation_info[DOORBIRD_INFO_KEY_BUILD_NUMBER] + return { + "connections": {(dr.CONNECTION_NETWORK_MAC, self._mac_addr)}, + "name": self._doorstation.name, + "manufacturer": MANUFACTURER, + "sw_version": f"{firmware} {firmware_build}", + "model": self._doorstation_info[DOORBIRD_INFO_KEY_DEVICE_TYPE], + } diff --git a/homeassistant/components/doorbird/manifest.json b/homeassistant/components/doorbird/manifest.json index 1703557cc9e..e0aef80ab61 100644 --- a/homeassistant/components/doorbird/manifest.json +++ b/homeassistant/components/doorbird/manifest.json @@ -2,7 +2,16 @@ "domain": "doorbird", "name": "DoorBird", "documentation": "https://www.home-assistant.io/integrations/doorbird", - "requirements": ["doorbirdpy==2.0.8"], - "dependencies": ["http", "logbook"], - "codeowners": ["@oblogic7"] + "requirements": [ + "doorbirdpy==2.0.8" + ], + "dependencies": [ + "http", + "logbook" + ], + "zeroconf": ["_axis-video._tcp.local."], + "codeowners": [ + "@oblogic7", "@bdraco" + ], + "config_flow": true } diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json new file mode 100644 index 00000000000..caf3177c681 --- /dev/null +++ b/homeassistant/components/doorbird/strings.json @@ -0,0 +1,34 @@ +{ + "options" : { + "step" : { + "init" : { + "data" : { + "events" : "Comma separated list of events." + }, + "description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } + } + }, + "config" : { + "step" : { + "user" : { + "title" : "Connect to the DoorBird", + "data" : { + "password" : "Password", + "host" : "Host (IP Address)", + "name" : "Device Name", + "username" : "Username" + } + } + }, + "abort" : { + "already_configured" : "This DoorBird is already configured" + }, + "title" : "DoorBird", + "error" : { + "invalid_auth" : "Invalid authentication", + "unknown" : "Unexpected error", + "cannot_connect" : "Failed to connect, please try again" + } + } +} diff --git a/homeassistant/components/doorbird/switch.py b/homeassistant/components/doorbird/switch.py index 7a0dfa82e76..9f292803b8b 100644 --- a/homeassistant/components/doorbird/switch.py +++ b/homeassistant/components/doorbird/switch.py @@ -5,33 +5,38 @@ import logging from homeassistant.components.switch import SwitchDevice import homeassistant.util.dt as dt_util -from . import DOMAIN as DOORBIRD_DOMAIN +from .const import DOMAIN, DOOR_STATION, DOOR_STATION_INFO +from .entity import DoorBirdEntity _LOGGER = logging.getLogger(__name__) IR_RELAY = "__ir_light__" -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the DoorBird switch platform.""" - switches = [] + entities = [] + config_entry_id = config_entry.entry_id - for doorstation in hass.data[DOORBIRD_DOMAIN]: - relays = doorstation.device.info()["RELAYS"] - relays.append(IR_RELAY) + doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] + doorstation_info = hass.data[DOMAIN][config_entry_id][DOOR_STATION_INFO] - for relay in relays: - switch = DoorBirdSwitch(doorstation, relay) - switches.append(switch) + relays = doorstation_info["RELAYS"] + relays.append(IR_RELAY) - add_entities(switches) + for relay in relays: + switch = DoorBirdSwitch(doorstation, doorstation_info, relay) + entities.append(switch) + + async_add_entities(entities) -class DoorBirdSwitch(SwitchDevice): +class DoorBirdSwitch(DoorBirdEntity, SwitchDevice): """A relay in a DoorBird device.""" - def __init__(self, doorstation, relay): + def __init__(self, doorstation, doorstation_info, relay): """Initialize a relay in a DoorBird device.""" + super().__init__(doorstation, doorstation_info) self._doorstation = doorstation self._relay = relay self._state = False @@ -41,6 +46,12 @@ class DoorBirdSwitch(SwitchDevice): self._time = datetime.timedelta(minutes=5) else: self._time = datetime.timedelta(seconds=5) + self._unique_id = f"{self._mac_addr}_{self._relay}" + + @property + def unique_id(self): + """Switch unique id.""" + return self._unique_id @property def name(self): diff --git a/homeassistant/components/doorbird/util.py b/homeassistant/components/doorbird/util.py new file mode 100644 index 00000000000..7db9063580d --- /dev/null +++ b/homeassistant/components/doorbird/util.py @@ -0,0 +1,19 @@ +"""DoorBird integration utils.""" + +from .const import DOMAIN, DOOR_STATION + + +def get_mac_address_from_doorstation_info(doorstation_info): + """Get the mac address depending on the device type.""" + if "PRIMARY_MAC_ADDR" in doorstation_info: + return doorstation_info["PRIMARY_MAC_ADDR"] + return doorstation_info["WIFI_MAC_ADDR"] + + +def get_doorstation_by_token(hass, token): + """Get doorstation by slug.""" + for config_entry_id in hass.data[DOMAIN]: + doorstation = hass.data[DOMAIN][config_entry_id][DOOR_STATION] + + if token == doorstation.token: + return doorstation diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index cc36af05da4..8c03702e8f9 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -24,6 +24,7 @@ FLOWS = [ "deconz", "dialogflow", "directv", + "doorbird", "dynalite", "ecobee", "elgato", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 1a9972e9a6e..968a73588e7 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -7,7 +7,8 @@ To update, run python3 -m script.hassfest ZEROCONF = { "_axis-video._tcp.local.": [ - "axis" + "axis", + "doorbird" ], "_coap._udp.local.": [ "tradfri" diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b671e7bd660..d5b56f9c14c 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -179,6 +179,9 @@ directpy==0.7 # homeassistant.components.updater distro==1.4.0 +# homeassistant.components.doorbird +doorbirdpy==2.0.8 + # homeassistant.components.dsmr dsmr_parser==0.18 diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py new file mode 100644 index 00000000000..57bf4c04e39 --- /dev/null +++ b/tests/components/doorbird/__init__.py @@ -0,0 +1 @@ +"""Tests for the DoorBird integration.""" diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py new file mode 100644 index 00000000000..7a70aec9041 --- /dev/null +++ b/tests/components/doorbird/test_config_flow.py @@ -0,0 +1,258 @@ +"""Test the DoorBird config flow.""" +import urllib + +from asynctest import MagicMock, patch + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN +from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry, init_recorder_component + +VALID_CONFIG = { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "friend", + CONF_PASSWORD: "password", + CONF_NAME: "mydoorbird", +} + + +def _get_mock_doorbirdapi_return_values(ready=None, info=None): + doorbirdapi_mock = MagicMock() + type(doorbirdapi_mock).ready = MagicMock(return_value=ready) + type(doorbirdapi_mock).info = MagicMock(return_value=info) + + return doorbirdapi_mock + + +def _get_mock_doorbirdapi_side_effects(ready=None, info=None): + doorbirdapi_mock = MagicMock() + type(doorbirdapi_mock).ready = MagicMock(side_effect=ready) + type(doorbirdapi_mock).info = MagicMock(side_effect=info) + + return doorbirdapi_mock + + +async def test_user_form(hass): + """Test we get the user form.""" + await hass.async_add_job(init_recorder_component, hass) # force in memory db + + 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 result["errors"] == {} + + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} + ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ), patch( + "homeassistant.components.doorbird.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.doorbird.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], VALID_CONFIG, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.2.3.4" + assert result2["data"] == { + "host": "1.2.3.4", + "name": "mydoorbird", + "password": "password", + "username": "friend", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await hass.async_add_job(init_recorder_component, hass) # force in memory db + + await setup.async_setup_component(hass, "persistent_notification", {}) + + import_config = VALID_CONFIG.copy() + import_config[CONF_EVENTS] = ["event1", "event2", "event3"] + import_config[CONF_TOKEN] = "imported_token" + import_config[ + CONF_CUSTOM_URL + ] = "http://legacy.custom.url/should/only/come/in/from/yaml" + + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} + ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( + "homeassistant.components.doorbird.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.doorbird.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=import_config, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "1.2.3.4" + assert result["data"] == { + "host": "1.2.3.4", + "name": "mydoorbird", + "password": "password", + "username": "friend", + "events": ["event1", "event2", "event3"], + "token": "imported_token", + # This will go away once we convert to cloud hooks + "hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml", + } + # It is not possible to import options at this time + # so they end up in the config entry data and are + # used a fallback when they are not in options + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_zeroconf_wrong_oui(hass): + """Test we abort when we get the wrong OUI via zeroconf.""" + await hass.async_add_job(init_recorder_component, hass) # force in memory db + + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={ + "properties": {"macaddress": "notdoorbirdoui"}, + "name": "Doorstation - abc123._axis-video._tcp.local.", + }, + ) + assert result["type"] == "abort" + + +async def test_form_zeroconf_correct_oui(hass): + """Test we can setup from zeroconf with the correct OUI source.""" + await hass.async_add_job(init_recorder_component, hass) # force in memory db + + await setup.async_setup_component(hass, "persistent_notification", {}) + + 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", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + doorbirdapi = _get_mock_doorbirdapi_return_values( + ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} + ) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( + "homeassistant.components.doorbird.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.doorbird.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], VALID_CONFIG + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "1.2.3.4" + assert result2["data"] == { + "host": "1.2.3.4", + "name": "mydoorbird", + "password": "password", + "username": "friend", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_cannot_connect(hass): + """Test we handle cannot connect error.""" + await hass.async_add_job(init_recorder_component, hass) # force in memory db + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=OSError) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], VALID_CONFIG, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_user_invalid_auth(hass): + """Test we handle cannot invalid auth error.""" + await hass.async_add_job(init_recorder_component, hass) # force in memory db + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_urllib_error = urllib.error.HTTPError( + "http://xyz.tld", 401, "login failed", {}, None + ) + doorbirdapi = _get_mock_doorbirdapi_side_effects(ready=mock_urllib_error) + with patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=doorbirdapi, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], VALID_CONFIG, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_options_flow(hass): + """Test config flow options.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + unique_id="abcde12345", + data=VALID_CONFIG, + options={CONF_EVENTS: ["event1", "event2", "event3"]}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.doorbird.async_setup_entry", return_value=True + ): + 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_EVENTS: "eventa, eventc, eventq"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options == {CONF_EVENTS: ["eventa", "eventc", "eventq"]} diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index c5790dc718c..a74c81ba307 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -62,7 +62,10 @@ async def test_setup(hass, mock_zeroconf): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) assert len(mock_service_browser.mock_calls) == len(zc_gen.ZEROCONF) - assert len(mock_config_flow.mock_calls) == len(zc_gen.ZEROCONF) * 2 + expected_flow_calls = 0 + for matching_components in zc_gen.ZEROCONF.values(): + expected_flow_calls += len(matching_components) + assert len(mock_config_flow.mock_calls) == expected_flow_calls * 2 async def test_homekit_match_partial(hass, mock_zeroconf): From 4332cbe1127777724f08170f4729fd7410e724c6 Mon Sep 17 00:00:00 2001 From: Olivier B Date: Mon, 23 Mar 2020 10:52:59 +0100 Subject: [PATCH 209/431] Add Tesla sentry mode switch (#32938) * Add Tesla sentry mode switch * add available property, fix is_on * bump teslajsonpy to 0.6.0 --- homeassistant/components/tesla/const.py | 1 + homeassistant/components/tesla/manifest.json | 2 +- homeassistant/components/tesla/switch.py | 31 ++++++++++++++++++++ requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 35 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tesla/const.py b/homeassistant/components/tesla/const.py index d7930c01fe8..2b8485c7616 100644 --- a/homeassistant/components/tesla/const.py +++ b/homeassistant/components/tesla/const.py @@ -25,4 +25,5 @@ ICONS = { "temperature sensor": "mdi:thermometer", "location tracker": "mdi:crosshairs-gps", "charging rate sensor": "mdi:speedometer", + "sentry mode switch": "mdi:shield-car", } diff --git a/homeassistant/components/tesla/manifest.json b/homeassistant/components/tesla/manifest.json index 950a860b308..1bba8436312 100644 --- a/homeassistant/components/tesla/manifest.json +++ b/homeassistant/components/tesla/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/tesla", "requirements": [ - "teslajsonpy==0.5.1" + "teslajsonpy==0.6.0" ], "dependencies": [], "codeowners": [ diff --git a/homeassistant/components/tesla/switch.py b/homeassistant/components/tesla/switch.py index 331f6bd8126..716836821c4 100644 --- a/homeassistant/components/tesla/switch.py +++ b/homeassistant/components/tesla/switch.py @@ -19,6 +19,8 @@ async def async_setup_entry(hass, config_entry, async_add_entities): entities.append(UpdateSwitch(device, controller, config_entry)) elif device.type == "maxrange switch": entities.append(RangeSwitch(device, controller, config_entry)) + elif device.type == "sentry mode switch": + entities.append(SentryModeSwitch(device, controller, config_entry)) async_add_entities(entities, True) @@ -114,3 +116,32 @@ class UpdateSwitch(TeslaDevice, SwitchDevice): _LOGGER.debug("Updating state for: %s %s", self._name, car_id) await super().async_update() self._state = bool(self.controller.get_updates(car_id)) + + +class SentryModeSwitch(TeslaDevice, SwitchDevice): + """Representation of a Tesla sentry mode switch.""" + + async def async_turn_on(self, **kwargs): + """Send the on command.""" + _LOGGER.debug("Enable sentry mode: %s", self._name) + await self.tesla_device.enable_sentry_mode() + + async def async_turn_off(self, **kwargs): + """Send the off command.""" + _LOGGER.debug("Disable sentry mode: %s", self._name) + await self.tesla_device.disable_sentry_mode() + + @property + def is_on(self): + """Get whether the switch is in on state.""" + return self.tesla_device.is_on() + + @property + def available(self): + """Indicate if Home Assistant is able to read the state and control the underlying device.""" + return self.tesla_device.available() + + async def async_update(self): + """Update the state of the switch.""" + _LOGGER.debug("Updating state for: %s", self._name) + await super().async_update() diff --git a/requirements_all.txt b/requirements_all.txt index e2f81dbcf20..44422771b13 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2000,7 +2000,7 @@ temperusb==1.5.3 tesla-powerwall==0.1.1 # homeassistant.components.tesla -teslajsonpy==0.5.1 +teslajsonpy==0.6.0 # homeassistant.components.thermoworks_smoke thermoworks_smoke==0.1.8 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index d5b56f9c14c..f8ed63bf8b3 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -719,7 +719,7 @@ tellduslive==0.10.10 tesla-powerwall==0.1.1 # homeassistant.components.tesla -teslajsonpy==0.5.1 +teslajsonpy==0.6.0 # homeassistant.components.toon toonapilib==3.2.4 From df2351b920031b3bae20c2f8a36cf2d0139a1e22 Mon Sep 17 00:00:00 2001 From: Vegetto Date: Mon, 23 Mar 2020 10:56:44 +0100 Subject: [PATCH 210/431] Support for COLOR_TEMP in Homematic dimmers (#31207) * Support for COLOR_TEMP in Homematic dimmers * Fix lint issues * Added pre-checks * Fix review feedback * Remove unneded code --- homeassistant/components/homematic/const.py | 1 + homeassistant/components/homematic/light.py | 23 ++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/homematic/const.py b/homeassistant/components/homematic/const.py index cd2d528044a..188ec1e2445 100644 --- a/homeassistant/components/homematic/const.py +++ b/homeassistant/components/homematic/const.py @@ -63,6 +63,7 @@ HM_DEVICE_TYPES = { "IPDimmer", "ColorEffectLight", "IPKeySwitchLevel", + "ColdWarmDimmer", ], DISCOVER_SENSORS: [ "SwitchPowermeter", diff --git a/homeassistant/components/homematic/light.py b/homeassistant/components/homematic/light.py index 52b2f9a7996..6e6ccb78371 100644 --- a/homeassistant/components/homematic/light.py +++ b/homeassistant/components/homematic/light.py @@ -3,11 +3,13 @@ import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_HS_COLOR, ATTR_TRANSITION, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, Light, ) @@ -60,6 +62,8 @@ class HMLight(HMDevice, Light): features |= SUPPORT_COLOR if "PROGRAM" in self._hmdevice.WRITENODE: features |= SUPPORT_EFFECT + if hasattr(self._hmdevice, "get_color_temp"): + features |= SUPPORT_COLOR_TEMP return features @property @@ -70,6 +74,14 @@ class HMLight(HMDevice, Light): hue, sat = self._hmdevice.get_hs_color(self._channel) return hue * 360.0, sat * 100.0 + @property + def color_temp(self): + """Return the color temp in mireds [int].""" + if not self.supported_features & SUPPORT_COLOR_TEMP: + return None + hm_color_temp = self._hmdevice.get_color_temp(self._channel) + return self.max_mireds - (self.max_mireds - self.min_mireds) * hm_color_temp + @property def effect_list(self): """Return the list of supported effects.""" @@ -92,7 +104,11 @@ class HMLight(HMDevice, Light): if ATTR_BRIGHTNESS in kwargs and self._state == "LEVEL": percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 self._hmdevice.set_level(percent_bright, self._channel) - elif ATTR_HS_COLOR not in kwargs and ATTR_EFFECT not in kwargs: + elif ( + ATTR_HS_COLOR not in kwargs + and ATTR_COLOR_TEMP not in kwargs + and ATTR_EFFECT not in kwargs + ): self._hmdevice.on(self._channel) if ATTR_HS_COLOR in kwargs: @@ -101,6 +117,11 @@ class HMLight(HMDevice, Light): saturation=kwargs[ATTR_HS_COLOR][1] / 100.0, channel=self._channel, ) + if ATTR_COLOR_TEMP in kwargs: + hm_temp = (self.max_mireds - kwargs[ATTR_COLOR_TEMP]) / ( + self.max_mireds - self.min_mireds + ) + self._hmdevice.set_color_temp(hm_temp) if ATTR_EFFECT in kwargs: self._hmdevice.set_effect(kwargs[ATTR_EFFECT]) From c21a2eab2253363d89c74424ef5dcf2cf5e5ccab Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 23 Mar 2020 13:14:50 +0100 Subject: [PATCH 211/431] [skip ci] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index a01e81789ab..2fc0ee39018 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -19,7 +19,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.8.0-3.7-alpine3.11' + value: '1.9.0-3.7-alpine3.11' resources: repositories: - repository: azure From df67ab995f6dcbed880e3a737e78f162f6b87dcb Mon Sep 17 00:00:00 2001 From: Balazs Keresztury Date: Mon, 23 Mar 2020 15:12:59 +0100 Subject: [PATCH 212/431] Add support for Bosch BMP280 Sensor (#30837) * Implement support for Bosch BMP280 Sensor * Fixed linting errors * Fixed case of the requirement RPi.GPIO so it is ignored in requirements * Update homeassistant/components/bmp280/manifest.json Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Update homeassistant/components/bmp280/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Fix linting errors * Implemented changes suggested by code review * Fixed incomplete refactoring Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/bmp280/__init__.py | 1 + homeassistant/components/bmp280/manifest.json | 9 + homeassistant/components/bmp280/sensor.py | 158 ++++++++++++++++++ requirements_all.txt | 4 + 6 files changed, 174 insertions(+) create mode 100644 homeassistant/components/bmp280/__init__.py create mode 100644 homeassistant/components/bmp280/manifest.json create mode 100644 homeassistant/components/bmp280/sensor.py diff --git a/.coveragerc b/.coveragerc index e8f349013fc..09ff6115ce2 100644 --- a/.coveragerc +++ b/.coveragerc @@ -75,6 +75,7 @@ omit = homeassistant/components/bluetooth_tracker/* homeassistant/components/bme280/sensor.py homeassistant/components/bme680/sensor.py + homeassistant/components/bmp280/sensor.py homeassistant/components/bmw_connected_drive/* homeassistant/components/bom/camera.py homeassistant/components/bom/sensor.py diff --git a/CODEOWNERS b/CODEOWNERS index 86ad7f5c2db..86a36551f57 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -52,6 +52,7 @@ homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria homeassistant/components/blink/* @fronzbot +homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 homeassistant/components/bom/* @maddenp homeassistant/components/braviatv/* @robbiet480 diff --git a/homeassistant/components/bmp280/__init__.py b/homeassistant/components/bmp280/__init__.py new file mode 100644 index 00000000000..0c884eafbf1 --- /dev/null +++ b/homeassistant/components/bmp280/__init__.py @@ -0,0 +1 @@ +"""The Bosch BMP280 sensor integration.""" diff --git a/homeassistant/components/bmp280/manifest.json b/homeassistant/components/bmp280/manifest.json new file mode 100644 index 00000000000..d7d3752392b --- /dev/null +++ b/homeassistant/components/bmp280/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "bmp280", + "name": "Bosch BMP280 Environmental Sensor", + "documentation": "https://www.home-assistant.io/integrations/bmp280", + "dependencies": [], + "codeowners": ["@belidzs"], + "requirements": ["adafruit-circuitpython-bmp280==3.1.1", "RPi.GPIO==0.7.0"], + "quality_scale": "silver" +} diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py new file mode 100644 index 00000000000..613902d1cd7 --- /dev/null +++ b/homeassistant/components/bmp280/sensor.py @@ -0,0 +1,158 @@ +"""Platform for Bosch BMP280 Environmental Sensor integration.""" +from datetime import timedelta +import logging + +from adafruit_bmp280 import Adafruit_BMP280_I2C +import board +from busio import I2C +import voluptuous as vol + +from homeassistant.components.sensor import ( + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + PLATFORM_SCHEMA, +) +from homeassistant.const import CONF_NAME, PRESSURE_HPA, TEMP_CELSIUS +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = "BMP280" +DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) + +MIN_I2C_ADDRESS = 0x76 +MAX_I2C_ADDRESS = 0x77 + +CONF_I2C_ADDRESS = "i2c_address" + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_I2C_ADDRESS): vol.All( + vol.Coerce(int), vol.Range(min=MIN_I2C_ADDRESS, max=MAX_I2C_ADDRESS) + ), + } +) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the sensor platform.""" + try: + # initializing I2C bus using the auto-detected pins + i2c = I2C(board.SCL, board.SDA) + # initializing the sensor + bmp280 = Adafruit_BMP280_I2C(i2c, address=config[CONF_I2C_ADDRESS]) + except ValueError as error: + # this usually happens when the board is I2C capable, but the device can't be found at the configured address + if str(error.args[0]).startswith("No I2C device at address"): + _LOGGER.error( + "%s. Hint: Check wiring and make sure that the SDO pin is tied to either ground (0x76) or VCC (0x77)!", + error.args[0], + ) + raise PlatformNotReady() + raise error + # use custom name if there's any + name = config.get(CONF_NAME) + # BMP280 has both temperature and pressure sensing capability + add_entities( + [Bmp280TemperatureSensor(bmp280, name), Bmp280PressureSensor(bmp280, name)] + ) + + +class Bmp280Sensor(Entity): + """Base class for BMP280 entities.""" + + def __init__( + self, + bmp280: Adafruit_BMP280_I2C, + name: str, + unit_of_measurement: str, + device_class: str, + ): + """Initialize the sensor.""" + self._bmp280 = bmp280 + self._name = name + self._unit_of_measurement = unit_of_measurement + self._device_class = device_class + self._state = None + self._errored = False + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + @property + def device_class(self): + """Return the device class.""" + return self._device_class + + @property + def available(self) -> bool: + """Return if the device is currently available.""" + return not self._errored + + +class Bmp280TemperatureSensor(Bmp280Sensor): + """Representation of a Bosch BMP280 Temperature Sensor.""" + + def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str): + """Initialize the entity.""" + super().__init__( + bmp280, f"{name} Temperature", TEMP_CELSIUS, DEVICE_CLASS_TEMPERATURE + ) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Fetch new state data for the sensor.""" + try: + self._state = round(self._bmp280.temperature, 1) + if self._errored: + _LOGGER.warning("Communication restored with temperature sensor") + self._errored = False + except OSError: + # this is thrown when a working sensor is unplugged between two updates + _LOGGER.warning( + "Unable to read temperature data due to a communication problem" + ) + self._errored = True + + +class Bmp280PressureSensor(Bmp280Sensor): + """Representation of a Bosch BMP280 Barometric Pressure Sensor.""" + + def __init__(self, bmp280: Adafruit_BMP280_I2C, name: str): + """Initialize the entity.""" + super().__init__( + bmp280, f"{name} Pressure", PRESSURE_HPA, DEVICE_CLASS_PRESSURE + ) + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self): + """Fetch new state data for the sensor.""" + try: + self._state = round(self._bmp280.pressure) + if self._errored: + _LOGGER.warning("Communication restored with pressure sensor") + self._errored = False + except OSError: + # this is thrown when a working sensor is unplugged between two updates + _LOGGER.warning( + "Unable to read pressure data due to a communication problem" + ) + self._errored = True diff --git a/requirements_all.txt b/requirements_all.txt index 44422771b13..8bae90ae60c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -83,6 +83,7 @@ PyViCare==0.1.7 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4 +# homeassistant.components.bmp280 # homeassistant.components.mcp23017 # homeassistant.components.rpi_gpio # RPi.GPIO==0.7.0 @@ -111,6 +112,9 @@ abodepy==0.18.1 # homeassistant.components.mcp23017 adafruit-blinka==3.9.0 +# homeassistant.components.bmp280 +adafruit-circuitpython-bmp280==3.1.1 + # homeassistant.components.mcp23017 adafruit-circuitpython-mcp230xx==2.2.2 From c8d4cf08d99477bbb8cfb6634697af346ddba65e Mon Sep 17 00:00:00 2001 From: jasperro <42558625+jasperro@users.noreply.github.com> Date: Mon, 23 Mar 2020 16:40:15 +0100 Subject: [PATCH 213/431] Add Tado set presence (#32765) * Updated tado integration climate.py to allow for presence change * Updated tado integration __init__.py to allow for presence change * Black formatting * Added missing docstring * Added missing period to docstring * Using constants from climate component * Filter out other preset_modes * Linting error fix * Isort error fix * Filtering of unwanted presence mode in init * Bumped python-tado version to 0.5.0 Removed unnecessary preset mode check * Update requirements_all.txt --- homeassistant/components/tado/__init__.py | 10 ++++++++++ homeassistant/components/tado/climate.py | 2 +- homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 727fb868a33..e5e3d1d409c 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -6,6 +6,7 @@ import urllib from PyTado.interface import Tado import voluptuous as vol +from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import load_platform @@ -162,6 +163,15 @@ class TadoConnector: self.tado.resetZoneOverlay(zone_id) self.update_sensor("zone", zone_id) + def set_presence( + self, presence=PRESET_HOME, + ): + """Set the presence to home or away.""" + if presence == PRESET_AWAY: + self.tado.setAway() + elif presence == PRESET_HOME: + self.tado.setHome() + def set_zone_overlay( self, zone_id, diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index b92a54edd5e..e202cc49da4 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -289,7 +289,7 @@ class TadoClimate(ClimateDevice): def set_preset_mode(self, preset_mode): """Set new preset mode.""" - pass + self._tado.set_presence(preset_mode) @property def temperature_unit(self): diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index e51cc53caa5..ab0be2d4346 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": [ - "python-tado==0.3.0" + "python-tado==0.5.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8bae90ae60c..72bc9d27e7e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1658,7 +1658,7 @@ python-songpal==0.11.2 python-synology==0.4.0 # homeassistant.components.tado -python-tado==0.3.0 +python-tado==0.5.0 # homeassistant.components.telegram_bot python-telegram-bot==11.1.0 From 0e3dc7976c7f41d24b56fd35e9e9f947f8db121f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 23 Mar 2020 16:54:06 +0100 Subject: [PATCH 214/431] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 2fc0ee39018..3ed413f4678 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -19,7 +19,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.9.0-3.7-alpine3.11' + value: '1.10.0-3.7-alpine3.11' resources: repositories: - repository: azure @@ -36,6 +36,7 @@ jobs: skipBinary: 'aiohttp' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' + wheelsConstraint: 'homeassistant/package_constraints.txt' preBuild: - script: | cp requirements_all.txt requirements_wheels.txt From b8fdebd05c277d123edfce1ca865e1022825e757 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2020 11:01:48 -0500 Subject: [PATCH 215/431] Add aircleaner and humidify service to nexia climate (#33078) * Add aircleaner and humidify service to nexia climate * These were removed from the original merge to reduce review scope * Additional tests for binary_sensor, sensor, and climate states * Switch to signals for services Get rid of everywhere we call device and change to zone or thermostat as it was too confusing Renames to make it clear that zone and thermostat are tightly coupled * Make scene activation responsive * no need to use update for only one key/value * stray comma * use async_call_later * its async, need ()s * cleaner * merge entity platform services testing branch --- homeassistant/components/nexia/__init__.py | 5 +- .../components/nexia/binary_sensor.py | 59 +--- homeassistant/components/nexia/climate.py | 277 ++++++++++-------- homeassistant/components/nexia/const.py | 7 +- homeassistant/components/nexia/entity.py | 105 ++++++- homeassistant/components/nexia/scene.py | 46 ++- homeassistant/components/nexia/sensor.py | 133 +++------ homeassistant/components/nexia/services.yaml | 19 ++ homeassistant/components/nexia/util.py | 6 + tests/components/nexia/test_binary_sensor.py | 35 +++ tests/components/nexia/test_climate.py | 35 +++ tests/components/nexia/test_scene.py | 6 +- tests/components/nexia/test_sensor.py | 133 +++++++++ 13 files changed, 575 insertions(+), 291 deletions(-) create mode 100644 homeassistant/components/nexia/services.yaml create mode 100644 homeassistant/components/nexia/util.py create mode 100644 tests/components/nexia/test_binary_sensor.py create mode 100644 tests/components/nexia/test_sensor.py diff --git a/homeassistant/components/nexia/__init__.py b/homeassistant/components/nexia/__init__.py index 41ecf6f1045..5c317794c2a 100644 --- a/homeassistant/components/nexia/__init__.py +++ b/homeassistant/components/nexia/__init__.py @@ -15,7 +15,7 @@ from homeassistant.exceptions import ConfigEntryNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator -from .const import DATA_NEXIA, DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR +from .const import DOMAIN, NEXIA_DEVICE, PLATFORMS, UPDATE_COORDINATOR _LOGGER = logging.getLogger(__name__) @@ -94,8 +94,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_interval=timedelta(seconds=DEFAULT_UPDATE_RATE), ) - hass.data[DOMAIN][entry.entry_id] = {} - hass.data[DOMAIN][entry.entry_id][DATA_NEXIA] = { + hass.data[DOMAIN][entry.entry_id] = { NEXIA_DEVICE: nexia_home, UPDATE_COORDINATOR: coordinator, } diff --git a/homeassistant/components/nexia/binary_sensor.py b/homeassistant/components/nexia/binary_sensor.py index 2802c3d7bd4..5c33412c647 100644 --- a/homeassistant/components/nexia/binary_sensor.py +++ b/homeassistant/components/nexia/binary_sensor.py @@ -1,23 +1,15 @@ """Support for Nexia / Trane XL Thermostats.""" from homeassistant.components.binary_sensor import BinarySensorDevice -from homeassistant.const import ATTR_ATTRIBUTION -from .const import ( - ATTRIBUTION, - DATA_NEXIA, - DOMAIN, - MANUFACTURER, - NEXIA_DEVICE, - UPDATE_COORDINATOR, -) -from .entity import NexiaEntity +from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR +from .entity import NexiaThermostatEntity async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for a Nexia device.""" - nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_data = hass.data[DOMAIN][config_entry.entry_id] nexia_home = nexia_data[NEXIA_DEVICE] coordinator = nexia_data[UPDATE_COORDINATOR] @@ -42,48 +34,21 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NexiaBinarySensor(NexiaEntity, BinarySensorDevice): +class NexiaBinarySensor(NexiaThermostatEntity, BinarySensorDevice): """Provices Nexia BinarySensor support.""" - def __init__(self, coordinator, device, sensor_call, sensor_name): + def __init__(self, coordinator, thermostat, sensor_call, sensor_name): """Initialize the nexia sensor.""" - super().__init__(coordinator) - self._coordinator = coordinator - self._device = device - self._name = f"{self._device.get_name()} {sensor_name}" + super().__init__( + coordinator, + thermostat, + name=f"{thermostat.get_name()} {sensor_name}", + unique_id=f"{thermostat.thermostat_id}_{sensor_call}", + ) self._call = sensor_call - self._unique_id = f"{self._device.thermostat_id}_{sensor_call}" self._state = None - @property - def unique_id(self): - """Return the unique id of the binary sensor.""" - return self._unique_id - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._device.thermostat_id)}, - "name": self._device.get_name(), - "model": self._device.get_model(), - "sw_version": self._device.get_firmware(), - "manufacturer": MANUFACTURER, - } - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } - @property def is_on(self): """Return the status of the sensor.""" - return getattr(self._device, self._call)() + return getattr(self._thermostat, self._call)() diff --git a/homeassistant/components/nexia/climate.py b/homeassistant/components/nexia/climate.py index 7231f2b8ba9..8af1be20b1e 100644 --- a/homeassistant/components/nexia/climate.py +++ b/homeassistant/components/nexia/climate.py @@ -12,9 +12,11 @@ from nexia.const import ( SYSTEM_STATUS_IDLE, UNIT_FAHRENHEIT, ) +import voluptuous as vol from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_HUMIDITY, ATTR_MAX_HUMIDITY, ATTR_MIN_HUMIDITY, ATTR_TARGET_TEMP_HIGH, @@ -36,26 +38,50 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.const import ( - ATTR_ATTRIBUTION, + ATTR_ENTITY_ID, ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.helpers import entity_platform +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.dispatcher import dispatcher_send from .const import ( + ATTR_AIRCLEANER_MODE, ATTR_DEHUMIDIFY_SETPOINT, ATTR_DEHUMIDIFY_SUPPORTED, ATTR_HUMIDIFY_SETPOINT, ATTR_HUMIDIFY_SUPPORTED, ATTR_ZONE_STATUS, - ATTRIBUTION, - DATA_NEXIA, DOMAIN, - MANUFACTURER, NEXIA_DEVICE, + SIGNAL_THERMOSTAT_UPDATE, + SIGNAL_ZONE_UPDATE, UPDATE_COORDINATOR, ) -from .entity import NexiaEntity +from .entity import NexiaThermostatZoneEntity +from .util import percent_conv + +SERVICE_SET_AIRCLEANER_MODE = "set_aircleaner_mode" +SERVICE_SET_HUMIDIFY_SETPOINT = "set_humidify_setpoint" + +SET_AIRCLEANER_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_AIRCLEANER_MODE): cv.string, + } +) + +SET_HUMIDITY_SCHEMA = vol.Schema( + { + vol.Required(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(ATTR_HUMIDITY): vol.All( + vol.Coerce(int), vol.Range(min=35, max=65) + ), + } +) + _LOGGER = logging.getLogger(__name__) @@ -83,10 +109,21 @@ NEXIA_TO_HA_HVAC_MODE_MAP = { async def async_setup_entry(hass, config_entry, async_add_entities): """Set up climate for a Nexia device.""" - nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_data = hass.data[DOMAIN][config_entry.entry_id] nexia_home = nexia_data[NEXIA_DEVICE] coordinator = nexia_data[UPDATE_COORDINATOR] + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_SET_HUMIDIFY_SETPOINT, + SET_HUMIDITY_SCHEMA, + SERVICE_SET_HUMIDIFY_SETPOINT, + ) + platform.async_register_entity_service( + SERVICE_SET_AIRCLEANER_MODE, SET_AIRCLEANER_SCHEMA, SERVICE_SET_AIRCLEANER_MODE, + ) + entities = [] for thermostat_id in nexia_home.get_thermostat_ids(): thermostat = nexia_home.get_thermostat_by_id(thermostat_id) @@ -97,26 +134,22 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -class NexiaZone(NexiaEntity, ClimateDevice): +class NexiaZone(NexiaThermostatZoneEntity, ClimateDevice): """Provides Nexia Climate support.""" - def __init__(self, coordinator, device): + def __init__(self, coordinator, zone): """Initialize the thermostat.""" - super().__init__(coordinator) - self.thermostat = device.thermostat - self._device = device - self._coordinator = coordinator + super().__init__( + coordinator, zone, name=zone.get_name(), unique_id=zone.zone_id + ) + self._undo_humidfy_dispatcher = None + self._undo_aircleaner_dispatcher = None # The has_* calls are stable for the life of the device # and do not do I/O - self._has_relative_humidity = self.thermostat.has_relative_humidity() - self._has_emergency_heat = self.thermostat.has_emergency_heat() - self._has_humidify_support = self.thermostat.has_humidify_support() - self._has_dehumidify_support = self.thermostat.has_dehumidify_support() - - @property - def unique_id(self): - """Device Uniqueid.""" - return self._device.zone_id + self._has_relative_humidity = self._thermostat.has_relative_humidity() + self._has_emergency_heat = self._thermostat.has_emergency_heat() + self._has_humidify_support = self._thermostat.has_humidify_support() + self._has_dehumidify_support = self._thermostat.has_dehumidify_support() @property def supported_features(self): @@ -139,27 +172,22 @@ class NexiaZone(NexiaEntity, ClimateDevice): @property def is_fan_on(self): """Blower is on.""" - return self.thermostat.is_blower_active() - - @property - def name(self): - """Name of the zone.""" - return self._device.get_name() + return self._thermostat.is_blower_active() @property def temperature_unit(self): """Return the unit of measurement.""" - return TEMP_CELSIUS if self.thermostat.get_unit() == "C" else TEMP_FAHRENHEIT + return TEMP_CELSIUS if self._thermostat.get_unit() == "C" else TEMP_FAHRENHEIT @property def current_temperature(self): """Return the current temperature.""" - return self._device.get_temperature() + return self._zone.get_temperature() @property def fan_mode(self): """Return the fan setting.""" - return self.thermostat.get_fan_mode() + return self._thermostat.get_fan_mode() @property def fan_modes(self): @@ -169,92 +197,92 @@ class NexiaZone(NexiaEntity, ClimateDevice): @property def min_temp(self): """Minimum temp for the current setting.""" - return (self._device.thermostat.get_setpoint_limits())[0] + return (self._thermostat.get_setpoint_limits())[0] @property def max_temp(self): """Maximum temp for the current setting.""" - return (self._device.thermostat.get_setpoint_limits())[1] + return (self._thermostat.get_setpoint_limits())[1] def set_fan_mode(self, fan_mode): """Set new target fan mode.""" - self.thermostat.set_fan_mode(fan_mode) - self.schedule_update_ha_state() + self._thermostat.set_fan_mode(fan_mode) + self._signal_thermostat_update() @property def preset_mode(self): """Preset that is active.""" - return self._device.get_preset() + return self._zone.get_preset() @property def preset_modes(self): """All presets.""" - return self._device.get_presets() + return self._zone.get_presets() def set_humidity(self, humidity): """Dehumidify target.""" - self.thermostat.set_dehumidify_setpoint(humidity / 100.0) - self.schedule_update_ha_state() + self._thermostat.set_dehumidify_setpoint(humidity / 100.0) + self._signal_thermostat_update() @property def target_humidity(self): """Humidity indoors setpoint.""" if self._has_dehumidify_support: - return round(self.thermostat.get_dehumidify_setpoint() * 100.0, 1) + return percent_conv(self._thermostat.get_dehumidify_setpoint()) if self._has_humidify_support: - return round(self.thermostat.get_humidify_setpoint() * 100.0, 1) + return percent_conv(self._thermostat.get_humidify_setpoint()) return None @property def current_humidity(self): """Humidity indoors.""" if self._has_relative_humidity: - return round(self.thermostat.get_relative_humidity() * 100.0, 1) + return percent_conv(self._thermostat.get_relative_humidity()) return None @property def target_temperature(self): """Temperature we try to reach.""" - current_mode = self._device.get_current_mode() + current_mode = self._zone.get_current_mode() if current_mode == OPERATION_MODE_COOL: - return self._device.get_cooling_setpoint() + return self._zone.get_cooling_setpoint() if current_mode == OPERATION_MODE_HEAT: - return self._device.get_heating_setpoint() + return self._zone.get_heating_setpoint() return None @property def target_temperature_step(self): """Step size of temperature units.""" - if self._device.thermostat.get_unit() == UNIT_FAHRENHEIT: + if self._thermostat.get_unit() == UNIT_FAHRENHEIT: return 1.0 return 0.5 @property def target_temperature_high(self): """Highest temperature we are trying to reach.""" - current_mode = self._device.get_current_mode() + current_mode = self._zone.get_current_mode() if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT): return None - return self._device.get_cooling_setpoint() + return self._zone.get_cooling_setpoint() @property def target_temperature_low(self): """Lowest temperature we are trying to reach.""" - current_mode = self._device.get_current_mode() + current_mode = self._zone.get_current_mode() if current_mode in (OPERATION_MODE_COOL, OPERATION_MODE_HEAT): return None - return self._device.get_heating_setpoint() + return self._zone.get_heating_setpoint() @property def hvac_action(self) -> str: """Operation ie. heat, cool, idle.""" - system_status = self.thermostat.get_system_status() - zone_called = self._device.is_calling() + system_status = self._thermostat.get_system_status() + zone_called = self._zone.is_calling() - if self._device.get_requested_mode() == OPERATION_MODE_OFF: + if self._zone.get_requested_mode() == OPERATION_MODE_OFF: return CURRENT_HVAC_OFF if not zone_called: return CURRENT_HVAC_IDLE @@ -269,8 +297,8 @@ class NexiaZone(NexiaEntity, ClimateDevice): @property def hvac_mode(self): """Return current mode, as the user-visible name.""" - mode = self._device.get_requested_mode() - hold = self._device.is_in_permanent_hold() + mode = self._zone.get_requested_mode() + hold = self._zone.is_in_permanent_hold() # If the device is in hold mode with # OPERATION_MODE_AUTO @@ -299,10 +327,10 @@ class NexiaZone(NexiaEntity, ClimateDevice): new_cool_temp = kwargs.get(ATTR_TARGET_TEMP_HIGH, None) set_temp = kwargs.get(ATTR_TEMPERATURE, None) - deadband = self.thermostat.get_deadband() - cur_cool_temp = self._device.get_cooling_setpoint() - cur_heat_temp = self._device.get_heating_setpoint() - (min_temp, max_temp) = self.thermostat.get_setpoint_limits() + deadband = self._thermostat.get_deadband() + cur_cool_temp = self._zone.get_cooling_setpoint() + cur_heat_temp = self._zone.get_heating_setpoint() + (min_temp, max_temp) = self._thermostat.get_setpoint_limits() # Check that we're not going to hit any minimum or maximum values if new_heat_temp and new_heat_temp + deadband > max_temp: @@ -318,114 +346,119 @@ class NexiaZone(NexiaEntity, ClimateDevice): if new_cool_temp - new_heat_temp < deadband: new_heat_temp = new_cool_temp - deadband - self._device.set_heat_cool_temp( + self._zone.set_heat_cool_temp( heat_temperature=new_heat_temp, cool_temperature=new_cool_temp, set_temperature=set_temp, ) - self.schedule_update_ha_state() + self._signal_zone_update() @property def is_aux_heat(self): """Emergency heat state.""" - return self.thermostat.is_emergency_heat_active() - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._device.zone_id)}, - "name": self._device.get_name(), - "model": self.thermostat.get_model(), - "sw_version": self.thermostat.get_firmware(), - "manufacturer": MANUFACTURER, - "via_device": (DOMAIN, self.thermostat.thermostat_id), - } + return self._thermostat.is_emergency_heat_active() @property def device_state_attributes(self): """Return the device specific state attributes.""" - data = { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_ZONE_STATUS: self._device.get_status(), - } + data = super().device_state_attributes - if self._has_relative_humidity: - data.update( - { - ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support, - ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support, - ATTR_MIN_HUMIDITY: round( - self.thermostat.get_humidity_setpoint_limits()[0] * 100.0, 1, - ), - ATTR_MAX_HUMIDITY: round( - self.thermostat.get_humidity_setpoint_limits()[1] * 100.0, 1, - ), - } + data[ATTR_ZONE_STATUS] = self._zone.get_status() + + if not self._has_relative_humidity: + return data + + min_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[0]) + max_humidity = percent_conv(self._thermostat.get_humidity_setpoint_limits()[1]) + data.update( + { + ATTR_MIN_HUMIDITY: min_humidity, + ATTR_MAX_HUMIDITY: max_humidity, + ATTR_DEHUMIDIFY_SUPPORTED: self._has_dehumidify_support, + ATTR_HUMIDIFY_SUPPORTED: self._has_humidify_support, + } + ) + + if self._has_dehumidify_support: + dehumdify_setpoint = percent_conv( + self._thermostat.get_dehumidify_setpoint() ) - if self._has_dehumidify_support: - data.update( - { - ATTR_DEHUMIDIFY_SETPOINT: round( - self.thermostat.get_dehumidify_setpoint() * 100.0, 1 - ), - } - ) - if self._has_humidify_support: - data.update( - { - ATTR_HUMIDIFY_SETPOINT: round( - self.thermostat.get_humidify_setpoint() * 100.0, 1 - ) - } - ) + data[ATTR_DEHUMIDIFY_SETPOINT] = dehumdify_setpoint + + if self._has_humidify_support: + humdify_setpoint = percent_conv(self._thermostat.get_humidify_setpoint()) + data[ATTR_HUMIDIFY_SETPOINT] = humdify_setpoint + return data def set_preset_mode(self, preset_mode: str): """Set the preset mode.""" - self._device.set_preset(preset_mode) - self.schedule_update_ha_state() + self._zone.set_preset(preset_mode) + self._signal_zone_update() def turn_aux_heat_off(self): """Turn. Aux Heat off.""" - self.thermostat.set_emergency_heat(False) - self.schedule_update_ha_state() + self._thermostat.set_emergency_heat(False) + self._signal_thermostat_update() def turn_aux_heat_on(self): """Turn. Aux Heat on.""" - self.thermostat.set_emergency_heat(True) - self.schedule_update_ha_state() + self._thermostat.set_emergency_heat(True) + self._signal_thermostat_update() def turn_off(self): """Turn. off the zone.""" self.set_hvac_mode(OPERATION_MODE_OFF) - self.schedule_update_ha_state() + self._signal_zone_update() def turn_on(self): """Turn. on the zone.""" self.set_hvac_mode(OPERATION_MODE_AUTO) - self.schedule_update_ha_state() + self._signal_zone_update() def set_hvac_mode(self, hvac_mode: str) -> None: """Set the system mode (Auto, Heat_Cool, Cool, Heat, etc).""" if hvac_mode == HVAC_MODE_AUTO: - self._device.call_return_to_schedule() - self._device.set_mode(mode=OPERATION_MODE_AUTO) + self._zone.call_return_to_schedule() + self._zone.set_mode(mode=OPERATION_MODE_AUTO) else: - self._device.call_permanent_hold() - self._device.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) + self._zone.call_permanent_hold() + self._zone.set_mode(mode=HA_TO_NEXIA_HVAC_MODE_MAP[hvac_mode]) self.schedule_update_ha_state() def set_aircleaner_mode(self, aircleaner_mode): """Set the aircleaner mode.""" - self.thermostat.set_air_cleaner(aircleaner_mode) - self.schedule_update_ha_state() + self._thermostat.set_air_cleaner(aircleaner_mode) + self._signal_thermostat_update() - def set_humidify_setpoint(self, humidify_setpoint): + def set_humidify_setpoint(self, humidity): """Set the humidify setpoint.""" - self.thermostat.set_humidify_setpoint(humidify_setpoint / 100.0) - self.schedule_update_ha_state() + self._thermostat.set_humidify_setpoint(humidity / 100.0) + self._signal_thermostat_update() + + def _signal_thermostat_update(self): + """Signal a thermostat update. + + Whenever the underlying library does an action against + a thermostat, the data for the thermostat and all + connected zone is updated. + + Update all the zones on the thermostat. + """ + dispatcher_send( + self.hass, f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}" + ) + + def _signal_zone_update(self): + """Signal a zone update. + + Whenever the underlying library does an action against + a zone, the data for the zone is updated. + + Update a single zone. + """ + dispatcher_send(self.hass, f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}") async def async_update(self): """Update the entity. diff --git a/homeassistant/components/nexia/const.py b/homeassistant/components/nexia/const.py index 384c3aad1b6..dbe7b71705c 100644 --- a/homeassistant/components/nexia/const.py +++ b/homeassistant/components/nexia/const.py @@ -7,7 +7,6 @@ ATTRIBUTION = "Data provided by mynexia.com" NOTIFICATION_ID = "nexia_notification" NOTIFICATION_TITLE = "Nexia Setup" -DATA_NEXIA = "nexia" NEXIA_DEVICE = "device" NEXIA_SCAN_INTERVAL = "scan_interval" @@ -16,6 +15,8 @@ DEFAULT_ENTITY_NAMESPACE = "nexia" ATTR_DESCRIPTION = "description" +ATTR_AIRCLEANER_MODE = "aircleaner_mode" + ATTR_ZONE_STATUS = "zone_status" ATTR_HUMIDIFY_SUPPORTED = "humidify_supported" ATTR_DEHUMIDIFY_SUPPORTED = "dehumidify_supported" @@ -24,5 +25,7 @@ ATTR_DEHUMIDIFY_SETPOINT = "dehumidify_setpoint" UPDATE_COORDINATOR = "update_coordinator" - MANUFACTURER = "Trane" + +SIGNAL_ZONE_UPDATE = "NEXIA_CLIMATE_ZONE_UPDATE" +SIGNAL_THERMOSTAT_UPDATE = "NEXIA_CLIMATE_THERMOSTAT_UPDATE" diff --git a/homeassistant/components/nexia/entity.py b/homeassistant/components/nexia/entity.py index ec02a7e5f21..60675cc5888 100644 --- a/homeassistant/components/nexia/entity.py +++ b/homeassistant/components/nexia/entity.py @@ -1,14 +1,26 @@ """The nexia integration base entity.""" +from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity +from .const import ( + ATTRIBUTION, + DOMAIN, + MANUFACTURER, + SIGNAL_THERMOSTAT_UPDATE, + SIGNAL_ZONE_UPDATE, +) + class NexiaEntity(Entity): """Base class for nexia entities.""" - def __init__(self, coordinator): + def __init__(self, coordinator, name, unique_id): """Initialize the entity.""" super().__init__() + self._unique_id = unique_id + self._name = name self._coordinator = coordinator @property @@ -16,6 +28,23 @@ class NexiaEntity(Entity): """Return True if entity is available.""" return self._coordinator.last_update_success + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name.""" + return self._name + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return { + ATTR_ATTRIBUTION: ATTRIBUTION, + } + @property def should_poll(self): """Return False, updates are controlled via coordinator.""" @@ -28,3 +57,77 @@ class NexiaEntity(Entity): async def async_will_remove_from_hass(self): """Undo subscription.""" self._coordinator.async_remove_listener(self.async_write_ha_state) + + +class NexiaThermostatEntity(NexiaEntity): + """Base class for nexia devices attached to a thermostat.""" + + def __init__(self, coordinator, thermostat, name, unique_id): + """Initialize the entity.""" + super().__init__(coordinator, name, unique_id) + self._thermostat = thermostat + self._thermostat_update_subscription = None + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._thermostat.thermostat_id)}, + "name": self._thermostat.get_name(), + "model": self._thermostat.get_model(), + "sw_version": self._thermostat.get_firmware(), + "manufacturer": MANUFACTURER, + } + + async def async_added_to_hass(self): + """Listen for signals for services.""" + await super().async_added_to_hass() + self._thermostat_update_subscription = async_dispatcher_connect( + self.hass, + f"{SIGNAL_THERMOSTAT_UPDATE}-{self._thermostat.thermostat_id}", + self.async_write_ha_state, + ) + + async def async_will_remove_from_hass(self): + """Unsub from signals for services.""" + await super().async_will_remove_from_hass() + if self._thermostat_update_subscription: + self._thermostat_update_subscription() + + +class NexiaThermostatZoneEntity(NexiaThermostatEntity): + """Base class for nexia devices attached to a thermostat.""" + + def __init__(self, coordinator, zone, name, unique_id): + """Initialize the entity.""" + super().__init__(coordinator, zone.thermostat, name, unique_id) + self._zone = zone + self._zone_update_subscription = None + + @property + def device_info(self): + """Return the device_info of the device.""" + data = super().device_info + data.update( + { + "identifiers": {(DOMAIN, self._zone.zone_id)}, + "name": self._zone.get_name(), + "via_device": (DOMAIN, self._zone.thermostat.thermostat_id), + } + ) + return data + + async def async_added_to_hass(self): + """Listen for signals for services.""" + await super().async_added_to_hass() + self._zone_update_subscription = async_dispatcher_connect( + self.hass, + f"{SIGNAL_ZONE_UPDATE}-{self._zone.zone_id}", + self.async_write_ha_state, + ) + + async def async_will_remove_from_hass(self): + """Unsub from signals for services.""" + await super().async_will_remove_from_hass() + if self._zone_update_subscription: + self._zone_update_subscription() diff --git a/homeassistant/components/nexia/scene.py b/homeassistant/components/nexia/scene.py index 4489a4de274..fb851618aec 100644 --- a/homeassistant/components/nexia/scene.py +++ b/homeassistant/components/nexia/scene.py @@ -1,23 +1,18 @@ """Support for Nexia Automations.""" from homeassistant.components.scene import Scene -from homeassistant.const import ATTR_ATTRIBUTION +from homeassistant.helpers.event import async_call_later -from .const import ( - ATTR_DESCRIPTION, - ATTRIBUTION, - DATA_NEXIA, - DOMAIN, - NEXIA_DEVICE, - UPDATE_COORDINATOR, -) +from .const import ATTR_DESCRIPTION, DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR from .entity import NexiaEntity +SCENE_ACTIVATION_TIME = 5 + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up automations for a Nexia device.""" - nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_data = hass.data[DOMAIN][config_entry.entry_id] nexia_home = nexia_data[NEXIA_DEVICE] coordinator = nexia_data[UPDATE_COORDINATOR] entities = [] @@ -36,33 +31,28 @@ class NexiaAutomationScene(NexiaEntity, Scene): def __init__(self, coordinator, automation): """Initialize the automation scene.""" - super().__init__(coordinator) + super().__init__( + coordinator, name=automation.name, unique_id=automation.automation_id, + ) self._automation = automation - @property - def unique_id(self): - """Return the unique id of the automation scene.""" - # This is the automation unique_id - return self._automation.automation_id - - @property - def name(self): - """Return the name of the automation scene.""" - return self._automation.name - @property def device_state_attributes(self): """Return the scene specific state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - ATTR_DESCRIPTION: self._automation.description, - } + data = super().device_state_attributes + data[ATTR_DESCRIPTION] = self._automation.description + return data @property def icon(self): """Return the icon of the automation scene.""" return "mdi:script-text-outline" - def activate(self): + async def async_activate(self): """Activate an automation scene.""" - self._automation.activate() + await self.hass.async_add_executor_job(self._automation.activate) + + async def refresh_callback(_): + await self._coordinator.async_refresh() + + async_call_later(self.hass, SCENE_ACTIVATION_TIME, refresh_callback) diff --git a/homeassistant/components/nexia/sensor.py b/homeassistant/components/nexia/sensor.py index 251101ccb1e..abbffa2b844 100644 --- a/homeassistant/components/nexia/sensor.py +++ b/homeassistant/components/nexia/sensor.py @@ -3,7 +3,6 @@ from nexia.const import UNIT_CELSIUS from homeassistant.const import ( - ATTR_ATTRIBUTION, DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, @@ -11,21 +10,15 @@ from homeassistant.const import ( UNIT_PERCENTAGE, ) -from .const import ( - ATTRIBUTION, - DATA_NEXIA, - DOMAIN, - MANUFACTURER, - NEXIA_DEVICE, - UPDATE_COORDINATOR, -) -from .entity import NexiaEntity +from .const import DOMAIN, NEXIA_DEVICE, UPDATE_COORDINATOR +from .entity import NexiaThermostatEntity, NexiaThermostatZoneEntity +from .util import percent_conv async def async_setup_entry(hass, config_entry, async_add_entities): """Set up sensors for a Nexia device.""" - nexia_data = hass.data[DOMAIN][config_entry.entry_id][DATA_NEXIA] + nexia_data = hass.data[DOMAIN][config_entry.entry_id] nexia_home = nexia_data[NEXIA_DEVICE] coordinator = nexia_data[UPDATE_COORDINATOR] entities = [] @@ -35,7 +28,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): thermostat = nexia_home.get_thermostat_by_id(thermostat_id) entities.append( - NexiaSensor( + NexiaThermostatSensor( coordinator, thermostat, "get_system_status", @@ -46,7 +39,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) # Air cleaner entities.append( - NexiaSensor( + NexiaThermostatSensor( coordinator, thermostat, "get_air_cleaner_mode", @@ -58,7 +51,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Compressor Speed if thermostat.has_variable_speed_compressor(): entities.append( - NexiaSensor( + NexiaThermostatSensor( coordinator, thermostat, "get_current_compressor_speed", @@ -69,7 +62,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) ) entities.append( - NexiaSensor( + NexiaThermostatSensor( coordinator, thermostat, "get_requested_compressor_speed", @@ -87,7 +80,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): else TEMP_FAHRENHEIT ) entities.append( - NexiaSensor( + NexiaThermostatSensor( coordinator, thermostat, "get_outdoor_temperature", @@ -99,7 +92,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): # Relative Humidity if thermostat.has_relative_humidity(): entities.append( - NexiaSensor( + NexiaThermostatSensor( coordinator, thermostat, "get_relative_humidity", @@ -120,7 +113,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) # Temperature entities.append( - NexiaZoneSensor( + NexiaThermostatZoneSensor( coordinator, zone, "get_temperature", @@ -132,13 +125,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) # Zone Status entities.append( - NexiaZoneSensor( + NexiaThermostatZoneSensor( coordinator, zone, "get_status", "Zone Status", None, None, ) ) # Setpoint Status entities.append( - NexiaZoneSensor( + NexiaThermostatZoneSensor( coordinator, zone, "get_setpoint_status", @@ -151,18 +144,13 @@ async def async_setup_entry(hass, config_entry, async_add_entities): async_add_entities(entities, True) -def percent_conv(val): - """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" - return val * 100.0 - - -class NexiaSensor(NexiaEntity): +class NexiaThermostatSensor(NexiaThermostatEntity): """Provides Nexia thermostat sensor support.""" def __init__( self, coordinator, - device, + thermostat, sensor_call, sensor_name, sensor_class, @@ -170,35 +158,18 @@ class NexiaSensor(NexiaEntity): modifier=None, ): """Initialize the sensor.""" - super().__init__(coordinator) - self._coordinator = coordinator - self._device = device + super().__init__( + coordinator, + thermostat, + name=f"{thermostat.get_name()} {sensor_name}", + unique_id=f"{thermostat.thermostat_id}_{sensor_call}", + ) self._call = sensor_call - self._sensor_name = sensor_name self._class = sensor_class self._state = None - self._name = f"{self._device.get_name()} {self._sensor_name}" self._unit_of_measurement = sensor_unit self._modifier = modifier - @property - def unique_id(self): - """Return the unique id of the sensor.""" - # This is the thermostat unique_id - return f"{self._device.thermostat_id}_{self._call}" - - @property - def name(self): - """Return the name of the sensor.""" - return self._name - - @property - def device_state_attributes(self): - """Return the device specific state attributes.""" - return { - ATTR_ATTRIBUTION: ATTRIBUTION, - } - @property def device_class(self): """Return the device class of the sensor.""" @@ -207,7 +178,7 @@ class NexiaSensor(NexiaEntity): @property def state(self): """Return the state of the sensor.""" - val = getattr(self._device, self._call)() + val = getattr(self._thermostat, self._call)() if self._modifier: val = self._modifier(val) if isinstance(val, float): @@ -219,25 +190,14 @@ class NexiaSensor(NexiaEntity): """Return the unit of measurement this sensor expresses itself in.""" return self._unit_of_measurement - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._device.thermostat_id)}, - "name": self._device.get_name(), - "model": self._device.get_model(), - "sw_version": self._device.get_firmware(), - "manufacturer": MANUFACTURER, - } - -class NexiaZoneSensor(NexiaSensor): +class NexiaThermostatZoneSensor(NexiaThermostatZoneEntity): """Nexia Zone Sensor Support.""" def __init__( self, coordinator, - device, + zone, sensor_call, sensor_name, sensor_class, @@ -248,29 +208,32 @@ class NexiaZoneSensor(NexiaSensor): super().__init__( coordinator, - device, - sensor_call, - sensor_name, - sensor_class, - sensor_unit, - modifier, + zone, + name=f"{zone.get_name()} {sensor_name}", + unique_id=f"{zone.zone_id}_{sensor_call}", ) - self._device = device + self._call = sensor_call + self._class = sensor_class + self._state = None + self._unit_of_measurement = sensor_unit + self._modifier = modifier @property - def unique_id(self): - """Return the unique id of the sensor.""" - # This is the zone unique_id - return f"{self._device.zone_id}_{self._call}" + def device_class(self): + """Return the device class of the sensor.""" + return self._class @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._device.zone_id)}, - "name": self._device.get_name(), - "model": self._device.thermostat.get_model(), - "sw_version": self._device.thermostat.get_firmware(), - "manufacturer": MANUFACTURER, - "via_device": (DOMAIN, self._device.thermostat.thermostat_id), - } + def state(self): + """Return the state of the sensor.""" + val = getattr(self._zone, self._call)() + if self._modifier: + val = self._modifier(val) + if isinstance(val, float): + val = round(val, 1) + return val + + @property + def unit_of_measurement(self): + """Return the unit of measurement this sensor expresses itself in.""" + return self._unit_of_measurement diff --git a/homeassistant/components/nexia/services.yaml b/homeassistant/components/nexia/services.yaml new file mode 100644 index 00000000000..725b215da5a --- /dev/null +++ b/homeassistant/components/nexia/services.yaml @@ -0,0 +1,19 @@ +set_aircleaner_mode: + description: "The air cleaner mode." + fields: + entity_id: + description: "This setting will affect all zones connected to the thermostat." + example: climate.master_bedroom + aircleaner_mode: + description: "The air cleaner mode to set. Options include \"auto\", \"quick\", or \"allergy\"." + example: allergy + +set_humidify_setpoint: + description: "The humidification set point." + fields: + entity_id: + description: "This setting will affect all zones connected to the thermostat." + example: climate.master_bedroom + humidity: + description: "The humidification setpoint as an int, range 35-65." + example: 45 \ No newline at end of file diff --git a/homeassistant/components/nexia/util.py b/homeassistant/components/nexia/util.py new file mode 100644 index 00000000000..d2ff10c8d34 --- /dev/null +++ b/homeassistant/components/nexia/util.py @@ -0,0 +1,6 @@ +"""Utils for Nexia / Trane XL Thermostats.""" + + +def percent_conv(val): + """Convert an actual percentage (0.0-1.0) to 0-100 scale.""" + return round(val * 100.0, 1) diff --git a/tests/components/nexia/test_binary_sensor.py b/tests/components/nexia/test_binary_sensor.py new file mode 100644 index 00000000000..64b2946ee2f --- /dev/null +++ b/tests/components/nexia/test_binary_sensor.py @@ -0,0 +1,35 @@ +"""The binary_sensor tests for the nexia platform.""" + +from homeassistant.const import STATE_OFF, STATE_ON + +from .util import async_init_integration + + +async def test_create_binary_sensors(hass): + """Test creation of binary sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.master_suite_blower_active") + assert state.state == STATE_ON + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Master Suite Blower Active", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("binary_sensor.downstairs_east_wing_blower_active") + assert state.state == STATE_OFF + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Downstairs East Wing Blower Active", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index 327c611d277..e7675ff68b1 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -43,3 +43,38 @@ async def test_climate_zones(hass): assert all( state.attributes[key] == expected_attributes[key] for key in expected_attributes ) + + state = hass.states.get("climate.kitchen") + assert state.state == HVAC_MODE_HEAT_COOL + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "current_humidity": 36.0, + "current_temperature": 25.0, + "dehumidify_setpoint": 50.0, + "dehumidify_supported": True, + "fan_mode": "auto", + "fan_modes": ["auto", "on", "circulate"], + "friendly_name": "Kitchen", + "humidify_supported": False, + "humidity": 50.0, + "hvac_action": "idle", + "hvac_modes": ["off", "auto", "heat_cool", "heat", "cool"], + "max_humidity": 65.0, + "max_temp": 37.2, + "min_humidity": 35.0, + "min_temp": 12.8, + "preset_mode": "None", + "preset_modes": ["None", "Home", "Away", "Sleep"], + "supported_features": 31, + "target_temp_high": 26.1, + "target_temp_low": 17.2, + "target_temp_step": 1.0, + "temperature": None, + "zone_status": "", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/nexia/test_scene.py b/tests/components/nexia/test_scene.py index e6a5e94f083..4a325552e80 100644 --- a/tests/components/nexia/test_scene.py +++ b/tests/components/nexia/test_scene.py @@ -1,10 +1,10 @@ -"""The lock tests for the august platform.""" +"""The scene tests for the nexia platform.""" from .util import async_init_integration -async def test_automation_scenees(hass): - """Test creation automation scenees.""" +async def test_automation_scenes(hass): + """Test creation automation scenes.""" await async_init_integration(hass) diff --git a/tests/components/nexia/test_sensor.py b/tests/components/nexia/test_sensor.py new file mode 100644 index 00000000000..6e258d0ad55 --- /dev/null +++ b/tests/components/nexia/test_sensor.py @@ -0,0 +1,133 @@ +"""The sensor tests for the nexia platform.""" + +from .util import async_init_integration + + +async def test_create_sensors(hass): + """Test creation of sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.nick_office_temperature") + assert state.state == "23" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "device_class": "temperature", + "friendly_name": "Nick Office Temperature", + "unit_of_measurement": "°C", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.nick_office_zone_setpoint_status") + assert state.state == "Permanent Hold" + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Nick Office Zone Setpoint Status", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.nick_office_zone_status") + assert state.state == "Relieving Air" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Nick Office Zone Status", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.master_suite_air_cleaner_mode") + assert state.state == "auto" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Master Suite Air Cleaner Mode", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.master_suite_current_compressor_speed") + assert state.state == "69.0" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Master Suite Current Compressor Speed", + "unit_of_measurement": "%", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.master_suite_outdoor_temperature") + assert state.state == "30.6" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "device_class": "temperature", + "friendly_name": "Master Suite Outdoor Temperature", + "unit_of_measurement": "°C", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.master_suite_relative_humidity") + assert state.state == "52.0" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "device_class": "humidity", + "friendly_name": "Master Suite Relative Humidity", + "unit_of_measurement": "%", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.master_suite_requested_compressor_speed") + assert state.state == "69.0" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Master Suite Requested Compressor Speed", + "unit_of_measurement": "%", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("sensor.master_suite_system_status") + assert state.state == "Cooling" + + expected_attributes = { + "attribution": "Data provided by mynexia.com", + "friendly_name": "Master Suite System Status", + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) From bf4b099f11c3e79fac5627e5841d5486ba8b4b5a Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Mon, 23 Mar 2020 12:42:41 -0400 Subject: [PATCH 216/431] Update sw_version in device registry for ZHA devices (#33184) * Update firmware version in device registry. Parse recevied OTA requests for firmware version and update device registry. * Update tests. * Cleanup sw_id_update listener. * Update ZHA test devices list. --- .../components/zha/core/channels/general.py | 12 +- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/device.py | 34 +++- tests/components/zha/test_device.py | 40 ++++- tests/components/zha/zha_devices_list.py | 149 +++++++++--------- 5 files changed, 154 insertions(+), 82 deletions(-) diff --git a/homeassistant/components/zha/core/channels/general.py b/homeassistant/components/zha/core/channels/general.py index ffd5a03fc13..f2afadbd657 100644 --- a/homeassistant/components/zha/core/channels/general.py +++ b/homeassistant/components/zha/core/channels/general.py @@ -19,6 +19,7 @@ from ..const import ( SIGNAL_MOVE_LEVEL, SIGNAL_SET_LEVEL, SIGNAL_STATE_ATTR, + SIGNAL_UPDATE_DEVICE, ) from .base import ClientChannel, ZigbeeChannel, parse_and_log_command @@ -333,11 +334,20 @@ class OnOffConfiguration(ZigbeeChannel): pass +@registries.CLIENT_CHANNELS_REGISTRY.register(general.Ota.cluster_id) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Ota.cluster_id) class Ota(ZigbeeChannel): """OTA Channel.""" - pass + @callback + def cluster_command( + self, tsn: int, command_id: int, args: Optional[List[Any]] + ) -> None: + """Handle OTA commands.""" + cmd_name = self.cluster.server_commands.get(command_id, [command_id])[0] + signal_id = self._ch_pool.unique_id.split("-")[0] + if cmd_name == "query_next_image": + self.async_send_signal(SIGNAL_UPDATE_DEVICE.format(signal_id), args[3]) @registries.ZIGBEE_CHANNEL_REGISTRY.register(general.Partition.cluster_id) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 4b5a5a0c6a1..c2813c464e5 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -206,6 +206,7 @@ SIGNAL_MOVE_LEVEL = "move_level" SIGNAL_REMOVE = "remove" SIGNAL_SET_LEVEL = "set_level" SIGNAL_STATE_ATTR = "update_state_attribute" +SIGNAL_UPDATE_DEVICE = "{}_zha_update_device" UNKNOWN = "unknown" UNKNOWN_MANUFACTURER = "unk_manufacturer" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index f2544b43882..47b564f1767 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -54,6 +54,7 @@ from .const import ( POWER_BATTERY_OR_UNKNOWN, POWER_MAINS_POWERED, SIGNAL_AVAILABLE, + SIGNAL_UPDATE_DEVICE, UNKNOWN, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, @@ -92,8 +93,11 @@ class ZHADevice(LogMixin): self.name, self.ieee, SIGNAL_AVAILABLE ) self._checkins_missed_count = 0 - self._unsub = async_dispatcher_connect( - self.hass, self._available_signal, self.async_initialize + self.unsubs = [] + self.unsubs.append( + async_dispatcher_connect( + self.hass, self._available_signal, self.async_initialize + ) ) self.quirk_applied = isinstance(self._zigpy_device, zigpy.quirks.CustomDevice) self.quirk_class = "{}.{}".format( @@ -105,8 +109,10 @@ class ZHADevice(LogMixin): else: self._consider_unavailable_time = _CONSIDER_UNAVAILABLE_BATTERY keep_alive_interval = random.randint(*_UPDATE_ALIVE_INTERVAL) - self._cancel_available_check = async_track_time_interval( - self.hass, self._check_available, timedelta(seconds=keep_alive_interval) + self.unsubs.append( + async_track_time_interval( + self.hass, self._check_available, timedelta(seconds=keep_alive_interval) + ) ) self._ha_device_id = None self.status = DeviceStatus.CREATED @@ -276,8 +282,24 @@ class ZHADevice(LogMixin): """Create new device.""" zha_dev = cls(hass, zigpy_dev, gateway) zha_dev.channels = channels.Channels.new(zha_dev) + zha_dev.unsubs.append( + async_dispatcher_connect( + hass, + SIGNAL_UPDATE_DEVICE.format(zha_dev.channels.unique_id), + zha_dev.async_update_sw_build_id, + ) + ) return zha_dev + @callback + def async_update_sw_build_id(self, sw_version: int): + """Update device sw version.""" + if self.device_id is None: + return + self._zha_gateway.ha_device_registry.async_update_device( + self.device_id, sw_version=f"0x{sw_version:08x}" + ) + async def _check_available(self, *_): if self.last_seen is None: self.update_available(False) @@ -370,8 +392,8 @@ class ZHADevice(LogMixin): @callback def async_cleanup_handles(self) -> None: """Unsubscribe the dispatchers and timers.""" - self._unsub() - self._cancel_available_check() + for unsubscribe in self.unsubs: + unsubscribe() @callback def async_update_last_seen(self, last_seen): diff --git a/tests/components/zha/test_device.py b/tests/components/zha/test_device.py index edfab1d11d1..c92f574825d 100644 --- a/tests/components/zha/test_device.py +++ b/tests/components/zha/test_device.py @@ -8,9 +8,10 @@ import pytest import zigpy.zcl.clusters.general as general import homeassistant.components.zha.core.device as zha_core_device +import homeassistant.helpers.device_registry as ha_dev_reg import homeassistant.util.dt as dt_util -from .common import async_enable_traffic +from .common import async_enable_traffic, make_zcl_header from tests.common import async_fire_time_changed @@ -63,6 +64,26 @@ def device_without_basic_channel(zigpy_device): return zigpy_device(with_basic_channel=False) +@pytest.fixture +async def ota_zha_device(zha_device_restored, zigpy_device_mock): + """ZHA device with OTA cluster fixture.""" + zigpy_dev = zigpy_device_mock( + { + 1: { + "in_clusters": [general.Basic.cluster_id], + "out_clusters": [general.Ota.cluster_id], + "device_type": 0x1234, + } + }, + "00:11:22:33:44:55:66:77", + "test manufacturer", + "test model", + ) + + zha_device = await zha_device_restored(zigpy_dev) + return zha_device + + def _send_time_changed(hass, seconds): """Send a time changed event.""" now = dt_util.utcnow() + timedelta(seconds=seconds) @@ -190,3 +211,20 @@ async def test_check_available_no_basic_channel( await hass.async_block_till_done() assert zha_device.available is False assert "does not have a mandatory basic cluster" in caplog.text + + +async def test_ota_sw_version(hass, ota_zha_device): + """Test device entry gets sw_version updated via OTA channel.""" + + ota_ch = ota_zha_device.channels.pools[0].client_channels["1:0x0019"] + dev_registry = await ha_dev_reg.async_get_registry(hass) + entry = dev_registry.async_get(ota_zha_device.device_id) + assert entry.sw_version is None + + cluster = ota_ch.cluster + hdr = make_zcl_header(1, global_command=False) + sw_version = 0x2345 + cluster.handle_message(hdr, [1, 2, 3, sw_version, None]) + await hass.async_block_till_done() + entry = dev_registry.async_get(ota_zha_device.device_id) + assert int(entry.sw_version, base=16) == sw_version diff --git a/tests/components/zha/zha_devices_list.py b/tests/components/zha/zha_devices_list.py index b92fc64dee2..1d88ba69e8d 100644 --- a/tests/components/zha/zha_devices_list.py +++ b/tests/components/zha/zha_devices_list.py @@ -53,7 +53,7 @@ DEVICES = [ "entity_id": "binary_sensor.bosch_isw_zpr1_wp13_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["5:0x0019"], "manufacturer": "Bosch", "model": "ISW-ZPR1-WP13", "node_descriptor": b"\x02@\x08\x00\x00l\x00\x00\x00\x00\x00\x00\x00", @@ -77,7 +77,7 @@ DEVICES = [ "entity_id": "sensor.centralite_3130_77665544_power", } }, - "event_channels": ["1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "CentraLite", "model": "3130", "node_descriptor": b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", @@ -116,7 +116,7 @@ DEVICES = [ "entity_id": "sensor.centralite_3210_l_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "CentraLite", "model": "3210-L", "node_descriptor": b"\x01@\x8eN\x10RR\x00\x00\x00R\x00\x00", @@ -154,7 +154,7 @@ DEVICES = [ "entity_id": "sensor.centralite_3310_s_77665544_manufacturer_specific", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "CentraLite", "model": "3310-S", "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", @@ -200,7 +200,7 @@ DEVICES = [ "entity_id": "binary_sensor.centralite_3315_s_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "CentraLite", "model": "3315-S", "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", @@ -246,7 +246,7 @@ DEVICES = [ "entity_id": "binary_sensor.centralite_3320_l_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "CentraLite", "model": "3320-L", "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", @@ -292,7 +292,7 @@ DEVICES = [ "entity_id": "binary_sensor.centralite_3326_l_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "CentraLite", "model": "3326-L", "node_descriptor": b"\x02@\x80\xdf\xc2RR\x00\x00\x00R\x00\x00", @@ -344,7 +344,7 @@ DEVICES = [ "entity_id": "binary_sensor.centralite_motion_sensor_a_77665544_occupancy", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "CentraLite", "model": "Motion Sensor-A", "node_descriptor": b"\x02@\x80N\x10RR\x00\x00\x00R\x00\x00", @@ -384,7 +384,7 @@ DEVICES = [ "entity_id": "sensor.climaxtechnology_psmp5_00_00_02_02tc_77665544_smartenergy_metering", }, }, - "event_channels": [], + "event_channels": ["4:0x0019"], "manufacturer": "ClimaxTechnology", "model": "PSMP5_00.00.02.02TC", "node_descriptor": b"\x01@\x8e\x00\x00P\xa0\x00\x00\x00\xa0\x00\x00", @@ -501,7 +501,7 @@ DEVICES = [ "entity_id": "binary_sensor.heiman_smokesensor_em_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "HEIMAN", "model": "SmokeSensor-EM", "node_descriptor": b"\x02@\x80\x0b\x12RR\x00\x00\x00R\x00\x00", @@ -525,7 +525,7 @@ DEVICES = [ "entity_id": "binary_sensor.heiman_co_v16_77665544_ias_zone", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Heiman", "model": "CO_V16", "node_descriptor": b"\x02@\x84\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", @@ -549,7 +549,7 @@ DEVICES = [ "entity_id": "binary_sensor.heiman_warningdevice_77665544_ias_zone", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Heiman", "model": "WarningDevice", "node_descriptor": b"\x01@\x8e\x0b\x12RR\x00\x00\x00R\x00\x00", @@ -593,7 +593,7 @@ DEVICES = [ "entity_id": "binary_sensor.hivehome_com_mot003_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["6:0x0019"], "manufacturer": "HiveHome.com", "model": "MOT003", "node_descriptor": b"\x02@\x809\x10PP\x00\x00\x00P\x00\x00", @@ -627,7 +627,7 @@ DEVICES = [ "entity_id": "light.ikea_of_sweden_tradfri_bulb_e12_ws_opal_600lm_77665544_level_light_color_on_off", } }, - "event_channels": ["1:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E12 WS opal 600lm", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", @@ -653,7 +653,7 @@ DEVICES = [ "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_cws_opal_600lm_77665544_level_light_color_on_off", } }, - "event_channels": ["1:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 CWS opal 600lm", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", @@ -679,7 +679,7 @@ DEVICES = [ "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_w_opal_1000lm_77665544_level_on_off", } }, - "event_channels": ["1:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 W opal 1000lm", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", @@ -705,7 +705,7 @@ DEVICES = [ "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_ws_opal_980lm_77665544_level_light_color_on_off", } }, - "event_channels": ["1:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 WS opal 980lm", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", @@ -731,7 +731,7 @@ DEVICES = [ "entity_id": "light.ikea_of_sweden_tradfri_bulb_e26_opal_1000lm_77665544_level_on_off", } }, - "event_channels": ["1:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI bulb E26 opal 1000lm", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00\x00R\x00\x00", @@ -755,7 +755,7 @@ DEVICES = [ "entity_id": "switch.ikea_of_sweden_tradfri_control_outlet_77665544_on_off", } }, - "event_channels": ["1:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI control outlet", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", @@ -788,7 +788,7 @@ DEVICES = [ "entity_id": "binary_sensor.ikea_of_sweden_tradfri_motion_sensor_77665544_on_off", }, }, - "event_channels": ["1:0x0006"], + "event_channels": ["1:0x0006", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI motion sensor", "node_descriptor": b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", @@ -813,7 +813,7 @@ DEVICES = [ "entity_id": "sensor.ikea_of_sweden_tradfri_on_off_switch_77665544_power", } }, - "event_channels": ["1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI on/off switch", "node_descriptor": b"\x02@\x80|\x11RR\x00\x00,R\x00\x00", @@ -838,7 +838,7 @@ DEVICES = [ "entity_id": "sensor.ikea_of_sweden_tradfri_remote_control_77665544_power", } }, - "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI remote control", "node_descriptor": b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", @@ -864,7 +864,7 @@ DEVICES = [ }, "entities": [], "entity_map": {}, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI signal repeater", "node_descriptor": b"\x01@\x8e|\x11RR\x00\x00,R\x00\x00", @@ -888,7 +888,7 @@ DEVICES = [ "entity_id": "sensor.ikea_of_sweden_tradfri_wireless_dimmer_77665544_power", } }, - "event_channels": ["1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "IKEA of Sweden", "model": "TRADFRI wireless dimmer", "node_descriptor": b"\x02@\x80|\x11RR\x00\x00\x00R\x00\x00", @@ -927,7 +927,7 @@ DEVICES = [ "entity_id": "sensor.jasco_products_45852_77665544_smartenergy_metering", }, }, - "event_channels": ["2:0x0006", "2:0x0008"], + "event_channels": ["1:0x0019", "2:0x0006", "2:0x0008"], "manufacturer": "Jasco Products", "model": "45852", "node_descriptor": b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", @@ -966,7 +966,7 @@ DEVICES = [ "entity_id": "sensor.jasco_products_45856_77665544_smartenergy_metering", }, }, - "event_channels": ["2:0x0006"], + "event_channels": ["1:0x0019", "2:0x0006"], "manufacturer": "Jasco Products", "model": "45856", "node_descriptor": b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", @@ -1005,7 +1005,7 @@ DEVICES = [ "entity_id": "sensor.jasco_products_45857_77665544_smartenergy_metering", }, }, - "event_channels": ["2:0x0006", "2:0x0008"], + "event_channels": ["1:0x0019", "2:0x0006", "2:0x0008"], "manufacturer": "Jasco Products", "model": "45857", "node_descriptor": b"\x01@\x8e$\x11R\xff\x00\x00\x00\xff\x00\x00", @@ -1063,7 +1063,7 @@ DEVICES = [ "entity_id": "sensor.keen_home_inc_sv02_610_mp_1_3_77665544_pressure", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Keen Home Inc", "model": "SV02-610-MP-1.3", "node_descriptor": b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", @@ -1121,7 +1121,7 @@ DEVICES = [ "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_2_77665544_pressure", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Keen Home Inc", "model": "SV02-612-MP-1.2", "node_descriptor": b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", @@ -1179,7 +1179,7 @@ DEVICES = [ "entity_id": "sensor.keen_home_inc_sv02_612_mp_1_3_77665544_pressure", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Keen Home Inc", "model": "SV02-612-MP-1.3", "node_descriptor": b"\x02@\x80[\x11RR\x00\x00*R\x00\x00", @@ -1212,7 +1212,7 @@ DEVICES = [ "entity_id": "fan.king_of_fans_inc_hbuniversalcfremote_77665544_fan", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "King Of Fans, Inc.", "model": "HBUniversalCFRemote", "node_descriptor": b"\x02@\x8c\x02\x10RR\x00\x00\x00R\x00\x00", @@ -1237,7 +1237,7 @@ DEVICES = [ "entity_id": "sensor.lds_zbt_cctswitch_d0001_77665544_power", } }, - "event_channels": ["1:0x0006", "1:0x0008", "1:0x0300"], + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019", "1:0x0300"], "manufacturer": "LDS", "model": "ZBT-CCTSwitch-D0001", "node_descriptor": b"\x02@\x80h\x11RR\x00\x00,R\x00\x00", @@ -1262,7 +1262,7 @@ DEVICES = [ "entity_id": "light.ledvance_a19_rgbw_77665544_level_light_color_on_off", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LEDVANCE", "model": "A19 RGBW", "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", @@ -1286,7 +1286,7 @@ DEVICES = [ "entity_id": "light.ledvance_flex_rgbw_77665544_level_light_color_on_off", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LEDVANCE", "model": "FLEX RGBW", "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", @@ -1310,7 +1310,7 @@ DEVICES = [ "entity_id": "switch.ledvance_plug_77665544_on_off", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LEDVANCE", "model": "PLUG", "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", @@ -1334,7 +1334,7 @@ DEVICES = [ "entity_id": "light.ledvance_rt_rgbw_77665544_level_light_color_on_off", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LEDVANCE", "model": "RT RGBW", "node_descriptor": b"\x01@\x8e\x89\x11RR\x00\x00\x00R\x00\x00", @@ -1399,7 +1399,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_plug_maus01_77665544_analog_input_2", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LUMI", "model": "lumi.plug.maus01", "node_descriptor": b"\x01@\x8e_\x11\x7fd\x00\x00\x00d\x00\x00", @@ -1451,7 +1451,7 @@ DEVICES = [ "entity_id": "light.lumi_lumi_relay_c2acn01_77665544_on_off_2", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LUMI", "model": "lumi.relay.c2acn01", "node_descriptor": b"\x01@\x8e7\x10\x7fd\x00\x00\x00d\x00\x00", @@ -1510,7 +1510,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_remote_b186acn01_77665544_multistate_input", }, }, - "event_channels": ["1:0x0005", "2:0x0005", "3:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], "manufacturer": "LUMI", "model": "lumi.remote.b186acn01", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -1569,7 +1569,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_remote_b286acn01_77665544_multistate_input", }, }, - "event_channels": ["1:0x0005", "2:0x0005", "3:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], "manufacturer": "LUMI", "model": "lumi.remote.b286acn01", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -1925,7 +1925,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_sensor_86sw1_77665544_multistate_input", }, }, - "event_channels": ["1:0x0005", "2:0x0005", "3:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], "manufacturer": "LUMI", "model": "lumi.sensor_86sw1", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -1978,7 +1978,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_sensor_cube_aqgl01_77665544_analog_input", }, }, - "event_channels": ["1:0x0005", "2:0x0005", "3:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], "manufacturer": "LUMI", "model": "lumi.sensor_cube.aqgl01", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2031,7 +2031,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_sensor_ht_77665544_humidity", }, }, - "event_channels": ["1:0x0005", "2:0x0005", "3:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019", "2:0x0005", "3:0x0005"], "manufacturer": "LUMI", "model": "lumi.sensor_ht", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2064,7 +2064,7 @@ DEVICES = [ "entity_id": "binary_sensor.lumi_lumi_sensor_magnet_77665544_on_off", }, }, - "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "LUMI", "model": "lumi.sensor_magnet", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2142,7 +2142,7 @@ DEVICES = [ "entity_id": "binary_sensor.lumi_lumi_sensor_motion_aq2_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LUMI", "model": "lumi.sensor_motion.aq2", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2187,7 +2187,7 @@ DEVICES = [ "entity_id": "binary_sensor.lumi_lumi_sensor_smoke_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LUMI", "model": "lumi.sensor_smoke", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2212,7 +2212,7 @@ DEVICES = [ "entity_id": "sensor.lumi_lumi_sensor_switch_77665544_power", } }, - "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "LUMI", "model": "lumi.sensor_switch", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2303,7 +2303,7 @@ DEVICES = [ "entity_id": "binary_sensor.lumi_lumi_sensor_wleak_aq1_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "LUMI", "model": "lumi.sensor_wleak.aq1", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2349,7 +2349,7 @@ DEVICES = [ "entity_id": "binary_sensor.lumi_lumi_vibration_aq1_77665544_ias_zone", }, }, - "event_channels": ["1:0x0005", "2:0x0005"], + "event_channels": ["1:0x0005", "1:0x0019", "2:0x0005"], "manufacturer": "LUMI", "model": "lumi.vibration.aq1", "node_descriptor": b"\x02@\x807\x10\x7fd\x00\x00\x00d\x00\x00", @@ -2482,7 +2482,7 @@ DEVICES = [ "profile_id": 41440, }, }, - "entities": [], + "entities": ["1:0x0019"], "entity_map": {}, "event_channels": [], "manufacturer": None, @@ -2526,7 +2526,7 @@ DEVICES = [ "entity_id": "light.osram_lightify_a19_rgbw_77665544_level_light_color_on_off", } }, - "event_channels": [], + "event_channels": ["3:0x0019"], "manufacturer": "OSRAM", "model": "LIGHTIFY A19 RGBW", "node_descriptor": b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", @@ -2551,7 +2551,7 @@ DEVICES = [ "entity_id": "sensor.osram_lightify_dimming_switch_77665544_power", } }, - "event_channels": ["1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0006", "1:0x0008", "1:0x0019"], "manufacturer": "OSRAM", "model": "LIGHTIFY Dimming Switch", "node_descriptor": b"\x02@\x80\x0c\x11RR\x00\x00\x00R\x00\x00", @@ -2578,7 +2578,7 @@ DEVICES = [ "entity_id": "light.osram_lightify_flex_rgbw_77665544_level_light_color_on_off", } }, - "event_channels": [], + "event_channels": ["3:0x0019"], "manufacturer": "OSRAM", "model": "LIGHTIFY Flex RGBW", "node_descriptor": b"\x19@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", @@ -2611,7 +2611,7 @@ DEVICES = [ "entity_id": "sensor.osram_lightify_rt_tunable_white_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["3:0x0019"], "manufacturer": "OSRAM", "model": "LIGHTIFY RT Tunable White", "node_descriptor": b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", @@ -2644,7 +2644,7 @@ DEVICES = [ "entity_id": "sensor.osram_plug_01_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["3:0x0019"], "manufacturer": "OSRAM", "model": "Plug 01", "node_descriptor": b"\x01@\x8e\xaa\xbb@\x00\x00\x00\x00\x00\x00\x03", @@ -2707,6 +2707,7 @@ DEVICES = [ "1:0x0005", "1:0x0006", "1:0x0008", + "1:0x0019", "1:0x0300", "2:0x0005", "2:0x0006", @@ -2760,7 +2761,7 @@ DEVICES = [ "entity_id": "sensor.philips_rwl020_77665544_power", } }, - "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008"], + "event_channels": ["1:0x0005", "1:0x0006", "1:0x0008", "2:0x0019"], "manufacturer": "Philips", "model": "RWL020", "node_descriptor": b"\x02@\x80\x0b\x10G-\x00\x00\x00-\x00\x00", @@ -2799,7 +2800,7 @@ DEVICES = [ "entity_id": "binary_sensor.samjin_button_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Samjin", "model": "button", "node_descriptor": b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", @@ -2845,7 +2846,7 @@ DEVICES = [ "default_match": True, }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Samjin", "model": "multi", "node_descriptor": b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", @@ -2884,7 +2885,7 @@ DEVICES = [ "entity_id": "binary_sensor.samjin_water_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Samjin", "model": "water", "node_descriptor": b"\x02@\x80A\x12RR\x00\x00,R\x00\x00", @@ -2916,7 +2917,7 @@ DEVICES = [ "entity_id": "sensor.securifi_ltd_unk_model_77665544_electrical_measurement", }, }, - "event_channels": ["1:0x0005", "1:0x0006"], + "event_channels": ["1:0x0005", "1:0x0006", "1:0x0019"], "manufacturer": "Securifi Ltd.", "model": None, "node_descriptor": b"\x01@\x8e\x02\x10RR\x00\x00\x00R\x00\x00", @@ -2954,7 +2955,7 @@ DEVICES = [ "entity_id": "binary_sensor.sercomm_corp_sz_dws04n_sf_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Sercomm Corp.", "model": "SZ-DWS04N_SF", "node_descriptor": b"\x02@\x801\x11R\xff\x00\x00\x00\xff\x00\x00", @@ -2999,7 +3000,7 @@ DEVICES = [ "entity_id": "sensor.sercomm_corp_sz_esw01_77665544_electrical_measurement", }, }, - "event_channels": ["2:0x0006"], + "event_channels": ["1:0x0019", "2:0x0006"], "manufacturer": "Sercomm Corp.", "model": "SZ-ESW01", "node_descriptor": b"\x01@\x8e1\x11RR\x00\x00\x00R\x00\x00", @@ -3043,7 +3044,7 @@ DEVICES = [ "entity_id": "binary_sensor.sercomm_corp_sz_pir04_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Sercomm Corp.", "model": "SZ-PIR04", "node_descriptor": b"\x02@\x801\x11RR\x00\x00\x00R\x00\x00", @@ -3075,7 +3076,7 @@ DEVICES = [ "entity_id": "sensor.sinope_technologies_rm3250zb_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Sinope Technologies", "model": "RM3250ZB", "node_descriptor": b"\x11@\x8e\x9c\x11G+\x00\x00*+\x00\x00", @@ -3114,7 +3115,7 @@ DEVICES = [ "entity_id": "sensor.sinope_technologies_th1123zb_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Sinope Technologies", "model": "TH1123ZB", "node_descriptor": b"\x12@\x8c\x9c\x11G+\x00\x00\x00+\x00\x00", @@ -3154,7 +3155,7 @@ DEVICES = [ "entity_id": "sensor.sinope_technologies_th1124zb_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Sinope Technologies", "model": "TH1124ZB", "node_descriptor": b"\x11@\x8e\x9c\x11G+\x00\x00\x00+\x00\x00", @@ -3187,7 +3188,7 @@ DEVICES = [ "entity_id": "sensor.smartthings_outletv4_77665544_electrical_measurement", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "SmartThings", "model": "outletv4", "node_descriptor": b"\x01@\x8e\n\x11RR\x00\x00\x00R\x00\x00", @@ -3211,7 +3212,7 @@ DEVICES = [ "entity_id": "device_tracker.smartthings_tagv4_77665544_power", } }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "SmartThings", "model": "tagv4", "node_descriptor": b"\x02@\x80\n\x11RR\x00\x00\x00R\x00\x00", @@ -3307,7 +3308,7 @@ DEVICES = [ "entity_id": "binary_sensor.visonic_mct_340_e_77665544_ias_zone", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Visonic", "model": "MCT-340 E", "node_descriptor": b"\x02@\x80\x11\x10RR\x00\x00\x00R\x00\x00", @@ -3340,7 +3341,7 @@ DEVICES = [ "entity_id": "fan.zen_within_zen_01_77665544_fan", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "Zen Within", "model": "Zen-01", "node_descriptor": b"\x02@\x80X\x11R\x80\x00\x00\x00\x80\x00\x00", @@ -3405,7 +3406,7 @@ DEVICES = [ "entity_id": "light.tyzb01_ns1ndbww_ts0004_77665544_on_off_2", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "_TYZB01_ns1ndbww", "model": "TS0004", "node_descriptor": b"\x01@\x8e\x02\x10R\x00\x02\x00,\x00\x02\x00", @@ -3470,7 +3471,7 @@ DEVICES = [ "entity_id": "sensor.sengled_e11_g13_77665544_smartenergy_metering", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "sengled", "model": "E11-G13", "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", @@ -3502,7 +3503,7 @@ DEVICES = [ "entity_id": "sensor.sengled_e12_n14_77665544_smartenergy_metering", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "sengled", "model": "E12-N14", "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", @@ -3534,7 +3535,7 @@ DEVICES = [ "entity_id": "sensor.sengled_z01_a19nae26_77665544_smartenergy_metering", }, }, - "event_channels": [], + "event_channels": ["1:0x0019"], "manufacturer": "sengled", "model": "Z01-A19NAE26", "node_descriptor": b"\x02@\x8c`\x11RR\x00\x00\x00R\x00\x00", From ea23ffedfec9228f8f7d7ac3b4a245c833936c9f Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Mon, 23 Mar 2020 17:47:43 +0100 Subject: [PATCH 217/431] Update azure-pipelines-wheels.yml for Azure Pipelines --- azure-pipelines-wheels.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 3ed413f4678..b4ad0a556b2 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -19,7 +19,7 @@ schedules: always: true variables: - name: versionWheels - value: '1.10.0-3.7-alpine3.11' + value: '1.10.1-3.7-alpine3.11' resources: repositories: - repository: azure From 2360fd414126ba38f6e243b6a8b89adaae7abc50 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 23 Mar 2020 13:27:45 -0500 Subject: [PATCH 218/431] =?UTF-8?q?Switch=20legacy=20calls=20to=20init=5Fr?= =?UTF-8?q?ecorder=5Fcomponent=20using=20async=5Fadd=E2=80=A6=20(#33185)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/components/doorbird/test_config_flow.py | 24 ++++++++++++++----- tests/components/history/test_init.py | 4 ++-- tests/components/logbook/test_init.py | 4 ++-- 3 files changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 7a70aec9041..009062d0193 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -36,7 +36,9 @@ def _get_mock_doorbirdapi_side_effects(ready=None, info=None): async def test_user_form(hass): """Test we get the user form.""" - await hass.async_add_job(init_recorder_component, hass) # force in memory db + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -75,7 +77,9 @@ async def test_user_form(hass): async def test_form_import(hass): """Test we get the form with import source.""" - await hass.async_add_job(init_recorder_component, hass) # force in memory db + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db await setup.async_setup_component(hass, "persistent_notification", {}) @@ -125,7 +129,9 @@ async def test_form_import(hass): async def test_form_zeroconf_wrong_oui(hass): """Test we abort when we get the wrong OUI via zeroconf.""" - await hass.async_add_job(init_recorder_component, hass) # force in memory db + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db await setup.async_setup_component(hass, "persistent_notification", {}) @@ -142,7 +148,9 @@ async def test_form_zeroconf_wrong_oui(hass): async def test_form_zeroconf_correct_oui(hass): """Test we can setup from zeroconf with the correct OUI source.""" - await hass.async_add_job(init_recorder_component, hass) # force in memory db + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db await setup.async_setup_component(hass, "persistent_notification", {}) @@ -188,7 +196,9 @@ async def test_form_zeroconf_correct_oui(hass): async def test_form_user_cannot_connect(hass): """Test we handle cannot connect error.""" - await hass.async_add_job(init_recorder_component, hass) # force in memory db + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} @@ -209,7 +219,9 @@ async def test_form_user_cannot_connect(hass): async def test_form_user_invalid_auth(hass): """Test we handle cannot invalid auth error.""" - await hass.async_add_job(init_recorder_component, hass) # force in memory db + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 65c0a717bee..51f1e3cb2ac 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -617,7 +617,7 @@ class TestComponentHistory(unittest.TestCase): async def test_fetch_period_api(hass, hass_client): """Test the fetch period view for history.""" - await hass.async_add_job(init_recorder_component, hass) + await hass.async_add_executor_job(init_recorder_component, hass) await async_setup_component(hass, "history", {}) await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() @@ -629,7 +629,7 @@ async def test_fetch_period_api(hass, hass_client): async def test_fetch_period_api_with_include_order(hass, hass_client): """Test the fetch period view for history.""" - await hass.async_add_job(init_recorder_component, hass) + await hass.async_add_executor_job(init_recorder_component, hass) await async_setup_component( hass, "history", diff --git a/tests/components/logbook/test_init.py b/tests/components/logbook/test_init.py index 98653dc5a6c..cc07d6cf40f 100644 --- a/tests/components/logbook/test_init.py +++ b/tests/components/logbook/test_init.py @@ -1270,7 +1270,7 @@ class TestComponentLogbook(unittest.TestCase): async def test_logbook_view(hass, hass_client): """Test the logbook view.""" - await hass.async_add_job(init_recorder_component, hass) + await hass.async_add_executor_job(init_recorder_component, hass) await async_setup_component(hass, "logbook", {}) await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) client = await hass_client() @@ -1280,7 +1280,7 @@ async def test_logbook_view(hass, hass_client): async def test_logbook_view_period_entity(hass, hass_client): """Test the logbook view with period and entity.""" - await hass.async_add_job(init_recorder_component, hass) + await hass.async_add_executor_job(init_recorder_component, hass) await async_setup_component(hass, "logbook", {}) await hass.async_add_job(hass.data[recorder.DATA_INSTANCE].block_till_done) From 1ff245d9c2d233466bde5dcba84ade3f6cb9d4c0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 23 Mar 2020 12:59:36 -0700 Subject: [PATCH 219/431] =?UTF-8?q?Make=20sure=20entity=20platform=20servi?= =?UTF-8?q?ces=20work=20for=20all=20platforms=20of=20d=E2=80=A6=20(#33176)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Make sure entity platform services work for all platforms of domain * Register a bad service handler * Fix cleaning up * Tiny cleanup --- homeassistant/helpers/entity_component.py | 8 +++++- homeassistant/helpers/entity_platform.py | 35 +++++++++++++++++------ tests/helpers/test_entity_platform.py | 35 +++++++++++++++++++++++ 3 files changed, 69 insertions(+), 9 deletions(-) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 71c57dc13f1..a761273fd25 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -254,7 +254,13 @@ class EntityComponent: This method must be run in the event loop. """ - tasks = [platform.async_reset() for platform in self._platforms.values()] + tasks = [] + + for key, platform in self._platforms.items(): + if key == self.domain: + tasks.append(platform.async_reset()) + else: + tasks.append(platform.async_destroy()) if tasks: await asyncio.wait(tasks) diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0c288b0ad21..0aebaff14de 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -24,6 +24,7 @@ if TYPE_CHECKING: SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 PLATFORM_NOT_READY_RETRIES = 10 +DATA_ENTITY_PLATFORM = "entity_platform" class EntityPlatform: @@ -57,15 +58,15 @@ class EntityPlatform: self._async_cancel_retry_setup: Optional[CALLBACK_TYPE] = None self._process_updates: Optional[asyncio.Lock] = None + self.parallel_updates: Optional[asyncio.Semaphore] = None + # Platform is None for the EntityComponent "catch-all" EntityPlatform # which powers entity_component.add_entities - if platform is None: - self.parallel_updates_created = True - self.parallel_updates: Optional[asyncio.Semaphore] = None - return + self.parallel_updates_created = platform is None - self.parallel_updates_created = False - self.parallel_updates = None + hass.data.setdefault(DATA_ENTITY_PLATFORM, {}).setdefault( + self.platform_name, [] + ).append(self) @callback def _get_parallel_updates_semaphore( @@ -464,6 +465,14 @@ class EntityPlatform: self._async_unsub_polling() self._async_unsub_polling = None + async def async_destroy(self) -> None: + """Destroy an entity platform. + + Call before discarding the object. + """ + await self.async_reset() + self.hass.data[DATA_ENTITY_PLATFORM][self.platform_name].remove(self) + async def async_remove_entity(self, entity_id: str) -> None: """Remove entity id from platform.""" await self.entities[entity_id].async_remove() @@ -488,14 +497,24 @@ class EntityPlatform: @callback def async_register_entity_service(self, name, schema, func, required_features=None): - """Register an entity service.""" + """Register an entity service. + + Services will automatically be shared by all platforms of the same domain. + """ + if self.hass.services.has_service(self.platform_name, name): + return + if isinstance(schema, dict): schema = cv.make_entity_service_schema(schema) async def handle_service(call): """Handle the service.""" await service.entity_service_call( - self.hass, [self], func, call, required_features + self.hass, + self.hass.data[DATA_ENTITY_PLATFORM][self.platform_name], + func, + call, + required_features, ) self.hass.services.async_register( diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index d9cbbb31561..199284c680b 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -8,6 +8,7 @@ import asynctest import pytest from homeassistant.const import UNIT_PERCENTAGE +from homeassistant.core import callback from homeassistant.exceptions import PlatformNotReady from homeassistant.helpers import entity_platform, entity_registry from homeassistant.helpers.entity import async_generate_entity_id @@ -847,3 +848,37 @@ async def test_platform_with_no_setup(hass, caplog): "The mock-platform platform for the mock-integration integration does not support platform setup." in caplog.text ) + + +async def test_platforms_sharing_services(hass): + """Test platforms share services.""" + entity_platform1 = MockEntityPlatform( + hass, domain="mock_integration", platform_name="mock_platform", platform=None + ) + entity1 = MockEntity(entity_id="mock_integration.entity_1") + await entity_platform1.async_add_entities([entity1]) + + entity_platform2 = MockEntityPlatform( + hass, domain="mock_integration", platform_name="mock_platform", platform=None + ) + entity2 = MockEntity(entity_id="mock_integration.entity_2") + await entity_platform2.async_add_entities([entity2]) + + entities = [] + + @callback + def handle_service(entity, data): + entities.append(entity) + + entity_platform1.async_register_entity_service("hello", {}, handle_service) + entity_platform2.async_register_entity_service( + "hello", {}, Mock(side_effect=AssertionError("Should not be called")) + ) + + await hass.services.async_call( + "mock_platform", "hello", {"entity_id": "all"}, blocking=True + ) + + assert len(entities) == 2 + assert entity1 in entities + assert entity2 in entities From 513abcb7e540d54072dff0f7ee8f25dbc8bc4a46 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Mon, 23 Mar 2020 21:21:35 +0100 Subject: [PATCH 220/431] Add effect service to WLED integration (#33026) * Add effect service to WLED integration * Inline service schema --- homeassistant/components/wled/const.py | 4 + homeassistant/components/wled/light.py | 61 +++++++-- homeassistant/components/wled/services.yaml | 18 +++ tests/components/wled/test_light.py | 131 ++++++++++++++++++++ 4 files changed, 206 insertions(+), 8 deletions(-) create mode 100644 homeassistant/components/wled/services.yaml diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index 6c5cd9eaad4..6006952a580 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -17,6 +17,7 @@ ATTR_ON = "on" ATTR_PALETTE = "palette" ATTR_PLAYLIST = "playlist" ATTR_PRESET = "preset" +ATTR_REVERSE = "reverse" ATTR_SEGMENT_ID = "segment_id" ATTR_SOFTWARE_VERSION = "sw_version" ATTR_SPEED = "speed" @@ -26,3 +27,6 @@ ATTR_UDP_PORT = "udp_port" # Units of measurement CURRENT_MA = "mA" SIGNAL_DBM = "dBm" + +# Services +SERVICE_EFFECT = "effect" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 22c7e0649fc..beda19b8101 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -1,6 +1,8 @@ """Support for LED lights.""" import logging -from typing import Any, Callable, Dict, List, Optional, Tuple +from typing import Any, Callable, Dict, List, Optional, Tuple, Union + +import voluptuous as vol from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -18,6 +20,7 @@ from homeassistant.components.light import ( Light, ) from homeassistant.config_entries import ConfigEntry +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType import homeassistant.util.color as color_util @@ -30,9 +33,11 @@ from .const import ( ATTR_PALETTE, ATTR_PLAYLIST, ATTR_PRESET, + ATTR_REVERSE, ATTR_SEGMENT_ID, ATTR_SPEED, DOMAIN, + SERVICE_EFFECT, ) _LOGGER = logging.getLogger(__name__) @@ -48,6 +53,23 @@ async def async_setup_entry( """Set up WLED light based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + platform = entity_platform.current_platform.get() + + 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_REVERSE): cv.boolean, + vol.Optional(ATTR_SPEED): vol.All( + vol.Coerce(int), vol.Range(min=0, max=255) + ), + }, + "async_effect", + ) + lights = [ WLEDLight(entry.entry_id, coordinator, light.segment_id) for light in coordinator.data.state.segments @@ -94,16 +116,14 @@ class WLEDLight(Light, WLEDDeviceEntity): if preset == -1: preset = None + segment = self.coordinator.data.state.segments[self._segment] return { - ATTR_INTENSITY: self.coordinator.data.state.segments[ - self._segment - ].intensity, - ATTR_PALETTE: self.coordinator.data.state.segments[ - self._segment - ].palette.name, + ATTR_INTENSITY: segment.intensity, + ATTR_PALETTE: segment.palette.name, ATTR_PLAYLIST: playlist, ATTR_PRESET: preset, - ATTR_SPEED: self.coordinator.data.state.segments[self._segment].speed, + ATTR_REVERSE: segment.reverse, + ATTR_SPEED: segment.speed, } @property @@ -214,3 +234,28 @@ class WLEDLight(Light, WLEDDeviceEntity): data[ATTR_COLOR_PRIMARY] += (self.white_value,) await self.coordinator.wled.light(**data) + + @wled_exception_handler + async def async_effect( + self, + effect: Optional[Union[int, str]] = None, + intensity: Optional[int] = None, + reverse: Optional[bool] = None, + speed: Optional[int] = None, + ) -> None: + """Set the effect of a WLED light.""" + data = {ATTR_SEGMENT_ID: self._segment} + + if effect is not None: + data[ATTR_EFFECT] = effect + + if intensity is not None: + data[ATTR_INTENSITY] = intensity + + if reverse is not None: + data[ATTR_REVERSE] = reverse + + if speed is not None: + data[ATTR_SPEED] = speed + + await self.coordinator.wled.light(**data) diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml new file mode 100644 index 00000000000..90b14125ad8 --- /dev/null +++ b/homeassistant/components/wled/services.yaml @@ -0,0 +1,18 @@ +effect: + description: Controls the effect settings of WLED + fields: + entity_id: + description: Name of the WLED light entity. + example: "light.wled" + effect: + description: Name or ID of the WLED light effect. + example: "Rainbow" + intensity: + description: Intensity of the effect + example: 100 + speed: + description: Speed of the effect. Number between 0 (slow) and 255 (fast). + example: 150 + reverse: + description: Reverse the effect. Either true to reverse or false otherwise. + example: false diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index 3a03b93af30..c49ae6a12df 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -17,7 +17,10 @@ from homeassistant.components.wled.const import ( ATTR_PALETTE, ATTR_PLAYLIST, ATTR_PRESET, + ATTR_REVERSE, ATTR_SPEED, + DOMAIN, + SERVICE_EFFECT, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -52,6 +55,7 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_PALETTE) == "Default" assert state.attributes.get(ATTR_PLAYLIST) is None 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 @@ -70,6 +74,7 @@ async def test_rgb_light_state( assert state.attributes.get(ATTR_PALETTE) == "Random Cycle" assert state.attributes.get(ATTR_PLAYLIST) is None assert state.attributes.get(ATTR_PRESET) is None + assert state.attributes.get(ATTR_REVERSE) is False assert state.attributes.get(ATTR_SPEED) == 16 assert state.state == STATE_ON @@ -223,3 +228,129 @@ async def test_rgbw_light( light_mock.assert_called_once_with( color_primary=(0, 0, 0, 100), on=True, segment_id=0, ) + + +async def test_effect_service( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the effect service of a WLED light.""" + await init_integration(hass, aioclient_mock) + + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_EFFECT: "Rainbow", + ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_INTENSITY: 200, + ATTR_REVERSE: True, + ATTR_SPEED: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + effect="Rainbow", intensity=200, reverse=True, segment_id=0, speed=100, + ) + + with patch("wled.WLED.light") as light_mock: + 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() + light_mock.assert_called_once_with( + segment_id=0, effect=9, + ) + + with patch("wled.WLED.light") as light_mock: + 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() + light_mock.assert_called_once_with( + intensity=200, reverse=True, segment_id=0, speed=100, + ) + + with patch("wled.WLED.light") as light_mock: + await hass.services.async_call( + DOMAIN, + SERVICE_EFFECT, + { + ATTR_EFFECT: "Rainbow", + ATTR_ENTITY_ID: "light.wled_rgb_light", + ATTR_REVERSE: True, + ATTR_SPEED: 100, + }, + blocking=True, + ) + await hass.async_block_till_done() + light_mock.assert_called_once_with( + effect="Rainbow", reverse=True, segment_id=0, speed=100, + ) + + with patch("wled.WLED.light") as light_mock: + 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() + light_mock.assert_called_once_with( + effect="Rainbow", intensity=200, segment_id=0, speed=100, + ) + + with patch("wled.WLED.light") as light_mock: + 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() + light_mock.assert_called_once_with( + effect="Rainbow", intensity=200, reverse=True, segment_id=0, + ) + + +async def test_effect_service_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error handling of the WLED effect service.""" + aioclient_mock.post("http://example.local:80/json/state", text="", status=400) + await init_integration(hass, aioclient_mock) + + with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): + 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.state == STATE_ON + assert "Invalid response from API" in caplog.text From 5c2bd8b74378cd32549dc25a3a65d189687f23e2 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Mon, 23 Mar 2020 22:16:17 +0100 Subject: [PATCH 221/431] Add unique ID propery to braviatv entity (#33037) * Add unique_id property * Use cid to generate unique_id * Remove debug file * Add _unique_id declaration * Suggested change * Remove unused self._id --- .../components/braviatv/media_player.py | 23 ++++++++++++++----- 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 2916bb319f8..6dd431aac69 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -75,7 +75,11 @@ def setup_platform(hass, config, add_entities, discovery_info=None): pin = host_config["pin"] mac = host_config["mac"] name = config.get(CONF_NAME) - add_entities([BraviaTVDevice(host, mac, name, pin)]) + braviarc = BraviaRC(host, mac) + braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + unique_id = braviarc.get_system_info()["cid"].lower() + + add_entities([BraviaTVDevice(braviarc, name, pin, unique_id)]) return setup_bravia(config, pin, hass, add_entities) @@ -111,8 +115,11 @@ def setup_bravia(config, pin, hass, add_entities): hass.config.path(BRAVIA_CONFIG_FILE), {host: {"pin": pin, "host": host, "mac": mac}}, ) + braviarc = BraviaRC(host, mac) + braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) + unique_id = braviarc.get_system_info()["cid"].lower() - add_entities([BraviaTVDevice(host, mac, name, pin)]) + add_entities([BraviaTVDevice(braviarc, name, pin, unique_id)]) def request_configuration(config, hass, add_entities): @@ -154,11 +161,11 @@ def request_configuration(config, hass, add_entities): class BraviaTVDevice(MediaPlayerDevice): """Representation of a Sony Bravia TV.""" - def __init__(self, host, mac, name, pin): + def __init__(self, client, name, pin, unique_id): """Initialize the Sony Bravia device.""" self._pin = pin - self._braviarc = BraviaRC(host, mac) + self._braviarc = client self._name = name self._state = STATE_OFF self._muted = False @@ -171,15 +178,14 @@ class BraviaTVDevice(MediaPlayerDevice): self._content_mapping = {} self._duration = None self._content_uri = None - self._id = None self._playing = False self._start_date_time = None self._program_media_type = None self._min_volume = None self._max_volume = None self._volume = None + self._unique_id = unique_id - self._braviarc.connect(pin, CLIENTID_PREFIX, NICKNAME) if self._braviarc.is_connected(): self.update() else: @@ -254,6 +260,11 @@ class BraviaTVDevice(MediaPlayerDevice): """Return the name of the device.""" return self._name + @property + def unique_id(self): + """Return a unique_id for this entity.""" + return self._unique_id + @property def state(self): """Return the state of the device.""" From c2a9aba4674c892a6f9e913e4442911db76d18a6 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Mon, 23 Mar 2020 22:45:04 +0100 Subject: [PATCH 222/431] Minor changes for new pvpc_hourly_pricing integration (#33177) * Use async_call_later for retrying, instead of async_track_point_in_time * Normalize test filename renaming it to `test_sensor` * Remove link to docs in docstring * Adjust test results to new behavior with async_call_later --- .../components/pvpc_hourly_pricing/sensor.py | 21 ++++--------------- .../{test_sensor_logic.py => test_sensor.py} | 16 +++++++------- 2 files changed, 13 insertions(+), 24 deletions(-) rename tests/components/pvpc_hourly_pricing/{test_sensor_logic.py => test_sensor.py} (91%) diff --git a/homeassistant/components/pvpc_hourly_pricing/sensor.py b/homeassistant/components/pvpc_hourly_pricing/sensor.py index ff0b01f9ae4..199e20d3e22 100644 --- a/homeassistant/components/pvpc_hourly_pricing/sensor.py +++ b/homeassistant/components/pvpc_hourly_pricing/sensor.py @@ -1,10 +1,4 @@ -""" -Sensor to collect the reference daily prices of electricity ('PVPC') in Spain. - -For more details about this platform, please refer to the documentation at -https://www.home-assistant.io/integrations/pvpc_hourly_pricing/ -""" -from datetime import timedelta +"""Sensor to collect the reference daily prices of electricity ('PVPC') in Spain.""" import logging from random import randint from typing import Optional @@ -15,10 +9,7 @@ from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.event import ( - async_track_point_in_time, - async_track_time_change, -) +from homeassistant.helpers.event import async_call_later, async_track_time_change from homeassistant.helpers.restore_state import RestoreEntity import homeassistant.util.dt as dt_util @@ -144,18 +135,14 @@ class ElecPriceSensor(RestoreEntity): self._pvpc_data.source_available = False return - retry_delay = 2 * self._pvpc_data.timeout + retry_delay = 2 * self._num_retries * self._pvpc_data.timeout _LOGGER.debug( "%s: Bad update[retry:%d], will try again in %d s", self.entity_id, self._num_retries, retry_delay, ) - async_track_point_in_time( - self.hass, - self.async_update_prices, - dt_util.now() + timedelta(seconds=retry_delay), - ) + async_call_later(self.hass, retry_delay, self.async_update_prices) return if not prices: diff --git a/tests/components/pvpc_hourly_pricing/test_sensor_logic.py b/tests/components/pvpc_hourly_pricing/test_sensor.py similarity index 91% rename from tests/components/pvpc_hourly_pricing/test_sensor_logic.py rename to tests/components/pvpc_hourly_pricing/test_sensor.py index c6ae6fa57c2..fdab7fd1008 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor_logic.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -1,4 +1,4 @@ -"""Tests for the pvpc_hourly_pricing component.""" +"""Tests for the pvpc_hourly_pricing sensor component.""" from datetime import datetime, timedelta import logging from unittest.mock import patch @@ -27,7 +27,9 @@ async def _process_time_step( return state -async def test_availability(hass, caplog, pvpc_aioclient_mock: AiohttpClientMocker): +async def test_sensor_availability( + hass, caplog, pvpc_aioclient_mock: AiohttpClientMocker +): """Test sensor availability and handling of cloud access.""" hass.config.time_zone = timezone("Europe/Madrid") config = {DOMAIN: [{CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}]} @@ -59,7 +61,7 @@ async def test_availability(hass, caplog, pvpc_aioclient_mock: AiohttpClientMock ) assert num_warnings == 1 assert num_errors == 0 - assert pvpc_aioclient_mock.call_count == 7 + assert pvpc_aioclient_mock.call_count == 9 # check that it is silent until it becomes available again caplog.clear() @@ -67,19 +69,19 @@ async def test_availability(hass, caplog, pvpc_aioclient_mock: AiohttpClientMock # silent mode for _ in range(21): await _process_time_step(hass, mock_data, value="unavailable") - assert pvpc_aioclient_mock.call_count == 28 + assert pvpc_aioclient_mock.call_count == 30 assert len(caplog.messages) == 0 # warning about data access recovered await _process_time_step(hass, mock_data, value="unavailable") - assert pvpc_aioclient_mock.call_count == 29 + assert pvpc_aioclient_mock.call_count == 31 assert len(caplog.messages) == 1 assert caplog.records[0].levelno == logging.WARNING # working ok again await _process_time_step(hass, mock_data, "price_00h", value=0.06821) - assert pvpc_aioclient_mock.call_count == 30 + assert pvpc_aioclient_mock.call_count == 32 await _process_time_step(hass, mock_data, "price_01h", value=0.06627) - assert pvpc_aioclient_mock.call_count == 31 + assert pvpc_aioclient_mock.call_count == 33 assert len(caplog.messages) == 1 assert caplog.records[0].levelno == logging.WARNING From cd57b764ce9b0f14568ff1c3da8893450b832e70 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 24 Mar 2020 00:05:21 +0100 Subject: [PATCH 223/431] Fix state_automation_listener when new state is None (#32985) * Fix state_automation_listener when new state is None (fix #32984) * Listen to EVENT_STATE_CHANGED instead of using async_track_state_change and use the event context on automation trigger. * Share `process_state_match` with helpers/event * Add test for state change automation on entity removal --- homeassistant/components/automation/state.py | 47 +++++++++++++++----- homeassistant/helpers/event.py | 6 +-- tests/components/automation/test_state.py | 22 +++++++++ 3 files changed, 60 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/automation/state.py b/homeassistant/components/automation/state.py index fc3fff47514..29aea64c9c5 100644 --- a/homeassistant/components/automation/state.py +++ b/homeassistant/components/automation/state.py @@ -6,10 +6,14 @@ from typing import Dict import voluptuous as vol from homeassistant import exceptions -from homeassistant.const import CONF_FOR, CONF_PLATFORM, MATCH_ALL +from homeassistant.const import CONF_FOR, CONF_PLATFORM, EVENT_STATE_CHANGED, MATCH_ALL from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers import config_validation as cv, template -from homeassistant.helpers.event import async_track_same_state, async_track_state_change +from homeassistant.helpers.event import ( + Event, + async_track_same_state, + process_state_match, +) # mypy: allow-incomplete-defs, allow-untyped-calls, allow-untyped-defs # mypy: no-check-untyped-defs @@ -56,10 +60,30 @@ async def async_attach_trigger( match_all = from_state == MATCH_ALL and to_state == MATCH_ALL unsub_track_same = {} period: Dict[str, timedelta] = {} + match_from_state = process_state_match(from_state) + match_to_state = process_state_match(to_state) @callback - def state_automation_listener(entity, from_s, to_s): + def state_automation_listener(event: Event): """Listen for state changes and calls action.""" + entity: str = event.data["entity_id"] + if entity not in entity_id: + return + + from_s = event.data.get("old_state") + to_s = event.data.get("new_state") + + if ( + (from_s is not None and not match_from_state(from_s.state)) + or (to_s is not None and not match_to_state(to_s.state)) + or ( + not match_all + and from_s is not None + and to_s is not None + and from_s.state == to_s.state + ) + ): + return @callback def call_action(): @@ -75,7 +99,7 @@ async def async_attach_trigger( "for": time_delta if not time_delta else period[entity], } }, - context=to_s.context, + context=event.context, ) ) @@ -120,17 +144,16 @@ async def async_attach_trigger( ) return + def _check_same_state(_, _2, new_st): + if new_st is None: + return False + return new_st.state == to_s.state + unsub_track_same[entity] = async_track_same_state( - hass, - period[entity], - call_action, - lambda _, _2, to_state: to_state.state == to_s.state, - entity_ids=entity, + hass, period[entity], call_action, _check_same_state, entity_ids=entity, ) - unsub = async_track_state_change( - hass, entity_id, state_automation_listener, from_state, to_state - ) + unsub = hass.bus.async_listen(EVENT_STATE_CHANGED, state_automation_listener) @callback def async_remove(): diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index 74faca6a1d2..8a4b4bc2b76 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -67,8 +67,8 @@ def async_track_state_change( Must be run within the event loop. """ - match_from_state = _process_state_match(from_state) - match_to_state = _process_state_match(to_state) + match_from_state = process_state_match(from_state) + match_to_state = process_state_match(to_state) # Ensure it is a lowercase list with entity ids we want to match on if entity_ids == MATCH_ALL: @@ -473,7 +473,7 @@ def async_track_time_change( track_time_change = threaded_listener_factory(async_track_time_change) -def _process_state_match( +def process_state_match( parameter: Union[None, str, Iterable[str]] ) -> Callable[[str], bool]: """Convert parameter to function that matches input against parameter.""" diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 9d4fa9a1100..173af8158a4 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -519,6 +519,28 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): assert 1 == len(calls) +async def test_if_fires_on_entity_removal(hass, calls): + """Test for firing on entity removal, when new_state is None.""" + hass.states.async_set("test.entity", "hello") + await hass.async_block_till_done() + + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: { + "trigger": {"platform": "state", "entity_id": "test.entity"}, + "action": {"service": "test.automation"}, + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.async_remove("test.entity") + await hass.async_block_till_done() + assert 1 == len(calls) + + async def test_if_fires_on_for_condition(hass, calls): """Test for firing if condition is on.""" point1 = dt_util.utcnow() From 45241e57ca518e453be3707fc47ccc24df0d5eef Mon Sep 17 00:00:00 2001 From: elmurato <1382097+elmurato@users.noreply.github.com> Date: Tue, 24 Mar 2020 00:51:13 +0100 Subject: [PATCH 224/431] Add support for Minecraft SRV records (#32372) * Added support for Minecraft SRV records * Switched from dnspython to aiodns, improved server ping and log messages, use address instead of host and port in config flow * Updated component requirements --- .coveragerc | 1 + .../components/minecraft_server/__init__.py | 48 ++++-- .../minecraft_server/config_flow.py | 85 ++++++--- .../components/minecraft_server/const.py | 4 +- .../components/minecraft_server/helpers.py | 32 ++++ .../components/minecraft_server/manifest.json | 2 +- .../components/minecraft_server/strings.json | 3 +- requirements_all.txt | 1 + requirements_test_all.txt | 4 + .../minecraft_server/test_config_flow.py | 161 ++++++++++++------ 10 files changed, 245 insertions(+), 96 deletions(-) create mode 100644 homeassistant/components/minecraft_server/helpers.py diff --git a/.coveragerc b/.coveragerc index 09ff6115ce2..cc04fb03456 100644 --- a/.coveragerc +++ b/.coveragerc @@ -425,6 +425,7 @@ omit = homeassistant/components/minecraft_server/__init__.py homeassistant/components/minecraft_server/binary_sensor.py homeassistant/components/minecraft_server/const.py + homeassistant/components/minecraft_server/helpers.py homeassistant/components/minecraft_server/sensor.py homeassistant/components/minio/* homeassistant/components/mitemp_bt/sensor.py diff --git a/homeassistant/components/minecraft_server/__init__.py b/homeassistant/components/minecraft_server/__init__.py index a025c44e33c..3a8598d3fac 100644 --- a/homeassistant/components/minecraft_server/__init__.py +++ b/homeassistant/components/minecraft_server/__init__.py @@ -18,6 +18,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_track_time_interval from homeassistant.helpers.typing import ConfigType, HomeAssistantType +from . import helpers from .const import DOMAIN, MANUFACTURER, SCAN_INTERVAL, SIGNAL_NAME_PREFIX PLATFORMS = ["binary_sensor", "sensor"] @@ -37,10 +38,9 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) # Create and store server instance. unique_id = config_entry.unique_id _LOGGER.debug( - "Creating server instance for '%s' (host='%s', port=%s)", + "Creating server instance for '%s' (%s)", config_entry.data[CONF_NAME], config_entry.data[CONF_HOST], - config_entry.data[CONF_PORT], ) server = MinecraftServer(hass, unique_id, config_entry.data) domain_data[unique_id] = server @@ -82,7 +82,6 @@ class MinecraftServer: """Representation of a Minecraft server.""" # Private constants - _MAX_RETRIES_PING = 3 _MAX_RETRIES_STATUS = 3 def __init__( @@ -98,6 +97,7 @@ class MinecraftServer: self.port = config_data[CONF_PORT] self.online = False self._last_status_request_failed = False + self.srv_record_checked = False # 3rd party library instance self._mc_status = MCStatus(self.host, self.port) @@ -127,15 +127,36 @@ class MinecraftServer: self._stop_periodic_update() async def async_check_connection(self) -> None: - """Check server connection using a 'ping' request and store result.""" + """Check server connection using a 'status' request and store connection status.""" + # Check if host is a valid SRV record, if not already done. + if not self.srv_record_checked: + self.srv_record_checked = True + srv_record = await helpers.async_check_srv_record(self._hass, self.host) + if srv_record is not None: + _LOGGER.debug( + "'%s' is a valid Minecraft SRV record ('%s:%s')", + self.host, + srv_record[CONF_HOST], + srv_record[CONF_PORT], + ) + # Overwrite host, port and 3rd party library instance + # with data extracted out of SRV record. + self.host = srv_record[CONF_HOST] + self.port = srv_record[CONF_PORT] + self._mc_status = MCStatus(self.host, self.port) + + # Ping the server with a status request. try: await self._hass.async_add_executor_job( - self._mc_status.ping, self._MAX_RETRIES_PING + self._mc_status.status, self._MAX_RETRIES_STATUS ) self.online = True except OSError as error: _LOGGER.debug( - "Error occurred while trying to ping the server - OSError: %s", error + "Error occurred while trying to check the connection to '%s:%s' - OSError: %s", + self.host, + self.port, + error, ) self.online = False @@ -148,9 +169,9 @@ class MinecraftServer: # Inform user once about connection state changes if necessary. if server_online_old and not server_online: - _LOGGER.warning("Connection to server lost") + _LOGGER.warning("Connection to '%s:%s' lost", self.host, self.port) elif not server_online_old and server_online: - _LOGGER.info("Connection to server (re-)established") + _LOGGER.info("Connection to '%s:%s' (re-)established", self.host, self.port) # Update the server properties if server is online. if server_online: @@ -179,7 +200,11 @@ class MinecraftServer: # Inform user once about successful update if necessary. if self._last_status_request_failed: - _LOGGER.info("Updating the server properties succeeded again") + _LOGGER.info( + "Updating the properties of '%s:%s' succeeded again", + self.host, + self.port, + ) self._last_status_request_failed = False except OSError as error: # No answer to request, set all properties to unknown. @@ -193,7 +218,10 @@ class MinecraftServer: # Inform user once about failed update if necessary. if not self._last_status_request_failed: _LOGGER.warning( - "Updating the server properties failed - OSError: %s", error, + "Updating the properties of '%s:%s' failed - OSError: %s", + self.host, + self.port, + error, ) self._last_status_request_failed = True diff --git a/homeassistant/components/minecraft_server/config_flow.py b/homeassistant/components/minecraft_server/config_flow.py index 8c6049a2c1b..a7cb0371f67 100644 --- a/homeassistant/components/minecraft_server/config_flow.py +++ b/homeassistant/components/minecraft_server/config_flow.py @@ -9,7 +9,7 @@ from homeassistant import config_entries from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT -from . import MinecraftServer +from . import MinecraftServer, helpers from .const import ( # pylint: disable=unused-import DEFAULT_HOST, DEFAULT_NAME, @@ -29,11 +29,24 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): errors = {} if user_input is not None: - # User inputs. - host = user_input[CONF_HOST] - port = user_input[CONF_PORT] + host = None + port = DEFAULT_PORT + # Split address at last occurrence of ':'. + address_left, separator, address_right = user_input[CONF_HOST].rpartition( + ":" + ) + # If no separator is found, 'rpartition' return ('', '', original_string). + if separator == "": + host = address_right + else: + host = address_left + try: + port = int(address_right) + except ValueError: + pass # 'port' is already set to default value. - unique_id = "" + # Remove '[' and ']' in case of an IPv6 address. + host = host.strip("[]") # Check if 'host' is a valid IP address and if so, get the MAC address. ip_address = None @@ -42,6 +55,7 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): ip_address = ipaddress.ip_address(host) except ValueError: # Host is not a valid IP address. + # Continue with host and port. pass else: # Host is a valid IP address. @@ -55,38 +69,56 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): partial(getmac.get_mac_address, **params) ) - # Validate IP address via valid MAC address. + # Validate IP address (MAC address must be available). if ip_address is not None and mac_address is None: errors["base"] = "invalid_ip" # Validate port configuration (limit to user and dynamic port range). elif (port < 1024) or (port > 65535): errors["base"] = "invalid_port" - # Validate host and port via ping request to server. + # Validate host and port by checking the server connection. else: - # Build unique_id. - if ip_address is not None: - # Since IP addresses can change and therefore are not allowed in a - # unique_id, fall back to the MAC address. - unique_id = f"{mac_address}-{port}" - else: - # Use host name in unique_id (host names should not change). - unique_id = f"{host}-{port}" - - # Abort in case the host was already configured before. - await self.async_set_unique_id(unique_id) - self._abort_if_unique_id_configured() - - # Create server instance with configuration data and try pinging the server. - server = MinecraftServer(self.hass, unique_id, user_input) + # Create server instance with configuration data and ping the server. + config_data = { + CONF_NAME: user_input[CONF_NAME], + CONF_HOST: host, + CONF_PORT: port, + } + server = MinecraftServer(self.hass, "dummy_unique_id", config_data) await server.async_check_connection() if not server.online: # Host or port invalid or server not reachable. errors["base"] = "cannot_connect" else: + # Build unique_id and config entry title. + unique_id = "" + title = f"{host}:{port}" + if ip_address is not None: + # Since IP addresses can change and therefore are not allowed in a + # unique_id, fall back to the MAC address and port (to support + # servers with same MAC address but different ports). + unique_id = f"{mac_address}-{port}" + if ip_address.version == 6: + title = f"[{host}]:{port}" + else: + # Check if 'host' is a valid SRV record. + srv_record = await helpers.async_check_srv_record( + self.hass, host + ) + if srv_record is not None: + # Use only SRV host name in unique_id (does not change). + unique_id = f"{host}-srv" + title = host + else: + # Use host name and port in unique_id (to support servers with + # same host name but different ports). + unique_id = f"{host}-{port}" + + # Abort in case the host was already configured before. + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + # Configuration data are available and no error was detected, create configuration entry. - return self.async_create_entry( - title=f"{host}:{port}", data=user_input - ) + return self.async_create_entry(title=title, data=config_data) # Show configuration form (default form in case of no user_input, # form filled with user_input and eventually with errors otherwise). @@ -107,9 +139,6 @@ class MinecraftServerConfigFlow(ConfigFlow, domain=DOMAIN): vol.Required( CONF_HOST, default=user_input.get(CONF_HOST, DEFAULT_HOST) ): vol.All(str, vol.Lower), - vol.Optional( - CONF_PORT, default=user_input.get(CONF_PORT, DEFAULT_PORT) - ): int, } ), errors=errors, diff --git a/homeassistant/components/minecraft_server/const.py b/homeassistant/components/minecraft_server/const.py index d86faf23a81..52e6ae8fd5e 100644 --- a/homeassistant/components/minecraft_server/const.py +++ b/homeassistant/components/minecraft_server/const.py @@ -2,7 +2,7 @@ ATTR_PLAYERS_LIST = "players_list" -DEFAULT_HOST = "localhost" +DEFAULT_HOST = "localhost:25565" DEFAULT_NAME = "Minecraft Server" DEFAULT_PORT = 25565 @@ -30,6 +30,8 @@ SCAN_INTERVAL = 60 SIGNAL_NAME_PREFIX = f"signal_{DOMAIN}" +SRV_RECORD_PREFIX = "_minecraft._tcp" + UNIT_PLAYERS_MAX = "players" UNIT_PLAYERS_ONLINE = "players" UNIT_PROTOCOL_VERSION = None diff --git a/homeassistant/components/minecraft_server/helpers.py b/homeassistant/components/minecraft_server/helpers.py new file mode 100644 index 00000000000..7f9380cdec2 --- /dev/null +++ b/homeassistant/components/minecraft_server/helpers.py @@ -0,0 +1,32 @@ +"""Helper functions for the Minecraft Server integration.""" + +from typing import Any, Dict + +import aiodns + +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.typing import HomeAssistantType + +from .const import SRV_RECORD_PREFIX + + +async def async_check_srv_record(hass: HomeAssistantType, host: str) -> Dict[str, Any]: + """Check if the given host is a valid Minecraft SRV record.""" + # Check if 'host' is a valid SRV record. + return_value = None + srv_records = None + try: + srv_records = await aiodns.DNSResolver().query( + host=f"{SRV_RECORD_PREFIX}.{host}", qtype="SRV" + ) + except (aiodns.error.DNSError): + # 'host' is not a SRV record. + pass + else: + # 'host' is a valid SRV record, extract the data. + return_value = { + CONF_HOST: srv_records[0].host, + CONF_PORT: srv_records[0].port, + } + + return return_value diff --git a/homeassistant/components/minecraft_server/manifest.json b/homeassistant/components/minecraft_server/manifest.json index 1dda76dee77..0811c168f9f 100644 --- a/homeassistant/components/minecraft_server/manifest.json +++ b/homeassistant/components/minecraft_server/manifest.json @@ -3,7 +3,7 @@ "name": "Minecraft Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/minecraft_server", - "requirements": ["getmac==0.8.1", "mcstatus==2.3.0"], + "requirements": ["aiodns==2.0.0", "getmac==0.8.1", "mcstatus==2.3.0"], "dependencies": [], "codeowners": ["@elmurato"], "quality_scale": "silver" diff --git a/homeassistant/components/minecraft_server/strings.json b/homeassistant/components/minecraft_server/strings.json index 7743d940be6..3a2408694ad 100644 --- a/homeassistant/components/minecraft_server/strings.json +++ b/homeassistant/components/minecraft_server/strings.json @@ -7,8 +7,7 @@ "description": "Set up your Minecraft Server instance to allow monitoring.", "data": { "name": "Name", - "host": "Host", - "port": "Port" + "host": "Host" } } }, diff --git a/requirements_all.txt b/requirements_all.txt index 72bc9d27e7e..00a7a0a0073 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -152,6 +152,7 @@ aioautomatic==0.6.5 aiobotocore==0.11.1 # homeassistant.components.dnsip +# homeassistant.components.minecraft_server aiodns==2.0.0 # homeassistant.components.esphome diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8ed63bf8b3..231dfcd0cc7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -58,6 +58,10 @@ aioautomatic==0.6.5 # homeassistant.components.aws aiobotocore==0.11.1 +# homeassistant.components.dnsip +# homeassistant.components.minecraft_server +aiodns==2.0.0 + # homeassistant.components.esphome aioesphomeapi==2.6.1 diff --git a/tests/components/minecraft_server/test_config_flow.py b/tests/components/minecraft_server/test_config_flow.py index 30626fbdcb0..bc49ed08109 100644 --- a/tests/components/minecraft_server/test_config_flow.py +++ b/tests/components/minecraft_server/test_config_flow.py @@ -1,5 +1,8 @@ """Test the Minecraft Server config flow.""" +import asyncio + +import aiodns from asynctest import patch from mcstatus.pinger import PingResponse @@ -19,6 +22,19 @@ from homeassistant.helpers.typing import HomeAssistantType from tests.common import MockConfigEntry + +class QueryMock: + """Mock for result of aiodns.DNSResolver.query.""" + + def __init__(self): + """Set up query result mock.""" + self.host = "mc.dummyserver.com" + self.port = 23456 + self.priority = 1 + self.weight = 1 + self.ttl = None + + STATUS_RESPONSE_RAW = { "description": {"text": "Dummy Description"}, "version": {"name": "Dummy Version", "protocol": 123}, @@ -35,34 +51,34 @@ STATUS_RESPONSE_RAW = { USER_INPUT = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: "mc.dummyserver.com", - CONF_PORT: DEFAULT_PORT, + CONF_HOST: f"mc.dummyserver.com:{DEFAULT_PORT}", } +USER_INPUT_SRV = {CONF_NAME: DEFAULT_NAME, CONF_HOST: "dummyserver.com"} + USER_INPUT_IPV4 = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: "1.1.1.1", - CONF_PORT: DEFAULT_PORT, + CONF_HOST: f"1.1.1.1:{DEFAULT_PORT}", } USER_INPUT_IPV6 = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: "::ffff:0101:0101", - CONF_PORT: DEFAULT_PORT, + CONF_HOST: f"[::ffff:0101:0101]:{DEFAULT_PORT}", } USER_INPUT_PORT_TOO_SMALL = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: "mc.dummyserver.com", - CONF_PORT: 1023, + CONF_HOST: f"mc.dummyserver.com:1023", } USER_INPUT_PORT_TOO_LARGE = { CONF_NAME: DEFAULT_NAME, - CONF_HOST: "mc.dummyserver.com", - CONF_PORT: 65536, + CONF_HOST: f"mc.dummyserver.com:65536", } +SRV_RECORDS = asyncio.Future() +SRV_RECORDS.set_result([QueryMock()]) + async def test_show_config_form(hass: HomeAssistantType) -> None: """Test if initial configuration form is shown.""" @@ -87,54 +103,96 @@ async def test_invalid_ip(hass: HomeAssistantType) -> None: async def test_same_host(hass: HomeAssistantType) -> None: """Test abort in case of same host name.""" - unique_id = f"{USER_INPUT[CONF_HOST]}-{USER_INPUT[CONF_PORT]}" - mock_config_entry = MockConfigEntry( - domain=DOMAIN, unique_id=unique_id, data=USER_INPUT - ) - mock_config_entry.add_to_hass(hass) + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): + with patch( + "mcstatus.server.MinecraftServer.status", + return_value=PingResponse(STATUS_RESPONSE_RAW), + ): + unique_id = "mc.dummyserver.com-25565" + config_data = { + CONF_NAME: DEFAULT_NAME, + CONF_HOST: "mc.dummyserver.com", + CONF_PORT: DEFAULT_PORT, + } + mock_config_entry = MockConfigEntry( + domain=DOMAIN, unique_id=unique_id, data=config_data + ) + mock_config_entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT - ) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT + ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" async def test_port_too_small(hass: HomeAssistantType) -> None: """Test error in case of a too small port.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL - ) + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_SMALL + ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_port"} + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_port"} async def test_port_too_large(hass: HomeAssistantType) -> None: """Test error in case of a too large port.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE - ) + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_PORT_TOO_LARGE + ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_port"} + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "invalid_port"} async def test_connection_failed(hass: HomeAssistantType) -> None: """Test error in case of a failed connection.""" - with patch("mcstatus.server.MinecraftServer.ping", side_effect=OSError): - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT - ) + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): + with patch("mcstatus.server.MinecraftServer.status", side_effect=OSError): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT + ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_connection_succeeded_with_srv_record(hass: HomeAssistantType) -> None: + """Test config entry in case of a successful connection with a SRV record.""" + with patch( + "aiodns.DNSResolver.query", return_value=SRV_RECORDS, + ): + with patch( + "mcstatus.server.MinecraftServer.status", + return_value=PingResponse(STATUS_RESPONSE_RAW), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=USER_INPUT_SRV + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == USER_INPUT_SRV[CONF_HOST] + assert result["data"][CONF_NAME] == USER_INPUT_SRV[CONF_NAME] + assert result["data"][CONF_HOST] == USER_INPUT_SRV[CONF_HOST] async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with a host name.""" - with patch("mcstatus.server.MinecraftServer.ping", return_value=50): + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): with patch( "mcstatus.server.MinecraftServer.status", return_value=PingResponse(STATUS_RESPONSE_RAW), @@ -144,16 +202,17 @@ async def test_connection_succeeded_with_host(hass: HomeAssistantType) -> None: ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == f"{USER_INPUT[CONF_HOST]}:{USER_INPUT[CONF_PORT]}" + assert result["title"] == USER_INPUT[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT[CONF_NAME] - assert result["data"][CONF_HOST] == USER_INPUT[CONF_HOST] - assert result["data"][CONF_PORT] == USER_INPUT[CONF_PORT] + assert result["data"][CONF_HOST] == "mc.dummyserver.com" async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with an IPv4 address.""" with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"): - with patch("mcstatus.server.MinecraftServer.ping", return_value=50): + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): with patch( "mcstatus.server.MinecraftServer.status", return_value=PingResponse(STATUS_RESPONSE_RAW), @@ -163,19 +222,17 @@ async def test_connection_succeeded_with_ip4(hass: HomeAssistantType) -> None: ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert ( - result["title"] - == f"{USER_INPUT_IPV4[CONF_HOST]}:{USER_INPUT_IPV4[CONF_PORT]}" - ) + assert result["title"] == USER_INPUT_IPV4[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV4[CONF_NAME] - assert result["data"][CONF_HOST] == USER_INPUT_IPV4[CONF_HOST] - assert result["data"][CONF_PORT] == USER_INPUT_IPV4[CONF_PORT] + assert result["data"][CONF_HOST] == "1.1.1.1" async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None: """Test config entry in case of a successful connection with an IPv6 address.""" with patch("getmac.get_mac_address", return_value="01:23:45:67:89:ab"): - with patch("mcstatus.server.MinecraftServer.ping", return_value=50): + with patch( + "aiodns.DNSResolver.query", side_effect=aiodns.error.DNSError, + ): with patch( "mcstatus.server.MinecraftServer.status", return_value=PingResponse(STATUS_RESPONSE_RAW), @@ -185,10 +242,6 @@ async def test_connection_succeeded_with_ip6(hass: HomeAssistantType) -> None: ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert ( - result["title"] - == f"{USER_INPUT_IPV6[CONF_HOST]}:{USER_INPUT_IPV6[CONF_PORT]}" - ) + assert result["title"] == USER_INPUT_IPV6[CONF_HOST] assert result["data"][CONF_NAME] == USER_INPUT_IPV6[CONF_NAME] - assert result["data"][CONF_HOST] == USER_INPUT_IPV6[CONF_HOST] - assert result["data"][CONF_PORT] == USER_INPUT_IPV6[CONF_PORT] + assert result["data"][CONF_HOST] == "::ffff:0101:0101" From 6180f7bd645b197880893de446056beac07fe30a Mon Sep 17 00:00:00 2001 From: Oxan van Leeuwen Date: Tue, 24 Mar 2020 00:57:26 +0100 Subject: [PATCH 225/431] =?UTF-8?q?Fix=20Google=20Assistant=20temperature?= =?UTF-8?q?=20control=20for=20entity=20without=20mo=E2=80=A6=20(#33107)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../components/google_assistant/trait.py | 8 ++++++ .../components/google_assistant/test_trait.py | 26 +++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 9da319226fa..1c47be45651 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -647,6 +647,14 @@ class TemperatureSettingTrait(_Trait): elif domain == climate.DOMAIN: modes = self.climate_google_modes + + # Some integrations don't support modes (e.g. opentherm), but Google doesn't + # support changing the temperature if we don't have any modes. If there's + # only one Google doesn't support changing it, so the default mode here is + # only cosmetic. + if len(modes) == 0: + modes.append("heat") + if "off" in modes and any( mode in modes for mode in ("heatcool", "heat", "cool") ): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 232da039ea7..ec1848bf1ed 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -557,6 +557,32 @@ async def test_temperature_setting_climate_onoff(hass): assert len(calls) == 1 +async def test_temperature_setting_climate_no_modes(hass): + """Test TemperatureSetting trait support for climate domain not supporting any modes.""" + assert helpers.get_google_type(climate.DOMAIN, None) is not None + assert trait.TemperatureSettingTrait.supported(climate.DOMAIN, 0, None) + + hass.config.units.temperature_unit = TEMP_CELSIUS + + trt = trait.TemperatureSettingTrait( + hass, + State( + "climate.bla", + climate.HVAC_MODE_AUTO, + { + climate.ATTR_HVAC_MODES: [], + climate.ATTR_MIN_TEMP: None, + climate.ATTR_MAX_TEMP: None, + }, + ), + BASIC_CONFIG, + ) + assert trt.sync_attributes() == { + "availableThermostatModes": "heat", + "thermostatTemperatureUnit": "C", + } + + async def test_temperature_setting_climate_range(hass): """Test TemperatureSetting trait support for climate domain - range.""" assert helpers.get_google_type(climate.DOMAIN, None) is not None From 2d002f3ef6eefcad3032e6825f26def34c55a74a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 24 Mar 2020 01:20:39 +0100 Subject: [PATCH 226/431] Fix minut point updating frozen config entry data (#33148) * Fix minut point updating frozen config entry data * Update webhooks handling for configuration entry --- homeassistant/components/point/__init__.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/point/__init__.py b/homeassistant/components/point/__init__.py index 9abae9ab025..2817871cd7c 100644 --- a/homeassistant/components/point/__init__.py +++ b/homeassistant/components/point/__init__.py @@ -75,8 +75,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): def token_saver(token): _LOGGER.debug("Saving updated token") - entry.data[CONF_TOKEN] = token - hass.config_entries.async_update_entry(entry, data={**entry.data}) + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_TOKEN: token} + ) # Force token update. entry.data[CONF_TOKEN]["expires_in"] = -1 @@ -105,12 +106,18 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): async def async_setup_webhook(hass: HomeAssistantType, entry: ConfigEntry, session): """Set up a webhook to handle binary sensor events.""" if CONF_WEBHOOK_ID not in entry.data: - entry.data[CONF_WEBHOOK_ID] = hass.components.webhook.async_generate_id() - entry.data[CONF_WEBHOOK_URL] = hass.components.webhook.async_generate_url( - entry.data[CONF_WEBHOOK_ID] + webhook_id = hass.components.webhook.async_generate_id() + webhook_url = hass.components.webhook.async_generate_url(webhook_id) + _LOGGER.info("Registering new webhook at: %s", webhook_url) + + hass.config_entries.async_update_entry( + entry, + data={ + **entry.data, + CONF_WEBHOOK_ID: webhook_id, + CONF_WEBHOOK_URL: webhook_url, + }, ) - _LOGGER.info("Registering new webhook at: %s", entry.data[CONF_WEBHOOK_URL]) - hass.config_entries.async_update_entry(entry, data={**entry.data}) await hass.async_add_executor_job( session.update_webhook, entry.data[CONF_WEBHOOK_URL], From b50281a9173e7fb4a37b3f813ca92876088eaac3 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 24 Mar 2020 02:32:21 +0200 Subject: [PATCH 227/431] Options flow for Monoprice sources (#33156) * Options flow for monoprice sources * Fix lint errors --- .../components/monoprice/__init__.py | 7 ++ .../components/monoprice/config_flow.py | 98 ++++++++++++++----- .../components/monoprice/media_player.py | 17 +++- .../components/monoprice/strings.json | 15 +++ .../components/monoprice/test_config_flow.py | 35 ++++++- .../components/monoprice/test_media_player.py | 25 ++++- 6 files changed, 169 insertions(+), 28 deletions(-) diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index d18229e3d09..37593f6828e 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -33,6 +33,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.error("Error connecting to Monoprice controller at %s", port) raise ConfigEntryNotReady + entry.add_update_listener(_update_listener) + for component in PLATFORMS: hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, component) @@ -53,3 +55,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) return unload_ok + + +async def _update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index 45434fac131..cbabc65a54b 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -21,17 +21,31 @@ from .const import DOMAIN # pylint:disable=unused-import _LOGGER = logging.getLogger(__name__) -DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_PORT): str, - vol.Optional(CONF_SOURCE_1): str, - vol.Optional(CONF_SOURCE_2): str, - vol.Optional(CONF_SOURCE_3): str, - vol.Optional(CONF_SOURCE_4): str, - vol.Optional(CONF_SOURCE_5): str, - vol.Optional(CONF_SOURCE_6): str, +SOURCES = [ + CONF_SOURCE_1, + CONF_SOURCE_2, + CONF_SOURCE_3, + CONF_SOURCE_4, + CONF_SOURCE_5, + CONF_SOURCE_6, +] + +OPTIONS_FOR_DATA = {vol.Optional(source): str for source in SOURCES} + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_PORT): str, **OPTIONS_FOR_DATA}) + + +@core.callback +def _sources_from_config(data): + sources_config = { + str(idx + 1): data.get(source) for idx, source in enumerate(SOURCES) + } + + return { + index: name.strip() + for index, name in sources_config.items() + if (name is not None and name.strip() != "") } -) async def validate_input(hass: core.HomeAssistant, data): @@ -45,19 +59,8 @@ async def validate_input(hass: core.HomeAssistant, data): _LOGGER.error("Error connecting to Monoprice controller") raise CannotConnect - sources_config = { - 1: data.get(CONF_SOURCE_1), - 2: data.get(CONF_SOURCE_2), - 3: data.get(CONF_SOURCE_3), - 4: data.get(CONF_SOURCE_4), - 5: data.get(CONF_SOURCE_5), - 6: data.get(CONF_SOURCE_6), - } - sources = { - index: name.strip() - for index, name in sources_config.items() - if (name is not None and name.strip() != "") - } + sources = _sources_from_config(data) + # Return info that you want to store in the config entry. return {CONF_PORT: data[CONF_PORT], CONF_SOURCES: sources} @@ -86,6 +89,55 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=DATA_SCHEMA, errors=errors ) + @staticmethod + @core.callback + def async_get_options_flow(config_entry): + """Define the config flow to handle options.""" + return MonopriceOptionsFlowHandler(config_entry) + + +@core.callback +def _key_for_source(index, source, previous_sources): + if str(index) in previous_sources: + key = vol.Optional(source, default=previous_sources[str(index)]) + else: + key = vol.Optional(source) + + return key + + +class MonopriceOptionsFlowHandler(config_entries.OptionsFlow): + """Handle a Monoprice options flow.""" + + def __init__(self, config_entry): + """Initialize.""" + self.config_entry = config_entry + + @core.callback + def _previous_sources(self): + if CONF_SOURCES in self.config_entry.options: + previous = self.config_entry.options[CONF_SOURCES] + else: + previous = self.config_entry.data[CONF_SOURCES] + + return previous + + async def async_step_init(self, user_input=None): + """Manage the options.""" + if user_input is not None: + return self.async_create_entry( + title="", data={CONF_SOURCES: _sources_from_config(user_input)} + ) + + previous_sources = self._previous_sources() + + options = { + _key_for_source(idx + 1, source, previous_sources): str + for idx, source in enumerate(SOURCES) + } + + return self.async_show_form(step_id="init", data_schema=vol.Schema(options),) + class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 6e898cd6d4c..d85c219691e 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -1,6 +1,7 @@ """Support for interfacing with Monoprice 6 zone home audio controller.""" import logging +from homeassistant import core from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( SUPPORT_SELECT_SOURCE, @@ -27,7 +28,10 @@ SUPPORT_MONOPRICE = ( ) -def _get_sources(sources_config): +@core.callback +def _get_sources_from_dict(data): + sources_config = data[CONF_SOURCES] + source_id_name = {int(index): name for index, name in sources_config.items()} source_name_id = {v: k for k, v in source_id_name.items()} @@ -37,13 +41,22 @@ def _get_sources(sources_config): return [source_id_name, source_name_id, source_names] +@core.callback +def _get_sources(config_entry): + if CONF_SOURCES in config_entry.options: + data = config_entry.options + else: + data = config_entry.data + return _get_sources_from_dict(data) + + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Monoprice 6-zone amplifier platform.""" port = config_entry.data[CONF_PORT] monoprice = hass.data[DOMAIN][config_entry.entry_id] - sources = _get_sources(config_entry.data.get(CONF_SOURCES)) + sources = _get_sources(config_entry) entities = [] for i in range(1, 4): diff --git a/homeassistant/components/monoprice/strings.json b/homeassistant/components/monoprice/strings.json index d0f5badbeb0..32332c7369a 100644 --- a/homeassistant/components/monoprice/strings.json +++ b/homeassistant/components/monoprice/strings.json @@ -22,5 +22,20 @@ "abort": { "already_configured": "Device is already configured" } + }, + "options": { + "step": { + "init": { + "title": "Configure sources", + "data": { + "source_1": "Name of source #1", + "source_2": "Name of source #2", + "source_3": "Name of source #3", + "source_4": "Name of source #4", + "source_5": "Name of source #5", + "source_6": "Name of source #6" + } + } + } } } \ No newline at end of file diff --git a/tests/components/monoprice/test_config_flow.py b/tests/components/monoprice/test_config_flow.py index 234f7538f19..ecafa17e174 100644 --- a/tests/components/monoprice/test_config_flow.py +++ b/tests/components/monoprice/test_config_flow.py @@ -2,7 +2,7 @@ from asynctest import patch from serial import SerialException -from homeassistant import config_entries, setup +from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.monoprice.const import ( CONF_SOURCE_1, CONF_SOURCE_4, @@ -12,6 +12,8 @@ from homeassistant.components.monoprice.const import ( ) from homeassistant.const import CONF_PORT +from tests.common import MockConfigEntry + CONFIG = { CONF_PORT: "/test/port", CONF_SOURCE_1: "one", @@ -45,7 +47,7 @@ async def test_form(hass): assert result2["title"] == CONFIG[CONF_PORT] assert result2["data"] == { CONF_PORT: CONFIG[CONF_PORT], - CONF_SOURCES: {1: CONFIG[CONF_SOURCE_1], 4: CONFIG[CONF_SOURCE_4]}, + CONF_SOURCES: {"1": CONFIG[CONF_SOURCE_1], "4": CONFIG[CONF_SOURCE_4]}, } await hass.async_block_till_done() assert len(mock_setup.mock_calls) == 1 @@ -86,3 +88,32 @@ async def test_generic_exception(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "unknown"} + + +async def test_options_flow(hass): + """Test config flow options.""" + conf = {CONF_PORT: "/test/port", CONF_SOURCES: {"4": "four"}} + + config_entry = MockConfigEntry( + domain=DOMAIN, + # unique_id="abcde12345", + data=conf, + # options={CONF_SHOW_ON_MAP: True}, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.monoprice.async_setup_entry", return_value=True + ): + 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_SOURCE_1: "one", CONF_SOURCE_4: "", CONF_SOURCE_5: "five"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert config_entry.options[CONF_SOURCES] == {"1": "one", "5": "five"} diff --git a/tests/components/monoprice/test_media_player.py b/tests/components/monoprice/test_media_player.py index 2aad854652b..3778f2af04b 100644 --- a/tests/components/monoprice/test_media_player.py +++ b/tests/components/monoprice/test_media_player.py @@ -37,6 +37,7 @@ from homeassistant.helpers.entity_component import async_update_entity from tests.common import MockConfigEntry MOCK_CONFIG = {CONF_PORT: "fake port", CONF_SOURCES: {"1": "one", "3": "three"}} +MOCK_OPTIONS = {CONF_SOURCES: {"2": "two", "4": "four"}} ZONE_1_ID = "media_player.zone_11" ZONE_2_ID = "media_player.zone_12" @@ -117,6 +118,20 @@ async def _setup_monoprice(hass, monoprice): await hass.async_block_till_done() +async def _setup_monoprice_with_options(hass, monoprice): + with patch( + "homeassistant.components.monoprice.get_monoprice", new=lambda *a: monoprice, + ): + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONFIG, options=MOCK_OPTIONS + ) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + # setup_component(self.hass, DOMAIN, MOCK_CONFIG) + # self.hass.async_block_till_done() + await hass.async_block_till_done() + + async def _call_media_player_service(hass, name, data): await hass.services.async_call( MEDIA_PLAYER_DOMAIN, name, service_data=data, blocking=True @@ -256,7 +271,6 @@ async def test_restore_without_snapshort(hass): async def test_update(hass): """Test updating values from monoprice.""" - """Test snapshot save/restore service calls.""" monoprice = MockMonoprice() await _setup_monoprice(hass, monoprice) @@ -305,6 +319,15 @@ async def test_source_list(hass): assert ["one", "three"] == state.attributes[ATTR_INPUT_SOURCE_LIST] +async def test_source_list_with_options(hass): + """Test source list property.""" + await _setup_monoprice_with_options(hass, MockMonoprice()) + + state = hass.states.get(ZONE_1_ID) + # Note, the list is sorted! + assert ["two", "four"] == state.attributes[ATTR_INPUT_SOURCE_LIST] + + async def test_select_source(hass): """Test source selection methods.""" monoprice = MockMonoprice() From 3c59791b2e74e214dfc85146471328c7f0b61742 Mon Sep 17 00:00:00 2001 From: MeIchthys <10717998+meichthys@users.noreply.github.com> Date: Tue, 24 Mar 2020 06:11:35 -0400 Subject: [PATCH 228/431] Add Nextcloud Integration (#30871) * some sensors working in homeassistant * bring up to date * add codeowner * update requirements * overhaul data imports from api & sensor discovery * remove print statement * delete requirements_test_all * add requrements_test_all.txt * Update homeassistant/components/nextcloud/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Update homeassistant/components/nextcloud/sensor.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * describe recursive function * clarify that dict is returned * remove requirements from requirements_test_all * improve and simplify sensor naming * add basic tests * restore pre-commit config * update requirements_test_all * remove codespell requirement * update pre-commit-config * add-back codespell * rename class variables as suggested by @springstan * add dev branch to no-commit-to-branch git hook Because my fork had the same 'dev' branch i wasn't able to push. Going forward I should probably name my branches differently. * move config logic to __init__.py * restore .pre-commit-config.yaml * remove tests * remove nextcloud test requirement * remove debugging code * implement binary sensors * restore .pre-commit-config.yaml * bump dependency version * bump requirements files * bump nextcloud reqirement to latest * update possible exceptions, use fstrings * add list of sensors & fix inconsistency in get_data_points * use domain for config * fix guard clause * repair pre-commit-config * Remove period from logging * include url in unique_id * update requirements_all.txt Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .coveragerc | 1 + CODEOWNERS | 1 + .../components/nextcloud/__init__.py | 147 ++++++++++++++++++ .../components/nextcloud/binary_sensor.py | 52 +++++++ .../components/nextcloud/manifest.json | 12 ++ homeassistant/components/nextcloud/sensor.py | 52 +++++++ requirements_all.txt | 3 + 7 files changed, 268 insertions(+) create mode 100644 homeassistant/components/nextcloud/__init__.py create mode 100644 homeassistant/components/nextcloud/binary_sensor.py create mode 100644 homeassistant/components/nextcloud/manifest.json create mode 100644 homeassistant/components/nextcloud/sensor.py diff --git a/.coveragerc b/.coveragerc index cc04fb03456..a218c812df4 100644 --- a/.coveragerc +++ b/.coveragerc @@ -468,6 +468,7 @@ omit = homeassistant/components/netgear_lte/* homeassistant/components/netio/switch.py homeassistant/components/neurio_energy/sensor.py + homeassistant/components/nextcloud/* homeassistant/components/nfandroidtv/notify.py homeassistant/components/niko_home_control/light.py homeassistant/components/nilu/air_quality.py diff --git a/CODEOWNERS b/CODEOWNERS index 86a36551f57..e425a4d2d1c 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -247,6 +247,7 @@ homeassistant/components/netatmo/* @cgtobi homeassistant/components/netdata/* @fabaff homeassistant/components/nexia/* @ryannazaretian @bdraco homeassistant/components/nextbus/* @vividboarder +homeassistant/components/nextcloud/* @meichthys homeassistant/components/nilu/* @hfurubotten homeassistant/components/nissan_leaf/* @filcole homeassistant/components/nmbs/* @thibmaek diff --git a/homeassistant/components/nextcloud/__init__.py b/homeassistant/components/nextcloud/__init__.py new file mode 100644 index 00000000000..39eb16ec265 --- /dev/null +++ b/homeassistant/components/nextcloud/__init__.py @@ -0,0 +1,147 @@ +"""The Nextcloud integration.""" +from datetime import timedelta +import logging + +from nextcloudmonitor import NextcloudMonitor, NextcloudMonitorError +import voluptuous as vol + +from homeassistant.const import ( + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_URL, + CONF_USERNAME, +) +from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.helpers.event import track_time_interval + +_LOGGER = logging.getLogger(__name__) + +DOMAIN = "nextcloud" +NEXTCLOUD_COMPONENTS = ("sensor", "binary_sensor") +SCAN_INTERVAL = timedelta(seconds=60) + +# Validate user configuration +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_URL): cv.url, + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=SCAN_INTERVAL): cv.time_period, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) +BINARY_SENSORS = ( + "nextcloud_system_enable_avatars", + "nextcloud_system_enable_previews", + "nextcloud_system_filelocking.enabled", + "nextcloud_system_debug", +) + +SENSORS = ( + "nextcloud_system_version", + "nextcloud_system_theme", + "nextcloud_system_memcache.local", + "nextcloud_system_memcache.distributed", + "nextcloud_system_memcache.locking", + "nextcloud_system_freespace", + "nextcloud_system_cpuload", + "nextcloud_system_mem_total", + "nextcloud_system_mem_free", + "nextcloud_system_swap_total", + "nextcloud_system_swap_free", + "nextcloud_system_apps_num_installed", + "nextcloud_system_apps_num_updates_available", + "nextcloud_system_apps_app_updates_calendar", + "nextcloud_system_apps_app_updates_contacts", + "nextcloud_system_apps_app_updates_tasks", + "nextcloud_system_apps_app_updates_twofactor_totp", + "nextcloud_storage_num_users", + "nextcloud_storage_num_files", + "nextcloud_storage_num_storages", + "nextcloud_storage_num_storages_local", + "nextcloud_storage_num_storage_home", + "nextcloud_storage_num_storages_other", + "nextcloud_shares_num_shares", + "nextcloud_shares_num_shares_user", + "nextcloud_shares_num_shares_groups", + "nextcloud_shares_num_shares_link", + "nextcloud_shares_num_shares_mail", + "nextcloud_shares_num_shares_room", + "nextcloud_shares_num_shares_link_no_password", + "nextcloud_shares_num_fed_shares_sent", + "nextcloud_shares_num_fed_shares_received", + "nextcloud_shares_permissions_3_1", + "nextcloud_server_webserver", + "nextcloud_server_php_version", + "nextcloud_server_php_memory_limit", + "nextcloud_server_php_max_execution_time", + "nextcloud_server_php_upload_max_filesize", + "nextcloud_database_type", + "nextcloud_database_version", + "nextcloud_database_version", + "nextcloud_activeusers_last5minutes", + "nextcloud_activeusers_last1hour", + "nextcloud_activeusers_last24hours", +) + + +def setup(hass, config): + """Set up the Nextcloud integration.""" + # Fetch Nextcloud Monitor api data + conf = config[DOMAIN] + + try: + ncm = NextcloudMonitor(conf[CONF_URL], conf[CONF_USERNAME], conf[CONF_PASSWORD]) + except NextcloudMonitorError: + _LOGGER.error("Nextcloud setup failed - Check configuration") + + hass.data[DOMAIN] = get_data_points(ncm.data) + hass.data[DOMAIN]["instance"] = conf[CONF_URL] + + def nextcloud_update(event_time): + """Update data from nextcloud api.""" + try: + ncm.update() + except NextcloudMonitorError: + _LOGGER.error("Nextcloud update failed") + return False + + hass.data[DOMAIN] = get_data_points(ncm.data) + + # Update sensors on time interval + track_time_interval(hass, nextcloud_update, conf[CONF_SCAN_INTERVAL]) + + for component in NEXTCLOUD_COMPONENTS: + discovery.load_platform(hass, component, DOMAIN, {}, config) + + return True + + +# Use recursion to create list of sensors & values based on nextcloud api data +def get_data_points(api_data, key_path="", leaf=False): + """Use Recursion to discover data-points and values. + + Get dictionary of data-points by recursing through dict returned by api until + the dictionary value does not contain another dictionary and use the + resulting path of dictionary keys and resulting value as the name/value + for the data-point. + + returns: dictionary of data-point/values + """ + result = {} + for key, value in api_data.items(): + if isinstance(value, dict): + if leaf: + key_path = f"{key}_" + if not leaf: + key_path += f"{key}_" + leaf = True + result.update(get_data_points(value, key_path, leaf)) + else: + result[f"{DOMAIN}_{key_path}{key}"] = value + leaf = False + return result diff --git a/homeassistant/components/nextcloud/binary_sensor.py b/homeassistant/components/nextcloud/binary_sensor.py new file mode 100644 index 00000000000..9e4c6f5d969 --- /dev/null +++ b/homeassistant/components/nextcloud/binary_sensor.py @@ -0,0 +1,52 @@ +"""Summary binary data from Nextcoud.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorDevice + +from . import BINARY_SENSORS, DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nextcloud sensors.""" + if discovery_info is None: + return + binary_sensors = [] + for name in hass.data[DOMAIN]: + if name in BINARY_SENSORS: + binary_sensors.append(NextcloudBinarySensor(name)) + add_entities(binary_sensors, True) + + +class NextcloudBinarySensor(BinarySensorDevice): + """Represents a Nextcloud binary sensor.""" + + def __init__(self, item): + """Initialize the Nextcloud binary sensor.""" + self._name = item + self._is_on = None + + @property + def icon(self): + """Return the icon for this binary sensor.""" + return "mdi:cloud" + + @property + def name(self): + """Return the name for this binary sensor.""" + return self._name + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return self._is_on == "yes" + + @property + def unique_id(self): + """Return the unique ID for this binary sensor.""" + return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" + + def update(self): + """Update the binary sensor.""" + self._is_on = self.hass.data[DOMAIN][self._name] diff --git a/homeassistant/components/nextcloud/manifest.json b/homeassistant/components/nextcloud/manifest.json new file mode 100644 index 00000000000..4db0019920d --- /dev/null +++ b/homeassistant/components/nextcloud/manifest.json @@ -0,0 +1,12 @@ +{ + "domain": "nextcloud", + "name": "Nextcloud", + "documentation": "https://www.home-assistant.io/integrations/nextcloud", + "requirements": [ + "nextcloudmonitor==1.1.0" + ], + "dependencies": [], + "codeowners": [ + "@meichthys" + ] +} \ No newline at end of file diff --git a/homeassistant/components/nextcloud/sensor.py b/homeassistant/components/nextcloud/sensor.py new file mode 100644 index 00000000000..aacd33ec3e8 --- /dev/null +++ b/homeassistant/components/nextcloud/sensor.py @@ -0,0 +1,52 @@ +"""Summary data from Nextcoud.""" +import logging + +from homeassistant.helpers.entity import Entity + +from . import DOMAIN, SENSORS + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Nextcloud sensors.""" + if discovery_info is None: + return + sensors = [] + for name in hass.data[DOMAIN]: + if name in SENSORS: + sensors.append(NextcloudSensor(name)) + add_entities(sensors, True) + + +class NextcloudSensor(Entity): + """Represents a Nextcloud sensor.""" + + def __init__(self, item): + """Initialize the Nextcloud sensor.""" + self._name = item + self._state = None + + @property + def icon(self): + """Return the icon for this sensor.""" + return "mdi:cloud" + + @property + def name(self): + """Return the name for this sensor.""" + return self._name + + @property + def state(self): + """Return the state for this sensor.""" + return self._state + + @property + def unique_id(self): + """Return the unique ID for this sensor.""" + return f"{self.hass.data[DOMAIN]['instance']}#{self._name}" + + def update(self): + """Update the sensor.""" + self._state = self.hass.data[DOMAIN][self._name] diff --git a/requirements_all.txt b/requirements_all.txt index 00a7a0a0073..141048c48b9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -924,6 +924,9 @@ neurio==0.3.1 # homeassistant.components.nexia nexia==0.7.1 +# homeassistant.components.nextcloud +nextcloudmonitor==1.1.0 + # homeassistant.components.niko_home_control niko-home-control==0.2.1 From e616ab5fb23dfb15006d2dd876cf23575c982ec8 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Tue, 24 Mar 2020 12:04:33 +0100 Subject: [PATCH 229/431] Bump OZW fork to 0.1.10 (#33205) --- homeassistant/components/zwave/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/manifest.json b/homeassistant/components/zwave/manifest.json index 81978aa96cd..72d61b278dd 100644 --- a/homeassistant/components/zwave/manifest.json +++ b/homeassistant/components/zwave/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave", - "requirements": ["homeassistant-pyozw==0.1.9", "pydispatcher==2.0.5"], + "requirements": ["homeassistant-pyozw==0.1.10", "pydispatcher==2.0.5"], "dependencies": [], "codeowners": ["@home-assistant/z-wave"] } diff --git a/requirements_all.txt b/requirements_all.txt index 141048c48b9..a79db47667c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -707,7 +707,7 @@ holidays==0.10.1 home-assistant-frontend==20200318.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.9 +homeassistant-pyozw==0.1.10 # homeassistant.components.homematicip_cloud homematicip==0.10.17 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 231dfcd0cc7..e0aa92d046d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ holidays==0.10.1 home-assistant-frontend==20200318.1 # homeassistant.components.zwave -homeassistant-pyozw==0.1.9 +homeassistant-pyozw==0.1.10 # homeassistant.components.homematicip_cloud homematicip==0.10.17 From 9226589bcd5eba77d4dee6dbed7c73806798e8d8 Mon Sep 17 00:00:00 2001 From: Oliver Date: Tue, 24 Mar 2020 13:03:22 +0100 Subject: [PATCH 230/431] Bump to version 0.8.1 of denonavr (#33169) * Bump to version 0.8.1 of denonavr * Update CODEWONERS file --- CODEOWNERS | 1 + homeassistant/components/denonavr/manifest.json | 4 ++-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index e425a4d2d1c..1fda4d6f44b 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -82,6 +82,7 @@ homeassistant/components/darksky/* @fabaff homeassistant/components/deconz/* @kane610 homeassistant/components/delijn/* @bollewolle homeassistant/components/demo/* @home-assistant/core +homeassistant/components/denonavr/* @scarface-4711 @starkillerOG homeassistant/components/derivative/* @afaucogney homeassistant/components/device_automation/* @home-assistant/core homeassistant/components/digital_ocean/* @fabaff diff --git a/homeassistant/components/denonavr/manifest.json b/homeassistant/components/denonavr/manifest.json index d13ae1d7701..7e06e781563 100644 --- a/homeassistant/components/denonavr/manifest.json +++ b/homeassistant/components/denonavr/manifest.json @@ -2,7 +2,7 @@ "domain": "denonavr", "name": "Denon AVR Network Receivers", "documentation": "https://www.home-assistant.io/integrations/denonavr", - "requirements": ["denonavr==0.8.0"], + "requirements": ["denonavr==0.8.1"], "dependencies": [], - "codeowners": [] + "codeowners": ["@scarface-4711", "@starkillerOG"] } diff --git a/requirements_all.txt b/requirements_all.txt index a79db47667c..edcae2e596c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -444,7 +444,7 @@ defusedxml==0.6.0 deluge-client==1.7.1 # homeassistant.components.denonavr -denonavr==0.8.0 +denonavr==0.8.1 # homeassistant.components.directv directpy==0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0aa92d046d..c69f2799c26 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -175,7 +175,7 @@ datadog==0.15.0 defusedxml==0.6.0 # homeassistant.components.denonavr -denonavr==0.8.0 +denonavr==0.8.1 # homeassistant.components.directv directpy==0.7 From d979648c01ff9242d4d4861731747b9ae0ffe0c7 Mon Sep 17 00:00:00 2001 From: Josef Schlehofer Date: Tue, 24 Mar 2020 13:04:16 +0100 Subject: [PATCH 231/431] Upgrade youtube_dl to version 2020.03.24 (#33202) --- homeassistant/components/media_extractor/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index 631dc7675ca..fd1d2172873 100644 --- a/homeassistant/components/media_extractor/manifest.json +++ b/homeassistant/components/media_extractor/manifest.json @@ -2,7 +2,7 @@ "domain": "media_extractor", "name": "Media Extractor", "documentation": "https://www.home-assistant.io/integrations/media_extractor", - "requirements": ["youtube_dl==2020.03.08"], + "requirements": ["youtube_dl==2020.03.24"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/requirements_all.txt b/requirements_all.txt index edcae2e596c..d40e646ef54 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2161,7 +2161,7 @@ yeelight==0.5.1 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.03.08 +youtube_dl==2020.03.24 # homeassistant.components.zengge zengge==0.2 From f150c9c65c4d0a574d898f61d95ec4b62c07ab88 Mon Sep 17 00:00:00 2001 From: Diogo Gomes Date: Tue, 24 Mar 2020 12:17:31 +0000 Subject: [PATCH 232/431] Increase timeout setting up IPMA (#33194) --- homeassistant/components/ipma/weather.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/ipma/weather.py b/homeassistant/components/ipma/weather.py index 1fce3922b58..62f1b0b39af 100644 --- a/homeassistant/components/ipma/weather.py +++ b/homeassistant/components/ipma/weather.py @@ -104,7 +104,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(10): + with async_timeout.timeout(30): location = await Location.get(api, float(latitude), float(longitude)) _LOGGER.debug( From 763ed0dc7b0fc75941a3edd69e8b607d8a12127b Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Tue, 24 Mar 2020 13:18:47 +0000 Subject: [PATCH 233/431] [ci skip] Translation update --- .../airvisual/.translations/ca.json | 11 +++ .../airvisual/.translations/de.json | 15 ++++- .../airvisual/.translations/en.json | 3 +- .../airvisual/.translations/es.json | 11 +++ .../airvisual/.translations/fr.json | 31 +++++++++ .../airvisual/.translations/it.json | 11 +++ .../airvisual/.translations/ko.json | 34 ++++++++++ .../airvisual/.translations/no.json | 11 +++ .../airvisual/.translations/pl.json | 34 ++++++++++ .../airvisual/.translations/ru.json | 11 +++ .../airvisual/.translations/sk.json | 12 ++++ .../airvisual/.translations/sl.json | 34 ++++++++++ .../airvisual/.translations/zh-Hant.json | 11 +++ .../alarm_control_panel/.translations/ca.json | 13 +++- .../alarm_control_panel/.translations/de.json | 7 ++ .../alarm_control_panel/.translations/en.json | 7 ++ .../alarm_control_panel/.translations/es.json | 7 ++ .../alarm_control_panel/.translations/fr.json | 7 ++ .../alarm_control_panel/.translations/it.json | 15 +++-- .../alarm_control_panel/.translations/ko.json | 7 ++ .../alarm_control_panel/.translations/lb.json | 7 ++ .../alarm_control_panel/.translations/no.json | 7 ++ .../alarm_control_panel/.translations/pl.json | 7 ++ .../alarm_control_panel/.translations/ru.json | 7 ++ .../alarm_control_panel/.translations/sl.json | 7 ++ .../.translations/zh-Hant.json | 7 ++ .../ambient_station/.translations/sl.json | 3 + .../components/august/.translations/de.json | 1 + .../components/august/.translations/fr.json | 22 ++++++ .../components/august/.translations/ko.json | 32 +++++++++ .../components/august/.translations/pl.json | 27 ++++++++ .../components/august/.translations/sl.json | 32 +++++++++ .../components/axis/.translations/sl.json | 2 +- .../binary_sensor/.translations/ko.json | 8 +-- .../cert_expiry/.translations/ca.json | 5 +- .../cert_expiry/.translations/de.json | 5 +- .../cert_expiry/.translations/fr.json | 5 +- .../cert_expiry/.translations/it.json | 5 +- .../cert_expiry/.translations/ko.json | 5 +- .../cert_expiry/.translations/pl.json | 5 +- .../cert_expiry/.translations/sl.json | 5 +- .../coronavirus/.translations/ca.json | 16 +++++ .../coronavirus/.translations/da.json | 16 +++++ .../coronavirus/.translations/de.json | 16 +++++ .../coronavirus/.translations/fr.json | 16 +++++ .../coronavirus/.translations/it.json | 16 +++++ .../coronavirus/.translations/ko.json | 16 +++++ .../coronavirus/.translations/pl.json | 16 +++++ .../coronavirus/.translations/sl.json | 16 +++++ .../components/cover/.translations/ca.json | 2 + .../components/cover/.translations/da.json | 8 +++ .../components/cover/.translations/de.json | 4 ++ .../components/cover/.translations/fr.json | 3 + .../components/cover/.translations/ko.json | 8 +++ .../components/cover/.translations/pl.json | 8 +++ .../components/cover/.translations/sl.json | 8 +++ .../components/deconz/.translations/fr.json | 12 ++-- .../components/deconz/.translations/sl.json | 3 +- .../components/demo/.translations/de.json | 6 ++ .../components/demo/.translations/es.json | 6 ++ .../components/demo/.translations/it.json | 2 +- .../components/directv/.translations/ca.json | 26 +++++++ .../components/directv/.translations/de.json | 28 ++++++++ .../components/directv/.translations/en.json | 6 +- .../components/directv/.translations/es.json | 26 +++++++ .../components/directv/.translations/fr.json | 26 +++++++ .../components/directv/.translations/it.json | 30 +++++++++ .../components/directv/.translations/ko.json | 26 +++++++ .../components/directv/.translations/lb.json | 8 +++ .../components/directv/.translations/no.json | 26 +++++++ .../components/directv/.translations/pl.json | 31 +++++++++ .../components/directv/.translations/ru.json | 26 +++++++ .../components/directv/.translations/sl.json | 32 +++++++++ .../directv/.translations/zh-Hant.json | 26 +++++++ .../components/doorbird/.translations/ca.json | 34 ++++++++++ .../components/doorbird/.translations/da.json | 12 ++++ .../components/doorbird/.translations/en.json | 64 +++++++++--------- .../components/doorbird/.translations/es.json | 34 ++++++++++ .../components/doorbird/.translations/fr.json | 24 +++++++ .../components/doorbird/.translations/ko.json | 34 ++++++++++ .../components/doorbird/.translations/lb.json | 33 +++++++++ .../components/doorbird/.translations/no.json | 34 ++++++++++ .../components/doorbird/.translations/ru.json | 34 ++++++++++ .../doorbird/.translations/zh-Hant.json | 34 ++++++++++ .../components/freebox/.translations/ca.json | 26 +++++++ .../components/freebox/.translations/de.json | 25 +++++++ .../components/freebox/.translations/es.json | 26 +++++++ .../components/freebox/.translations/fr.json | 25 +++++++ .../components/freebox/.translations/it.json | 26 +++++++ .../components/freebox/.translations/ko.json | 26 +++++++ .../components/freebox/.translations/no.json | 26 +++++++ .../components/freebox/.translations/pl.json | 26 +++++++ .../components/freebox/.translations/ru.json | 26 +++++++ .../components/freebox/.translations/sl.json | 26 +++++++ .../freebox/.translations/zh-Hant.json | 26 +++++++ .../garmin_connect/.translations/ko.json | 4 +- .../geonetnz_quakes/.translations/ca.json | 3 + .../geonetnz_quakes/.translations/de.json | 3 + .../geonetnz_quakes/.translations/en.json | 3 + .../geonetnz_quakes/.translations/es.json | 3 + .../geonetnz_quakes/.translations/fr.json | 3 + .../geonetnz_quakes/.translations/it.json | 3 + .../geonetnz_quakes/.translations/ko.json | 3 + .../geonetnz_quakes/.translations/lb.json | 3 + .../geonetnz_quakes/.translations/no.json | 3 + .../geonetnz_quakes/.translations/pl.json | 3 + .../geonetnz_quakes/.translations/ru.json | 3 + .../geonetnz_quakes/.translations/sl.json | 3 + .../.translations/zh-Hant.json | 3 + .../components/griddy/.translations/ca.json | 21 ++++++ .../components/griddy/.translations/da.json | 5 ++ .../components/griddy/.translations/de.json | 17 +++++ .../components/griddy/.translations/en.json | 40 +++++------ .../components/griddy/.translations/es.json | 21 ++++++ .../components/griddy/.translations/fr.json | 19 ++++++ .../components/griddy/.translations/it.json | 21 ++++++ .../components/griddy/.translations/ko.json | 21 ++++++ .../components/griddy/.translations/lb.json | 9 +++ .../components/griddy/.translations/no.json | 21 ++++++ .../components/griddy/.translations/pl.json | 21 ++++++ .../components/griddy/.translations/ru.json | 21 ++++++ .../components/griddy/.translations/sl.json | 21 ++++++ .../components/griddy/.translations/tr.json | 8 +++ .../griddy/.translations/zh-Hant.json | 21 ++++++ .../components/harmony/.translations/ca.json | 38 +++++++++++ .../components/harmony/.translations/de.json | 38 +++++++++++ .../components/harmony/.translations/en.json | 67 ++++++++++--------- .../components/harmony/.translations/es.json | 38 +++++++++++ .../components/harmony/.translations/fr.json | 38 +++++++++++ .../components/harmony/.translations/it.json | 38 +++++++++++ .../components/harmony/.translations/ko.json | 38 +++++++++++ .../components/harmony/.translations/lb.json | 36 ++++++++++ .../components/harmony/.translations/no.json | 38 +++++++++++ .../components/harmony/.translations/ru.json | 38 +++++++++++ .../harmony/.translations/zh-Hant.json | 38 +++++++++++ .../components/heos/.translations/ko.json | 2 +- .../homekit_controller/.translations/sl.json | 4 +- .../components/hue/.translations/ko.json | 2 +- .../iaqualink/.translations/ko.json | 2 +- .../components/icloud/.translations/ca.json | 6 +- .../components/icloud/.translations/da.json | 6 +- .../components/icloud/.translations/de.json | 6 +- .../components/icloud/.translations/es.json | 6 +- .../components/icloud/.translations/fr.json | 3 +- .../components/icloud/.translations/it.json | 6 +- .../components/icloud/.translations/ko.json | 6 +- .../components/icloud/.translations/no.json | 6 +- .../components/icloud/.translations/pl.json | 6 +- .../components/icloud/.translations/ru.json | 6 +- .../components/icloud/.translations/sl.json | 6 +- .../components/icloud/.translations/tr.json | 7 ++ .../icloud/.translations/zh-Hant.json | 6 +- .../konnected/.translations/de.json | 12 +++- .../konnected/.translations/es.json | 2 +- .../konnected/.translations/it.json | 2 +- .../konnected/.translations/ko.json | 2 +- .../konnected/.translations/sl.json | 6 +- .../components/light/.translations/da.json | 2 + .../components/light/.translations/de.json | 2 + .../components/light/.translations/ko.json | 2 + .../components/light/.translations/pl.json | 2 + .../components/light/.translations/sl.json | 2 + .../media_player/.translations/ko.json | 2 +- .../components/melcloud/.translations/ko.json | 2 +- .../monoprice/.translations/ca.json | 26 +++++++ .../monoprice/.translations/en.json | 41 ++++++++++++ .../monoprice/.translations/es.json | 26 +++++++ .../monoprice/.translations/fr.json | 24 +++++++ .../monoprice/.translations/ko.json | 41 ++++++++++++ .../monoprice/.translations/lb.json | 25 +++++++ .../monoprice/.translations/no.json | 41 ++++++++++++ .../monoprice/.translations/ru.json | 41 ++++++++++++ .../monoprice/.translations/zh-Hant.json | 26 +++++++ .../components/mqtt/.translations/sl.json | 22 ++++++ .../components/myq/.translations/ca.json | 22 ++++++ .../components/myq/.translations/da.json | 12 ++++ .../components/myq/.translations/de.json | 22 ++++++ .../components/myq/.translations/en.json | 38 +++++------ .../components/myq/.translations/es.json | 22 ++++++ .../components/myq/.translations/fr.json | 22 ++++++ .../components/myq/.translations/ko.json | 22 ++++++ .../components/myq/.translations/lb.json | 22 ++++++ .../components/myq/.translations/no.json | 22 ++++++ .../components/myq/.translations/ru.json | 22 ++++++ .../components/myq/.translations/zh-Hant.json | 22 ++++++ .../components/nexia/.translations/ca.json | 22 ++++++ .../components/nexia/.translations/da.json | 12 ++++ .../components/nexia/.translations/de.json | 19 ++++++ .../components/nexia/.translations/en.json | 38 +++++------ .../components/nexia/.translations/es.json | 22 ++++++ .../components/nexia/.translations/fr.json | 22 ++++++ .../components/nexia/.translations/ko.json | 22 ++++++ .../components/nexia/.translations/lb.json | 22 ++++++ .../components/nexia/.translations/no.json | 22 ++++++ .../components/nexia/.translations/ru.json | 22 ++++++ .../nexia/.translations/zh-Hant.json | 22 ++++++ .../components/notion/.translations/sl.json | 3 + .../components/nuheat/.translations/ca.json | 25 +++++++ .../components/nuheat/.translations/da.json | 12 ++++ .../components/nuheat/.translations/en.json | 46 ++++++------- .../components/nuheat/.translations/es.json | 25 +++++++ .../components/nuheat/.translations/fr.json | 24 +++++++ .../components/nuheat/.translations/ko.json | 25 +++++++ .../components/nuheat/.translations/lb.json | 24 +++++++ .../components/nuheat/.translations/no.json | 25 +++++++ .../components/nuheat/.translations/ru.json | 25 +++++++ .../nuheat/.translations/zh-Hant.json | 25 +++++++ .../components/plex/.translations/sl.json | 4 +- .../powerwall/.translations/ca.json | 20 ++++++ .../powerwall/.translations/de.json | 20 ++++++ .../powerwall/.translations/en.json | 20 ++++++ .../powerwall/.translations/es.json | 20 ++++++ .../powerwall/.translations/fr.json | 20 ++++++ .../powerwall/.translations/it.json | 20 ++++++ .../powerwall/.translations/ko.json | 20 ++++++ .../powerwall/.translations/lb.json | 20 ++++++ .../powerwall/.translations/no.json | 20 ++++++ .../powerwall/.translations/ru.json | 20 ++++++ .../powerwall/.translations/zh-Hant.json | 20 ++++++ .../pvpc_hourly_pricing/.translations/ca.json | 18 +++++ .../pvpc_hourly_pricing/.translations/es.json | 6 +- .../pvpc_hourly_pricing/.translations/fr.json | 14 ++++ .../pvpc_hourly_pricing/.translations/lb.json | 12 ++++ .../pvpc_hourly_pricing/.translations/no.json | 18 +++++ .../pvpc_hourly_pricing/.translations/ru.json | 18 +++++ .../.translations/zh-Hant.json | 18 +++++ .../components/rachio/.translations/ca.json | 31 +++++++++ .../components/rachio/.translations/de.json | 21 ++++++ .../components/rachio/.translations/en.json | 54 +++++++-------- .../components/rachio/.translations/es.json | 31 +++++++++ .../components/rachio/.translations/fr.json | 31 +++++++++ .../components/rachio/.translations/it.json | 31 +++++++++ .../components/rachio/.translations/ko.json | 31 +++++++++ .../components/rachio/.translations/lb.json | 10 +++ .../components/rachio/.translations/no.json | 31 +++++++++ .../components/rachio/.translations/ru.json | 31 +++++++++ .../components/rachio/.translations/sl.json | 31 +++++++++ .../rachio/.translations/zh-Hant.json | 31 +++++++++ .../rainmachine/.translations/sl.json | 3 + .../rainmachine/.translations/sv.json | 3 + .../components/ring/.translations/es.json | 4 +- .../components/roku/.translations/ca.json | 27 ++++++++ .../components/roku/.translations/cs.json | 7 ++ .../components/roku/.translations/de.json | 31 +++++++++ .../components/roku/.translations/en.json | 8 +-- .../components/roku/.translations/es.json | 27 ++++++++ .../components/roku/.translations/fr.json | 27 ++++++++ .../components/roku/.translations/it.json | 31 +++++++++ .../components/roku/.translations/ko.json | 27 ++++++++ .../components/roku/.translations/lb.json | 26 +++++++ .../components/roku/.translations/no.json | 27 ++++++++ .../components/roku/.translations/pl.json | 32 +++++++++ .../components/roku/.translations/ru.json | 27 ++++++++ .../components/roku/.translations/sl.json | 33 +++++++++ .../roku/.translations/zh-Hant.json | 27 ++++++++ .../samsungtv/.translations/de.json | 2 +- .../components/sense/.translations/da.json | 11 +++ .../components/sense/.translations/fr.json | 21 ++++++ .../components/sense/.translations/ko.json | 22 ++++++ .../components/sense/.translations/pl.json | 19 ++++++ .../components/sense/.translations/sl.json | 22 ++++++ .../shopping_list/.translations/ca.json | 14 ++++ .../shopping_list/.translations/de.json | 14 ++++ .../shopping_list/.translations/es.json | 14 ++++ .../shopping_list/.translations/fr.json | 14 ++++ .../shopping_list/.translations/it.json | 14 ++++ .../shopping_list/.translations/ko.json | 14 ++++ .../shopping_list/.translations/lb.json | 14 ++++ .../shopping_list/.translations/no.json | 14 ++++ .../shopping_list/.translations/pl.json | 14 ++++ .../shopping_list/.translations/ru.json | 14 ++++ .../shopping_list/.translations/sk.json | 14 ++++ .../shopping_list/.translations/sl.json | 14 ++++ .../shopping_list/.translations/zh-Hant.json | 14 ++++ .../simplisafe/.translations/ca.json | 10 +++ .../simplisafe/.translations/de.json | 10 +++ .../simplisafe/.translations/en.json | 1 + .../simplisafe/.translations/es.json | 10 +++ .../simplisafe/.translations/fr.json | 10 +++ .../simplisafe/.translations/it.json | 10 +++ .../simplisafe/.translations/ko.json | 10 +++ .../simplisafe/.translations/no.json | 10 +++ .../simplisafe/.translations/ru.json | 10 +++ .../simplisafe/.translations/sl.json | 13 ++++ .../simplisafe/.translations/zh-Hant.json | 10 +++ .../components/soma/.translations/ko.json | 2 +- .../components/tesla/.translations/ca.json | 1 + .../components/tesla/.translations/en.json | 50 +++++++------- .../components/tesla/.translations/es.json | 1 + .../components/tesla/.translations/no.json | 1 + .../components/tesla/.translations/ru.json | 1 + .../tesla/.translations/zh-Hant.json | 1 + .../components/toon/.translations/sl.json | 2 +- .../components/unifi/.translations/ca.json | 20 ++++-- .../components/unifi/.translations/de.json | 20 ++++-- .../components/unifi/.translations/en.json | 22 +++--- .../components/unifi/.translations/es.json | 18 ++++- .../components/unifi/.translations/fr.json | 13 +++- .../components/unifi/.translations/it.json | 20 ++++-- .../components/unifi/.translations/ko.json | 20 ++++-- .../components/unifi/.translations/lb.json | 3 + .../components/unifi/.translations/no.json | 18 ++++- .../components/unifi/.translations/pl.json | 14 +++- .../components/unifi/.translations/ru.json | 14 +++- .../components/unifi/.translations/sl.json | 25 +++++-- .../unifi/.translations/zh-Hant.json | 18 ++++- .../components/vilfo/.translations/de.json | 1 + .../components/vizio/.translations/ca.json | 29 +++++++- .../components/vizio/.translations/de.json | 31 ++++++++- .../components/vizio/.translations/en.json | 25 ++++++- .../components/vizio/.translations/es.json | 21 +++++- .../components/vizio/.translations/fr.json | 31 +++++++++ .../components/vizio/.translations/it.json | 25 ++++++- .../components/vizio/.translations/ko.json | 38 ++++++++++- .../components/vizio/.translations/no.json | 25 ++++++- .../components/vizio/.translations/pl.json | 27 ++++++++ .../components/vizio/.translations/ru.json | 21 +++++- .../components/vizio/.translations/sk.json | 22 ++++++ .../components/vizio/.translations/sl.json | 40 ++++++++++- .../vizio/.translations/zh-Hant.json | 25 ++++++- .../components/withings/.translations/sl.json | 2 +- .../components/wwlln/.translations/ca.json | 4 ++ .../components/wwlln/.translations/de.json | 4 ++ .../components/wwlln/.translations/en.json | 6 +- .../components/wwlln/.translations/es.json | 4 ++ .../components/wwlln/.translations/fr.json | 4 ++ .../components/wwlln/.translations/it.json | 4 ++ .../components/wwlln/.translations/ko.json | 4 ++ .../components/wwlln/.translations/no.json | 4 ++ .../components/wwlln/.translations/pl.json | 4 ++ .../components/wwlln/.translations/ru.json | 3 + .../components/wwlln/.translations/sl.json | 4 ++ .../wwlln/.translations/zh-Hant.json | 4 ++ .../components/zha/.translations/ca.json | 8 +++ .../components/zha/.translations/de.json | 8 +++ .../components/zha/.translations/fr.json | 2 +- .../components/zha/.translations/it.json | 8 +++ .../components/zha/.translations/ko.json | 8 +++ .../components/zha/.translations/pl.json | 8 +++ .../components/zha/.translations/sl.json | 8 +++ 340 files changed, 5429 insertions(+), 355 deletions(-) create mode 100644 homeassistant/components/airvisual/.translations/fr.json create mode 100644 homeassistant/components/airvisual/.translations/ko.json create mode 100644 homeassistant/components/airvisual/.translations/pl.json create mode 100644 homeassistant/components/airvisual/.translations/sk.json create mode 100644 homeassistant/components/airvisual/.translations/sl.json create mode 100644 homeassistant/components/august/.translations/fr.json create mode 100644 homeassistant/components/august/.translations/ko.json create mode 100644 homeassistant/components/august/.translations/pl.json create mode 100644 homeassistant/components/august/.translations/sl.json create mode 100644 homeassistant/components/coronavirus/.translations/ca.json create mode 100644 homeassistant/components/coronavirus/.translations/da.json create mode 100644 homeassistant/components/coronavirus/.translations/de.json create mode 100644 homeassistant/components/coronavirus/.translations/fr.json create mode 100644 homeassistant/components/coronavirus/.translations/it.json create mode 100644 homeassistant/components/coronavirus/.translations/ko.json create mode 100644 homeassistant/components/coronavirus/.translations/pl.json create mode 100644 homeassistant/components/coronavirus/.translations/sl.json create mode 100644 homeassistant/components/directv/.translations/ca.json create mode 100644 homeassistant/components/directv/.translations/de.json create mode 100644 homeassistant/components/directv/.translations/es.json create mode 100644 homeassistant/components/directv/.translations/fr.json create mode 100644 homeassistant/components/directv/.translations/it.json create mode 100644 homeassistant/components/directv/.translations/ko.json create mode 100644 homeassistant/components/directv/.translations/lb.json create mode 100644 homeassistant/components/directv/.translations/no.json create mode 100644 homeassistant/components/directv/.translations/pl.json create mode 100644 homeassistant/components/directv/.translations/ru.json create mode 100644 homeassistant/components/directv/.translations/sl.json create mode 100644 homeassistant/components/directv/.translations/zh-Hant.json create mode 100644 homeassistant/components/doorbird/.translations/ca.json create mode 100644 homeassistant/components/doorbird/.translations/da.json create mode 100644 homeassistant/components/doorbird/.translations/es.json create mode 100644 homeassistant/components/doorbird/.translations/fr.json create mode 100644 homeassistant/components/doorbird/.translations/ko.json create mode 100644 homeassistant/components/doorbird/.translations/lb.json create mode 100644 homeassistant/components/doorbird/.translations/no.json create mode 100644 homeassistant/components/doorbird/.translations/ru.json create mode 100644 homeassistant/components/doorbird/.translations/zh-Hant.json create mode 100644 homeassistant/components/freebox/.translations/ca.json create mode 100644 homeassistant/components/freebox/.translations/de.json create mode 100644 homeassistant/components/freebox/.translations/es.json create mode 100644 homeassistant/components/freebox/.translations/fr.json create mode 100644 homeassistant/components/freebox/.translations/it.json create mode 100644 homeassistant/components/freebox/.translations/ko.json create mode 100644 homeassistant/components/freebox/.translations/no.json create mode 100644 homeassistant/components/freebox/.translations/pl.json create mode 100644 homeassistant/components/freebox/.translations/ru.json create mode 100644 homeassistant/components/freebox/.translations/sl.json create mode 100644 homeassistant/components/freebox/.translations/zh-Hant.json create mode 100644 homeassistant/components/griddy/.translations/ca.json create mode 100644 homeassistant/components/griddy/.translations/da.json create mode 100644 homeassistant/components/griddy/.translations/de.json create mode 100644 homeassistant/components/griddy/.translations/es.json create mode 100644 homeassistant/components/griddy/.translations/fr.json create mode 100644 homeassistant/components/griddy/.translations/it.json create mode 100644 homeassistant/components/griddy/.translations/ko.json create mode 100644 homeassistant/components/griddy/.translations/lb.json create mode 100644 homeassistant/components/griddy/.translations/no.json create mode 100644 homeassistant/components/griddy/.translations/pl.json create mode 100644 homeassistant/components/griddy/.translations/ru.json create mode 100644 homeassistant/components/griddy/.translations/sl.json create mode 100644 homeassistant/components/griddy/.translations/tr.json create mode 100644 homeassistant/components/griddy/.translations/zh-Hant.json create mode 100644 homeassistant/components/harmony/.translations/ca.json create mode 100644 homeassistant/components/harmony/.translations/de.json create mode 100644 homeassistant/components/harmony/.translations/es.json create mode 100644 homeassistant/components/harmony/.translations/fr.json create mode 100644 homeassistant/components/harmony/.translations/it.json create mode 100644 homeassistant/components/harmony/.translations/ko.json create mode 100644 homeassistant/components/harmony/.translations/lb.json create mode 100644 homeassistant/components/harmony/.translations/no.json create mode 100644 homeassistant/components/harmony/.translations/ru.json create mode 100644 homeassistant/components/harmony/.translations/zh-Hant.json create mode 100644 homeassistant/components/icloud/.translations/tr.json create mode 100644 homeassistant/components/monoprice/.translations/ca.json create mode 100644 homeassistant/components/monoprice/.translations/en.json create mode 100644 homeassistant/components/monoprice/.translations/es.json create mode 100644 homeassistant/components/monoprice/.translations/fr.json create mode 100644 homeassistant/components/monoprice/.translations/ko.json create mode 100644 homeassistant/components/monoprice/.translations/lb.json create mode 100644 homeassistant/components/monoprice/.translations/no.json create mode 100644 homeassistant/components/monoprice/.translations/ru.json create mode 100644 homeassistant/components/monoprice/.translations/zh-Hant.json create mode 100644 homeassistant/components/myq/.translations/ca.json create mode 100644 homeassistant/components/myq/.translations/da.json create mode 100644 homeassistant/components/myq/.translations/de.json create mode 100644 homeassistant/components/myq/.translations/es.json create mode 100644 homeassistant/components/myq/.translations/fr.json create mode 100644 homeassistant/components/myq/.translations/ko.json create mode 100644 homeassistant/components/myq/.translations/lb.json create mode 100644 homeassistant/components/myq/.translations/no.json create mode 100644 homeassistant/components/myq/.translations/ru.json create mode 100644 homeassistant/components/myq/.translations/zh-Hant.json create mode 100644 homeassistant/components/nexia/.translations/ca.json create mode 100644 homeassistant/components/nexia/.translations/da.json create mode 100644 homeassistant/components/nexia/.translations/de.json create mode 100644 homeassistant/components/nexia/.translations/es.json create mode 100644 homeassistant/components/nexia/.translations/fr.json create mode 100644 homeassistant/components/nexia/.translations/ko.json create mode 100644 homeassistant/components/nexia/.translations/lb.json create mode 100644 homeassistant/components/nexia/.translations/no.json create mode 100644 homeassistant/components/nexia/.translations/ru.json create mode 100644 homeassistant/components/nexia/.translations/zh-Hant.json create mode 100644 homeassistant/components/nuheat/.translations/ca.json create mode 100644 homeassistant/components/nuheat/.translations/da.json create mode 100644 homeassistant/components/nuheat/.translations/es.json create mode 100644 homeassistant/components/nuheat/.translations/fr.json create mode 100644 homeassistant/components/nuheat/.translations/ko.json create mode 100644 homeassistant/components/nuheat/.translations/lb.json create mode 100644 homeassistant/components/nuheat/.translations/no.json create mode 100644 homeassistant/components/nuheat/.translations/ru.json create mode 100644 homeassistant/components/nuheat/.translations/zh-Hant.json create mode 100644 homeassistant/components/powerwall/.translations/ca.json create mode 100644 homeassistant/components/powerwall/.translations/de.json create mode 100644 homeassistant/components/powerwall/.translations/en.json create mode 100644 homeassistant/components/powerwall/.translations/es.json create mode 100644 homeassistant/components/powerwall/.translations/fr.json create mode 100644 homeassistant/components/powerwall/.translations/it.json create mode 100644 homeassistant/components/powerwall/.translations/ko.json create mode 100644 homeassistant/components/powerwall/.translations/lb.json create mode 100644 homeassistant/components/powerwall/.translations/no.json create mode 100644 homeassistant/components/powerwall/.translations/ru.json create mode 100644 homeassistant/components/powerwall/.translations/zh-Hant.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/ca.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/fr.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/lb.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/no.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/ru.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/zh-Hant.json create mode 100644 homeassistant/components/rachio/.translations/ca.json create mode 100644 homeassistant/components/rachio/.translations/de.json create mode 100644 homeassistant/components/rachio/.translations/es.json create mode 100644 homeassistant/components/rachio/.translations/fr.json create mode 100644 homeassistant/components/rachio/.translations/it.json create mode 100644 homeassistant/components/rachio/.translations/ko.json create mode 100644 homeassistant/components/rachio/.translations/lb.json create mode 100644 homeassistant/components/rachio/.translations/no.json create mode 100644 homeassistant/components/rachio/.translations/ru.json create mode 100644 homeassistant/components/rachio/.translations/sl.json create mode 100644 homeassistant/components/rachio/.translations/zh-Hant.json create mode 100644 homeassistant/components/roku/.translations/ca.json create mode 100644 homeassistant/components/roku/.translations/cs.json create mode 100644 homeassistant/components/roku/.translations/de.json create mode 100644 homeassistant/components/roku/.translations/es.json create mode 100644 homeassistant/components/roku/.translations/fr.json create mode 100644 homeassistant/components/roku/.translations/it.json create mode 100644 homeassistant/components/roku/.translations/ko.json create mode 100644 homeassistant/components/roku/.translations/lb.json create mode 100644 homeassistant/components/roku/.translations/no.json create mode 100644 homeassistant/components/roku/.translations/pl.json create mode 100644 homeassistant/components/roku/.translations/ru.json create mode 100644 homeassistant/components/roku/.translations/sl.json create mode 100644 homeassistant/components/roku/.translations/zh-Hant.json create mode 100644 homeassistant/components/sense/.translations/da.json create mode 100644 homeassistant/components/sense/.translations/fr.json create mode 100644 homeassistant/components/sense/.translations/ko.json create mode 100644 homeassistant/components/sense/.translations/pl.json create mode 100644 homeassistant/components/sense/.translations/sl.json create mode 100644 homeassistant/components/shopping_list/.translations/ca.json create mode 100644 homeassistant/components/shopping_list/.translations/de.json create mode 100644 homeassistant/components/shopping_list/.translations/es.json create mode 100644 homeassistant/components/shopping_list/.translations/fr.json create mode 100644 homeassistant/components/shopping_list/.translations/it.json create mode 100644 homeassistant/components/shopping_list/.translations/ko.json create mode 100644 homeassistant/components/shopping_list/.translations/lb.json create mode 100644 homeassistant/components/shopping_list/.translations/no.json create mode 100644 homeassistant/components/shopping_list/.translations/pl.json create mode 100644 homeassistant/components/shopping_list/.translations/ru.json create mode 100644 homeassistant/components/shopping_list/.translations/sk.json create mode 100644 homeassistant/components/shopping_list/.translations/sl.json create mode 100644 homeassistant/components/shopping_list/.translations/zh-Hant.json create mode 100644 homeassistant/components/vizio/.translations/sk.json diff --git a/homeassistant/components/airvisual/.translations/ca.json b/homeassistant/components/airvisual/.translations/ca.json index b80386dc75b..66cd796d752 100644 --- a/homeassistant/components/airvisual/.translations/ca.json +++ b/homeassistant/components/airvisual/.translations/ca.json @@ -19,5 +19,16 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra al mapa l'\u00e0rea geogr\u00e0fica monitoritzada" + }, + "description": "Estableix les diferents opcions de la integraci\u00f3 AirVisual.", + "title": "Configuraci\u00f3 d'AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/de.json b/homeassistant/components/airvisual/.translations/de.json index 0c624614610..116e5ff500c 100644 --- a/homeassistant/components/airvisual/.translations/de.json +++ b/homeassistant/components/airvisual/.translations/de.json @@ -11,11 +11,24 @@ "data": { "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad" + "longitude": "L\u00e4ngengrad", + "show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" }, + "description": "\u00dcberwachen Sie die Luftqualit\u00e4t an einem geografischen Ort.", "title": "Konfigurieren Sie AirVisual" } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" + }, + "description": "Legen Sie verschiedene Optionen f\u00fcr die AirVisual-Integration fest.", + "title": "Konfigurieren Sie AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/en.json b/homeassistant/components/airvisual/.translations/en.json index 2bcff29b770..604baf1feb6 100644 --- a/homeassistant/components/airvisual/.translations/en.json +++ b/homeassistant/components/airvisual/.translations/en.json @@ -11,7 +11,8 @@ "data": { "api_key": "API Key", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "show_on_map": "Show monitored geography on the map" }, "description": "Monitor air quality in a geographical location.", "title": "Configure AirVisual" diff --git a/homeassistant/components/airvisual/.translations/es.json b/homeassistant/components/airvisual/.translations/es.json index 3ec5c12f1e9..a1054c79098 100644 --- a/homeassistant/components/airvisual/.translations/es.json +++ b/homeassistant/components/airvisual/.translations/es.json @@ -19,5 +19,16 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa" + }, + "description": "Ajustar varias opciones para la integraci\u00f3n de AirVisual.", + "title": "Configurar AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/fr.json b/homeassistant/components/airvisual/.translations/fr.json new file mode 100644 index 00000000000..9f32bbf5d94 --- /dev/null +++ b/homeassistant/components/airvisual/.translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e." + }, + "error": { + "invalid_api_key": "Cl\u00e9 API invalide" + }, + "step": { + "user": { + "data": { + "api_key": "Cl\u00e9 API", + "latitude": "Latitude", + "longitude": "Longitude", + "show_on_map": "Afficher la g\u00e9ographie surveill\u00e9e sur la carte" + }, + "description": "Surveiller la qualit\u00e9 de l\u2019air dans un emplacement g\u00e9ographique.", + "title": "Configurer AirVisual" + } + }, + "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "description": "D\u00e9finissez diverses options pour l'int\u00e9gration d'AirVisual.", + "title": "Configurer AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/it.json b/homeassistant/components/airvisual/.translations/it.json index 860a1e3e577..9db76248a36 100644 --- a/homeassistant/components/airvisual/.translations/it.json +++ b/homeassistant/components/airvisual/.translations/it.json @@ -19,5 +19,16 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Mostra l'area geografica monitorata sulla mappa" + }, + "description": "Impostare varie opzioni per l'integrazione AirVisual.", + "title": "Configurare AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/ko.json b/homeassistant/components/airvisual/.translations/ko.json new file mode 100644 index 00000000000..8f1155aa5f9 --- /dev/null +++ b/homeassistant/components/airvisual/.translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 API \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + }, + "error": { + "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" + }, + "step": { + "user": { + "data": { + "api_key": "API \ud0a4", + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4", + "show_on_map": "\uc9c0\ub3c4\uc5d0 \ubaa8\ub2c8\ud130\ub9c1\ub41c \uc9c0\ub9ac \uc815\ubcf4 \ud45c\uc2dc" + }, + "description": "\uc9c0\ub9ac\uc801 \uc704\uce58\uc5d0\uc11c \ub300\uae30\uc9c8\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "AirVisual \uad6c\uc131" + } + }, + "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\uc9c0\ub3c4\uc5d0 \ubaa8\ub2c8\ud130\ub9c1\ub41c \uc9c0\ub9ac \uc815\ubcf4 \ud45c\uc2dc" + }, + "description": "AirVisual \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\uc5d0 \ub300\ud55c \ub2e4\uc591\ud55c \uc635\uc158\uc744 \uc124\uc815\ud574\uc8fc\uc138\uc694.", + "title": "AirVisual \uad6c\uc131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/no.json b/homeassistant/components/airvisual/.translations/no.json index bf089c485d6..de2991f0757 100644 --- a/homeassistant/components/airvisual/.translations/no.json +++ b/homeassistant/components/airvisual/.translations/no.json @@ -19,5 +19,16 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Vis overv\u00e5ket geografi p\u00e5 kartet" + }, + "description": "Angi forskjellige alternativer for AirVisual-integrasjonen.", + "title": "Konfigurer AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/pl.json b/homeassistant/components/airvisual/.translations/pl.json new file mode 100644 index 00000000000..ebcbc12e405 --- /dev/null +++ b/homeassistant/components/airvisual/.translations/pl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ten klucz API jest ju\u017c w u\u017cyciu." + }, + "error": { + "invalid_api_key": "Nieprawid\u0142owy klucz API" + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API", + "latitude": "Szeroko\u015b\u0107 geograficzna", + "longitude": "D\u0142ugo\u015b\u0107 geograficzna", + "show_on_map": "Wy\u015bwietlaj encje na mapie" + }, + "description": "Monitoruj jako\u015b\u0107 powietrza w okre\u015blonej lokalizacji geograficznej.", + "title": "Konfiguracja AirVisual" + } + }, + "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Wy\u015bwietlaj encje na mapie" + }, + "description": "Konfiguracja opcji integracji AirVisual.", + "title": "Konfiguracja AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/ru.json b/homeassistant/components/airvisual/.translations/ru.json index 2eac29c9ecc..5c9634390c6 100644 --- a/homeassistant/components/airvisual/.translations/ru.json +++ b/homeassistant/components/airvisual/.translations/ru.json @@ -19,5 +19,16 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 AirVisual.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/sk.json b/homeassistant/components/airvisual/.translations/sk.json new file mode 100644 index 00000000000..e6945904d90 --- /dev/null +++ b/homeassistant/components/airvisual/.translations/sk.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "latitude": "Zemepisn\u00e1 \u0161\u00edrka", + "longitude": "Zemepisn\u00e1 d\u013a\u017eka" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/sl.json b/homeassistant/components/airvisual/.translations/sl.json new file mode 100644 index 00000000000..97ed91592d5 --- /dev/null +++ b/homeassistant/components/airvisual/.translations/sl.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Ta klju\u010d API je \u017ee v uporabi." + }, + "error": { + "invalid_api_key": "Neveljaven API klju\u010d" + }, + "step": { + "user": { + "data": { + "api_key": "API Klju\u010d", + "latitude": "Zemljepisna \u0161irina", + "longitude": "Zemljepisna dol\u017eina", + "show_on_map": "Prika\u017ei nadzorovano obmo\u010dje na zemljevidu" + }, + "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", + "title": "Nastavite AirVisual" + } + }, + "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "Prika\u017ei nadzorovano obmo\u010dje na zemljevidu" + }, + "description": "Nastavite razli\u010dne mo\u017enosti za integracijo AirVisual.", + "title": "Nastavite AirVisual" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/zh-Hant.json b/homeassistant/components/airvisual/.translations/zh-Hant.json index 3f62c06a9e2..c1e9777d860 100644 --- a/homeassistant/components/airvisual/.translations/zh-Hant.json +++ b/homeassistant/components/airvisual/.translations/zh-Hant.json @@ -19,5 +19,16 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "data": { + "show_on_map": "\u65bc\u5730\u5716\u4e0a\u986f\u793a\u76e3\u63a7\u4f4d\u7f6e\u3002" + }, + "description": "\u8a2d\u5b9a AirVisual \u6574\u5408\u9078\u9805\u3002", + "title": "\u8a2d\u5b9a AirVisual" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/alarm_control_panel/.translations/ca.json b/homeassistant/components/alarm_control_panel/.translations/ca.json index d60cf3173c7..5c33ac3c963 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ca.json +++ b/homeassistant/components/alarm_control_panel/.translations/ca.json @@ -7,10 +7,17 @@ "disarm": "Desactiva {entity_name}", "trigger": "Dispara {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e0 activada en mode 'a fora'", + "is_armed_home": "{entity_name} est\u00e0 activada en mode 'a casa'", + "is_armed_night": "{entity_name} est\u00e0 activada en mode 'nocturn'", + "is_disarmed": "{entity_name} est\u00e0 desactivada", + "is_triggered": "{entity_name} est\u00e0 disparada" + }, "trigger_type": { - "armed_away": "{entity_name} activada en mode a fora", - "armed_home": "{entity_name} activada en mode a casa", - "armed_night": "{entity_name} activada en mode nocturn", + "armed_away": "{entity_name} activada en mode 'a fora'", + "armed_home": "{entity_name} activada en mode 'a casa'", + "armed_night": "{entity_name} activada en mode 'nocturn'", "disarmed": "{entity_name} desactivada", "triggered": "{entity_name} disparat/ada" } diff --git a/homeassistant/components/alarm_control_panel/.translations/de.json b/homeassistant/components/alarm_control_panel/.translations/de.json index 1787391c292..2b319c4a8a6 100644 --- a/homeassistant/components/alarm_control_panel/.translations/de.json +++ b/homeassistant/components/alarm_control_panel/.translations/de.json @@ -7,6 +7,13 @@ "disarm": "Deaktivere {entity_name}", "trigger": "Ausl\u00f6ser {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} ist aktiviert - Unterwegs", + "is_armed_home": "{entity_name} ist aktiviert - Zuhause", + "is_armed_night": "{entity_name} ist aktiviert - Nacht", + "is_disarmed": "{entity_name} ist deaktiviert", + "is_triggered": "{entity_name} wurde ausgel\u00f6st" + }, "trigger_type": { "armed_away": "{entity_name} Unterwegs", "armed_home": "{entity_name} Zuhause", diff --git a/homeassistant/components/alarm_control_panel/.translations/en.json b/homeassistant/components/alarm_control_panel/.translations/en.json index a00e81feb92..85b6be1138c 100644 --- a/homeassistant/components/alarm_control_panel/.translations/en.json +++ b/homeassistant/components/alarm_control_panel/.translations/en.json @@ -7,6 +7,13 @@ "disarm": "Disarm {entity_name}", "trigger": "Trigger {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} is armed away", + "is_armed_home": "{entity_name} is armed home", + "is_armed_night": "{entity_name} is armed night", + "is_disarmed": "{entity_name} is disarmed", + "is_triggered": "{entity_name} is triggered" + }, "trigger_type": { "armed_away": "{entity_name} armed away", "armed_home": "{entity_name} armed home", diff --git a/homeassistant/components/alarm_control_panel/.translations/es.json b/homeassistant/components/alarm_control_panel/.translations/es.json index 8200755de0f..0acc0e5c98c 100644 --- a/homeassistant/components/alarm_control_panel/.translations/es.json +++ b/homeassistant/components/alarm_control_panel/.translations/es.json @@ -7,6 +7,13 @@ "disarm": "Desarmar {entity_name}", "trigger": "Lanzar {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} est\u00e1 armada fuera", + "is_armed_home": "{entity_name} est\u00e1 armada en casa", + "is_armed_night": "{entity_name} est\u00e1 armada noche", + "is_disarmed": "{entity_name} est\u00e1 desarmada", + "is_triggered": "{entity_name} est\u00e1 disparada" + }, "trigger_type": { "armed_away": "{entity_name} armado fuera", "armed_home": "{entity_name} armado en casa", diff --git a/homeassistant/components/alarm_control_panel/.translations/fr.json b/homeassistant/components/alarm_control_panel/.translations/fr.json index fbdc6a5605f..f87f1b79b87 100644 --- a/homeassistant/components/alarm_control_panel/.translations/fr.json +++ b/homeassistant/components/alarm_control_panel/.translations/fr.json @@ -7,6 +7,13 @@ "disarm": "D\u00e9sarmer {entity_name}", "trigger": "D\u00e9clencheur {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} est arm\u00e9", + "is_armed_home": "{entity_name} est arm\u00e9 \u00e0 la maison", + "is_armed_night": "{entity_name} est arm\u00e9 la nuit", + "is_disarmed": "{entity_name} est d\u00e9sarm\u00e9", + "is_triggered": "{entity_name} est d\u00e9clench\u00e9" + }, "trigger_type": { "armed_away": "Armer {entity_name} en mode \"sortie\"", "armed_home": "Armer {entity_name} en mode \"maison\"", diff --git a/homeassistant/components/alarm_control_panel/.translations/it.json b/homeassistant/components/alarm_control_panel/.translations/it.json index 78a3f0b07e5..0857f0665aa 100644 --- a/homeassistant/components/alarm_control_panel/.translations/it.json +++ b/homeassistant/components/alarm_control_panel/.translations/it.json @@ -7,11 +7,18 @@ "disarm": "Disarmare {entity_name}", "trigger": "Attivazione {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} \u00e8 attivo in modalit\u00e0 fuori casa", + "is_armed_home": "{entity_name} \u00e8 attivo in modalit\u00e0 a casa", + "is_armed_night": "{entity_name} \u00e8 attivo in modalit\u00e0 notte", + "is_disarmed": "{entity_name} \u00e8 disattivo", + "is_triggered": "{entity_name} \u00e8 attivato" + }, "trigger_type": { - "armed_away": "{entity_name} armata modalit\u00e0 fuori casa", - "armed_home": "{entity_name} armata modalit\u00e0 a casa", - "armed_night": "{entity_name} armata modalit\u00e0 notte", - "disarmed": "{entity_name} disarmato", + "armed_away": "{entity_name} attivato in modalit\u00e0 fuori casa", + "armed_home": "{entity_name} attivato in modalit\u00e0 a casa", + "armed_night": "{entity_name} attivato in modalit\u00e0 notte", + "disarmed": "{entity_name} disattivato", "triggered": "{entity_name} attivato" } } diff --git a/homeassistant/components/alarm_control_panel/.translations/ko.json b/homeassistant/components/alarm_control_panel/.translations/ko.json index b70ae8dc025..321bc442444 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ko.json +++ b/homeassistant/components/alarm_control_panel/.translations/ko.json @@ -7,6 +7,13 @@ "disarm": "{entity_name} \uacbd\ube44\ud574\uc81c", "trigger": "{entity_name} \ud2b8\ub9ac\uac70" }, + "condition_type": { + "is_armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_armed_night": "{entity_name} \uc774(\uac00) \uc57c\uac04 \uacbd\ube44\ubaa8\ub4dc \uc0c1\ud0dc\uc774\uba74", + "is_disarmed": "{entity_name} \uc774(\uac00) \ud574\uc81c \uc0c1\ud0dc\uc774\uba74", + "is_triggered": "{entity_name} \uc774(\uac00) \ud2b8\ub9ac\uac70\ub418\uc5c8\uc73c\uba74" + }, "trigger_type": { "armed_away": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", "armed_home": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uacbd\ube44\ubaa8\ub4dc\ub85c \uc124\uc815\ub420 \ub54c", diff --git a/homeassistant/components/alarm_control_panel/.translations/lb.json b/homeassistant/components/alarm_control_panel/.translations/lb.json index add11f5b8fe..6c0d32f42ad 100644 --- a/homeassistant/components/alarm_control_panel/.translations/lb.json +++ b/homeassistant/components/alarm_control_panel/.translations/lb.json @@ -7,6 +7,13 @@ "disarm": "{entity_name} entsch\u00e4rfen", "trigger": "{entity_name} ausl\u00e9isen" }, + "condition_type": { + "is_armed_away": "{entity_name} ass ugeschalt fir Ennerwee", + "is_armed_home": "{entity_name} ass ugeschalt fir Doheem", + "is_armed_night": "{entity_name} ass ugeschalt fir Nuecht", + "is_disarmed": "{entity_name} ass entsch\u00e4rft", + "is_triggered": "{entity_name} ass ausgel\u00e9ist" + }, "trigger_type": { "armed_away": "{entity_name} ugeschalt fir Ennerwee", "armed_home": "{entity_name} ugeschalt fir Doheem", diff --git a/homeassistant/components/alarm_control_panel/.translations/no.json b/homeassistant/components/alarm_control_panel/.translations/no.json index 0b58064fe09..1177e130150 100644 --- a/homeassistant/components/alarm_control_panel/.translations/no.json +++ b/homeassistant/components/alarm_control_panel/.translations/no.json @@ -7,6 +7,13 @@ "disarm": "Deaktiver {entity_name}", "trigger": "Utl\u00f8ser {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} aktivert borte", + "is_armed_home": "{entity_name} aktivert hjemme", + "is_armed_night": "{entity_name} aktivert natt", + "is_disarmed": "{entity_name} er deaktivert", + "is_triggered": "{entity_name} er utl\u00f8st" + }, "trigger_type": { "armed_away": "{entity_name} aktivert borte", "armed_home": "{entity_name} aktivert hjemme", diff --git a/homeassistant/components/alarm_control_panel/.translations/pl.json b/homeassistant/components/alarm_control_panel/.translations/pl.json index 024a0861c1c..c1125be31b6 100644 --- a/homeassistant/components/alarm_control_panel/.translations/pl.json +++ b/homeassistant/components/alarm_control_panel/.translations/pl.json @@ -7,6 +7,13 @@ "disarm": "rozbr\u00f3j {entity_name}", "trigger": "wyzw\u00f3l {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} jest uzbrojony (poza domem)", + "is_armed_home": "{entity_name} jest uzbrojony (w domu)", + "is_armed_night": "{entity_name} jest uzbrojony (noc)", + "is_disarmed": "{entity_name} jest rozbrojony", + "is_triggered": "{entity_name} jest wyzwolony" + }, "trigger_type": { "armed_away": "{entity_name} zostanie uzbrojony (poza domem)", "armed_home": "{entity_name} zostanie uzbrojony (w domu)", diff --git a/homeassistant/components/alarm_control_panel/.translations/ru.json b/homeassistant/components/alarm_control_panel/.translations/ru.json index f9a0e859e11..36705dbcefd 100644 --- a/homeassistant/components/alarm_control_panel/.translations/ru.json +++ b/homeassistant/components/alarm_control_panel/.translations/ru.json @@ -7,6 +7,13 @@ "disarm": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043e\u0445\u0440\u0430\u043d\u0443 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "trigger": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" }, + "condition_type": { + "is_armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_armed_night": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u043e\u0447\u044c\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_disarmed": "\u041e\u0442\u043a\u043b\u044e\u0447\u0435\u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0430 \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", + "is_triggered": "{entity_name} \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u0435\u0442" + }, "trigger_type": { "armed_away": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u041d\u0435 \u0434\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", "armed_home": "\u0412\u043a\u043b\u044e\u0447\u0435\u043d \u0440\u0435\u0436\u0438\u043c \u043e\u0445\u0440\u0430\u043d\u044b \"\u0414\u043e\u043c\u0430\" \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 {entity_name}", diff --git a/homeassistant/components/alarm_control_panel/.translations/sl.json b/homeassistant/components/alarm_control_panel/.translations/sl.json index 855c50ab827..c817f7830ba 100644 --- a/homeassistant/components/alarm_control_panel/.translations/sl.json +++ b/homeassistant/components/alarm_control_panel/.translations/sl.json @@ -7,6 +7,13 @@ "disarm": "Razoro\u017ei {entity_name}", "trigger": "Spro\u017ei {entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name} je oboro\u017een na \"zdoma\"", + "is_armed_home": "{entity_name} je oboro\u017een na \"dom\"", + "is_armed_night": "{entity_name} je oboro\u017een na \"no\u010d\"", + "is_disarmed": "{entity_name} razoro\u017een", + "is_triggered": "{entity_name} spro\u017een" + }, "trigger_type": { "armed_away": "{entity_name} oboro\u017een - zdoma", "armed_home": "{entity_name} oboro\u017een - dom", diff --git a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json index 94729865c6f..a02ea1c1966 100644 --- a/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json +++ b/homeassistant/components/alarm_control_panel/.translations/zh-Hant.json @@ -7,6 +7,13 @@ "disarm": "\u89e3\u9664{entity_name}", "trigger": "\u89f8\u767c{entity_name}" }, + "condition_type": { + "is_armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", + "is_armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", + "is_armed_night": "{entity_name}\u8a2d\u5b9a\u591c\u9593", + "is_disarmed": "{entity_name}\u5df2\u89e3\u9664", + "is_triggered": "{entity_name}\u5df2\u89f8\u767c" + }, "trigger_type": { "armed_away": "{entity_name}\u8a2d\u5b9a\u5916\u51fa", "armed_home": "{entity_name}\u8a2d\u5b9a\u5728\u5bb6", diff --git a/homeassistant/components/ambient_station/.translations/sl.json b/homeassistant/components/ambient_station/.translations/sl.json index 906a6b404c4..d5cf039b9f4 100644 --- a/homeassistant/components/ambient_station/.translations/sl.json +++ b/homeassistant/components/ambient_station/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ta klju\u010d za aplikacijo je \u017ee v uporabi." + }, "error": { "identifier_exists": "Aplikacijski klju\u010d in / ali klju\u010d API je \u017ee registriran", "invalid_key": "Neveljaven klju\u010d API in / ali klju\u010d aplikacije", diff --git a/homeassistant/components/august/.translations/de.json b/homeassistant/components/august/.translations/de.json index dd3b2ea9f44..8d34eaaf5ee 100644 --- a/homeassistant/components/august/.translations/de.json +++ b/homeassistant/components/august/.translations/de.json @@ -16,6 +16,7 @@ "timeout": "Zeit\u00fcberschreitung (Sekunden)", "username": "Benutzername" }, + "description": "Wenn die Anmeldemethode \"E-Mail\" lautet, ist Benutzername die E-Mail-Adresse. Wenn die Anmeldemethode \"Telefon\" ist, ist Benutzername die Telefonnummer im Format \"+ NNNNNNNNN\".", "title": "Richten Sie ein August-Konto ein" }, "validation": { diff --git a/homeassistant/components/august/.translations/fr.json b/homeassistant/components/august/.translations/fr.json new file mode 100644 index 00000000000..3a116c7bc06 --- /dev/null +++ b/homeassistant/components/august/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "error": { + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "login_method": "M\u00e9thode de connexion", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + } + }, + "validation": { + "data": { + "code": "Code de v\u00e9rification" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/.translations/ko.json b/homeassistant/components/august/.translations/ko.json new file mode 100644 index 00000000000..018bb9d6a56 --- /dev/null +++ b/homeassistant/components/august/.translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "login_method": "\ub85c\uadf8\uc778 \ubc29\ubc95", + "password": "\ube44\ubc00\ubc88\ud638", + "timeout": "\uc81c\ud55c \uc2dc\uac04 (\ucd08)", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "\ub85c\uadf8\uc778 \ubc29\ubc95\uc774 '\uc774\uba54\uc77c'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\uba54\uc77c \uc8fc\uc18c\uc785\ub2c8\ub2e4. \ub85c\uadf8\uc778 \ubc29\ubc95\uc774 'phone'\uc778 \uacbd\uc6b0, \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 '+NNNNNNNNN' \ud615\uc2dd\uc758 \uc804\ud654\ubc88\ud638\uc785\ub2c8\ub2e4.", + "title": "August \uacc4\uc815 \uc124\uc815" + }, + "validation": { + "data": { + "code": "\uc778\uc99d \ucf54\ub4dc" + }, + "description": "{login_method} ({username}) \uc744(\ub97c) \ud655\uc778\ud558\uace0 \uc544\ub798\uc5d0 \uc778\uc99d \ucf54\ub4dc\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694", + "title": "2\ub2e8\uacc4 \uc778\uc99d" + } + }, + "title": "August" + } +} \ No newline at end of file diff --git a/homeassistant/components/august/.translations/pl.json b/homeassistant/components/august/.translations/pl.json new file mode 100644 index 00000000000..70654e12566 --- /dev/null +++ b/homeassistant/components/august/.translations/pl.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Konto jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "login_method": "Metoda logowania", + "password": "Has\u0142o", + "username": "Nazwa u\u017cytkownika" + } + }, + "validation": { + "data": { + "code": "Kod weryfikacyjny" + }, + "title": "Uwierzytelnianie dwusk\u0142adnikowe" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/august/.translations/sl.json b/homeassistant/components/august/.translations/sl.json new file mode 100644 index 00000000000..d0497278fee --- /dev/null +++ b/homeassistant/components/august/.translations/sl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ra\u010dun je \u017ee nastavljen" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "login_method": "Na\u010din prijave", + "password": "Geslo", + "timeout": "\u010casovna omejitev (sekunde)", + "username": "Uporabni\u0161ko ime" + }, + "description": "\u010ce je metoda za prijavo 'e-po\u0161ta', je e-po\u0161tni naslov uporabni\u0161ko ime. V kolikor je na\u010din prijave \"telefon\", je uporabni\u0161ko ime telefonska \u0161tevilka v obliki \" +NNNNNNNNN\".", + "title": "Nastavite ra\u010dun August" + }, + "validation": { + "data": { + "code": "Koda za preverjanje" + }, + "description": "Preverite svoj {login_method} ({username}) in spodaj vnesite verifikacijsko kodo", + "title": "Dvofaktorska avtentikacija" + } + }, + "title": "Avgust" + } +} \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json index 43a352c4bc0..44a701ed117 100644 --- a/homeassistant/components/axis/.translations/sl.json +++ b/homeassistant/components/axis/.translations/sl.json @@ -2,7 +2,7 @@ "config": { "abort": { "already_configured": "Naprava je \u017ee konfigurirana", - "bad_config_file": "Napa\u010dni podatki iz konfiguracijske datoteke", + "bad_config_file": "Slabi podatki iz konfiguracijske datoteke", "link_local_address": "Lokalni naslovi povezave niso podprti", "not_axis_device": "Odkrita naprava ni naprava Axis", "updated_configuration": "Posodobljena konfiguracija naprave z novim naslovom gostitelja" diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 4c1cba2bec5..7fa745a9a9a 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -25,13 +25,13 @@ "is_not_locked": "{entity_name} \uc774(\uac00) \uc7a0\uaca8\uc788\uc9c0 \uc54a\uc73c\uba74", "is_not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud558\uba74", "is_not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc73c\uba74", - "is_not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uc73c\uba74", + "is_not_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uba74", "is_not_open": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", "is_not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud600 \uc788\uc73c\uba74", "is_not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc73c\uba74", "is_not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc911\uc774\uba74", "is_not_unsafe": "{entity_name} \uc774(\uac00) \uc548\uc804\ud558\uba74", - "is_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uba74", + "is_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uc774\uba74", "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", "is_open": "{entity_name} \uc774(\uac00) \uc5f4\ub824 \uc788\uc73c\uba74", @@ -71,13 +71,13 @@ "not_locked": "{entity_name} \uc758 \uc7a0\uae08\uc774 \ud574\uc81c\ub420 \ub54c", "not_moist": "{entity_name} \uc774(\uac00) \uac74\uc870\ud574\uc9c8 \ub54c", "not_moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc774\uc9c0 \uc54a\uc744 \ub54c", - "not_occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774\uc9c0 \uc54a\uac8c \ub420 \ub54c", + "not_occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \uc544\ub2c8\uac8c \ub420 \ub54c", "not_opened": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", "not_plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \ubf51\ud790 \ub54c", "not_powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub418\uc9c0 \uc54a\uc744 \ub54c", "not_present": "{entity_name} \uc774(\uac00) \uc678\ucd9c \uc0c1\ud0dc\uac00 \ub420 \ub54c", "not_unsafe": "{entity_name} \uc740(\ub294) \uc548\uc804\ud574\uc9c8 \ub54c", - "occupied": "{entity_name} \uc774(\uac00) \uc0ac\uc6a9 \uc911\uc774 \ub420 \ub54c", + "occupied": "{entity_name} \uc774(\uac00) \uc7ac\uc2e4 \uc0c1\ud0dc\uac00 \ub420 \ub54c", "opened": "{entity_name} \uc774(\uac00) \uc5f4\ub9b4 \ub54c", "plugged_in": "{entity_name} \ud50c\ub7ec\uadf8\uac00 \uaf3d\ud790 \ub54c", "powered": "{entity_name} \uc5d0 \uc804\uc6d0\uc774 \uacf5\uae09\ub420 \ub54c", diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json index f1df9a06be1..4786e258ff8 100644 --- a/homeassistant/components/cert_expiry/.translations/ca.json +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada" + "already_configured": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", + "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", + "import_failed": "La importaci\u00f3 des de configuraci\u00f3 ha fallat" }, "error": { "certificate_error": "El certificat no ha pogut ser validat", "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", + "connection_refused": "La connexi\u00f3 s'ha rebutjat en connectar-se a l'amfitri\u00f3", "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3", diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json index e344e2dfd29..edf116203c7 100644 --- a/homeassistant/components/cert_expiry/.translations/de.json +++ b/homeassistant/components/cert_expiry/.translations/de.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert." + "already_configured": "Diese Kombination aus Host und Port ist bereits konfiguriert.", + "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.", + "import_failed": "Import aus Konfiguration fehlgeschlagen" }, "error": { "certificate_error": "Zertifikat konnte nicht validiert werden", "certificate_fetch_failed": "Zertifikat kann von dieser Kombination aus Host und Port nicht abgerufen werden", + "connection_refused": "Verbindung beim Herstellen einer Verbindung zum Host abgelehnt", "connection_timeout": "Zeit\u00fcberschreitung beim Herstellen einer Verbindung mit diesem Host", "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.", "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden", diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json index 9e7df5564a2..245b899fadf 100644 --- a/homeassistant/components/cert_expiry/.translations/fr.json +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e" + "already_configured": "Cette combinaison h\u00f4te et port est d\u00e9j\u00e0 configur\u00e9e", + "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", + "import_failed": "\u00c9chec de l'importation \u00e0 partir de la configuration" }, "error": { "certificate_error": "Le certificat n'a pas pu \u00eatre valid\u00e9", "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", + "connection_refused": "Connexion refus\u00e9e lors de la connexion \u00e0 l'h\u00f4te", "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu", diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index d95b9cd84a1..e7a2801423d 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata" + "already_configured": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", + "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", + "import_failed": "Importazione dalla configurazione non riuscita" }, "error": { "certificate_error": "Il certificato non pu\u00f2 essere convalidato", "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", + "connection_refused": "Connessione rifiutata durante la connessione all'host", "connection_timeout": "Tempo scaduto collegandosi a questo host", "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", "resolve_failed": "Questo host non pu\u00f2 essere risolto", diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json index 25c518f8629..060bf6e26bd 100644 --- a/homeassistant/components/cert_expiry/.translations/ko.json +++ b/homeassistant/components/cert_expiry/.translations/ko.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "import_failed": "\uad6c\uc131\uc5d0\uc11c \uac00\uc838\uc624\uae30 \uc2e4\ud328" }, "error": { "certificate_error": "\uc778\uc99d\uc11c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", + "connection_refused": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\uc774 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index 2e50a9f8cbc..a4806ff13aa 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany." + "already_configured": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", + "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.", + "import_failed": "Import z konfiguracji nie powi\u00f3d\u0142 si\u0119" }, "error": { "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", + "connection_refused": "Po\u0142\u0105czenie odrzucone podczas \u0142\u0105czenia z hostem", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.", "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.", "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index d375c626c66..284b0b960ba 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -1,11 +1,14 @@ { "config": { "abort": { - "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana" + "already_configured": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", + "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", + "import_failed": "Uvoz iz konfiguracije ni uspel" }, "error": { "certificate_error": "Certifikata ni bilo mogo\u010de preveriti", "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", + "connection_refused": "Povezava zavrnjena, ko ste se povezali z gostiteljem", "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti", diff --git a/homeassistant/components/coronavirus/.translations/ca.json b/homeassistant/components/coronavirus/.translations/ca.json new file mode 100644 index 00000000000..43bd868d0c4 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/ca.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest pa\u00eds ja est\u00e0 configurat." + }, + "step": { + "user": { + "data": { + "country": "Pa\u00eds" + }, + "title": "Tria un pa\u00eds a monitoritzar" + } + }, + "title": "Coronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/da.json b/homeassistant/components/coronavirus/.translations/da.json new file mode 100644 index 00000000000..5f3dc09cf20 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/da.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Dette land er allerede konfigureret." + }, + "step": { + "user": { + "data": { + "country": "Land" + }, + "title": "V\u00e6lg et land at overv\u00e5ge" + } + }, + "title": "Coronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/de.json b/homeassistant/components/coronavirus/.translations/de.json new file mode 100644 index 00000000000..d3602540349 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/de.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Dieses Land ist bereits konfiguriert." + }, + "step": { + "user": { + "data": { + "country": "Land" + }, + "title": "W\u00e4hlen Sie ein Land aus, das \u00fcberwacht werden soll" + } + }, + "title": "Coronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/fr.json b/homeassistant/components/coronavirus/.translations/fr.json new file mode 100644 index 00000000000..923a4cdc819 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/fr.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Ce pays est d\u00e9j\u00e0 configur\u00e9." + }, + "step": { + "user": { + "data": { + "country": "Pays" + }, + "title": "Choisissez un pays \u00e0 surveiller" + } + }, + "title": "Coronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/it.json b/homeassistant/components/coronavirus/.translations/it.json new file mode 100644 index 00000000000..6fc6bd8f811 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/it.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Questa Nazione \u00e8 gi\u00e0 configurata." + }, + "step": { + "user": { + "data": { + "country": "Nazione" + }, + "title": "Scegliere una Nazione da monitorare" + } + }, + "title": "Coronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/ko.json b/homeassistant/components/coronavirus/.translations/ko.json new file mode 100644 index 00000000000..8c03db18527 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/ko.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \uad6d\uac00\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "data": { + "country": "\uad6d\uac00" + }, + "title": "\ubaa8\ub2c8\ud130\ub9c1 \ud560 \uad6d\uac00\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694" + } + }, + "title": "\ucf54\ub85c\ub098 \ubc14\uc774\ub7ec\uc2a4" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/pl.json b/homeassistant/components/coronavirus/.translations/pl.json new file mode 100644 index 00000000000..9862d924ca4 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/pl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Ten kraj jest ju\u017c skonfigurowany." + }, + "step": { + "user": { + "data": { + "country": "Kraj" + }, + "title": "Wybierz kraj do monitorowania" + } + }, + "title": "Koronawirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/sl.json b/homeassistant/components/coronavirus/.translations/sl.json new file mode 100644 index 00000000000..180de6d8c18 --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/sl.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Ta dr\u017eava je \u017ee nastavljena." + }, + "step": { + "user": { + "data": { + "country": "Dr\u017eava" + }, + "title": "Izberite dr\u017eavo za spremljanje" + } + }, + "title": "Koronavirus" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/ca.json b/homeassistant/components/cover/.translations/ca.json index bbff63722d2..1a0f0544698 100644 --- a/homeassistant/components/cover/.translations/ca.json +++ b/homeassistant/components/cover/.translations/ca.json @@ -2,7 +2,9 @@ "device_automation": { "action_type": { "close": "Tanca {entity_name}", + "close_tilt": "Inclinaci\u00f3 {entity_name} tancat/ada", "open": "Obre {entity_name}", + "open_tilt": "Inclinaci\u00f3 {entity_name} obert/a", "set_position": "Estableix la posici\u00f3 de {entity_name}", "set_tilt_position": "Estableix la inclinaci\u00f3 de {entity_name}" }, diff --git a/homeassistant/components/cover/.translations/da.json b/homeassistant/components/cover/.translations/da.json index 64b89be5267..29691b4154b 100644 --- a/homeassistant/components/cover/.translations/da.json +++ b/homeassistant/components/cover/.translations/da.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "Luk {entity_name}", + "close_tilt": "Luk vippeposition for {entity_name}", + "open": "\u00c5bn {entity_name}", + "open_tilt": "\u00c5bn vippeposition for {entity_name}", + "set_position": "Indstil {entity_name}-position", + "set_tilt_position": "Angiv vippeposition for {entity_name}" + }, "condition_type": { "is_closed": "{entity_name} er lukket", "is_closing": "{entity_name} lukker", diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json index 24589c733b8..c40a7d074f5 100644 --- a/homeassistant/components/cover/.translations/de.json +++ b/homeassistant/components/cover/.translations/de.json @@ -1,5 +1,9 @@ { "device_automation": { + "action_type": { + "close": "Schlie\u00dfe {entity_name}", + "open": "\u00d6ffne {entity_name}" + }, "condition_type": { "is_closed": "{entity_name} ist geschlossen", "is_closing": "{entity_name} wird geschlossen", diff --git a/homeassistant/components/cover/.translations/fr.json b/homeassistant/components/cover/.translations/fr.json index 3aa877637d9..83bd5df826e 100644 --- a/homeassistant/components/cover/.translations/fr.json +++ b/homeassistant/components/cover/.translations/fr.json @@ -1,5 +1,8 @@ { "device_automation": { + "action_type": { + "close": "Fermer {entity_name}" + }, "condition_type": { "is_closed": "{entity_name} est ferm\u00e9", "is_closing": "{entity_name} se ferme", diff --git a/homeassistant/components/cover/.translations/ko.json b/homeassistant/components/cover/.translations/ko.json index 145938b6f24..ae67663f46f 100644 --- a/homeassistant/components/cover/.translations/ko.json +++ b/homeassistant/components/cover/.translations/ko.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "{entity_name} \ub2eb\uae30", + "close_tilt": "{entity_name} \ub2eb\uae30", + "open": "{entity_name} \uc5f4\uae30", + "open_tilt": "{entity_name} \uc5f4\uae30", + "set_position": "{entity_name} \uac1c\ud3d0 \uc704\uce58 \uc124\uc815\ud558\uae30", + "set_tilt_position": "{entity_name} \uac1c\ud3d0 \uae30\uc6b8\uae30 \uc124\uc815\ud558\uae30" + }, "condition_type": { "is_closed": "{entity_name} \uc774(\uac00) \ub2eb\ud600 \uc788\uc73c\uba74", "is_closing": "{entity_name} \uc774(\uac00) \ub2eb\ud788\ub294 \uc911\uc774\uba74", diff --git a/homeassistant/components/cover/.translations/pl.json b/homeassistant/components/cover/.translations/pl.json index 718c4b86fbd..ce035b2533e 100644 --- a/homeassistant/components/cover/.translations/pl.json +++ b/homeassistant/components/cover/.translations/pl.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "zamknij {entity_name}", + "close_tilt": "zamknij pochylenie {entity_name}", + "open": "otw\u00f3rz {entity_name}", + "open_tilt": "otw\u00f3rz {entity_name} do pochylenia", + "set_position": "ustaw pozycj\u0119 {entity_name}", + "set_tilt_position": "ustaw pochylenie {entity_name}" + }, "condition_type": { "is_closed": "pokrywa {entity_name} jest zamkni\u0119ta", "is_closing": "{entity_name} si\u0119 zamyka", diff --git a/homeassistant/components/cover/.translations/sl.json b/homeassistant/components/cover/.translations/sl.json index cd3570d39ba..818f17d58fe 100644 --- a/homeassistant/components/cover/.translations/sl.json +++ b/homeassistant/components/cover/.translations/sl.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "Zapri {entity_name}", + "close_tilt": "Zapri {entity_name} nagib", + "open": "Odprite {entity_name}", + "open_tilt": "Odprite {entity_name} nagib", + "set_position": "Nastavite polo\u017eaj {entity_name}", + "set_tilt_position": "Nastavite {entity_name} nagibni polo\u017eaj" + }, "condition_type": { "is_closed": "{entity_name} je/so zaprt/a", "is_closing": "{entity_name} se zapira/jo", diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 214c887cc34..60c18217aef 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -62,20 +62,20 @@ "side_5": "Face 5", "side_6": "Face 6", "turn_off": "\u00c9teint", - "turn_on": "Allum\u00e9" + "turn_on": "Allumer" }, "trigger_type": { "remote_awakened": "Appareil r\u00e9veill\u00e9", - "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", - "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", + "remote_button_double_press": "Double clic sur le bouton \" {subtype} \"", + "remote_button_long_press": "Appuyer en continu sur le bouton \" {subtype} \"", "remote_button_long_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9 apr\u00e8s appui long", - "remote_button_quadruple_press": "Bouton \"{subtype}\" quadruple cliqu\u00e9", - "remote_button_quintuple_press": "Bouton \"{subtype}\" quintuple cliqu\u00e9", + "remote_button_quadruple_press": "Quadruple clic sur le bouton \" {subtype} \"", + "remote_button_quintuple_press": "Quintuple clic sur le bouton \" {subtype} \"", "remote_button_rotated": "Bouton \"{subtype}\" tourn\u00e9", "remote_button_rotation_stopped": "La rotation du bouton \" {subtype} \" s'est arr\u00eat\u00e9e", "remote_button_short_press": "Bouton \"{subtype}\" appuy\u00e9", "remote_button_short_release": "Bouton \"{subtype}\" rel\u00e2ch\u00e9", - "remote_button_triple_press": "Bouton \"{subtype}\" triple cliqu\u00e9", + "remote_button_triple_press": "Triple clic sur le bouton \" {subtype} \"", "remote_double_tap": "Appareil \"{subtype}\" tapot\u00e9 deux fois", "remote_double_tap_any_side": "Appareil double tap\u00e9 de n\u2019importe quel c\u00f4t\u00e9", "remote_falling": "Appareil en chute libre", diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index 385de6f0f01..d8d98c103c3 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" }, - "description": "Konfiguracija vidnosti tipov naprav deCONZ" + "description": "Konfiguracija vidnosti tipov naprav deCONZ", + "title": "mo\u017enosti deCONZ" } } } diff --git a/homeassistant/components/demo/.translations/de.json b/homeassistant/components/demo/.translations/de.json index a600790d2fc..658a39246d9 100644 --- a/homeassistant/components/demo/.translations/de.json +++ b/homeassistant/components/demo/.translations/de.json @@ -4,6 +4,12 @@ }, "options": { "step": { + "init": { + "data": { + "one": "eins", + "other": "andere" + } + }, "options_1": { "data": { "bool": "Optionaler Boolescher Wert", diff --git a/homeassistant/components/demo/.translations/es.json b/homeassistant/components/demo/.translations/es.json index 73ed9809d65..9fd9b61dda1 100644 --- a/homeassistant/components/demo/.translations/es.json +++ b/homeassistant/components/demo/.translations/es.json @@ -4,6 +4,12 @@ }, "options": { "step": { + "init": { + "data": { + "one": "Vacio", + "other": "Vacio" + } + }, "options_1": { "data": { "bool": "Booleano opcional", diff --git a/homeassistant/components/demo/.translations/it.json b/homeassistant/components/demo/.translations/it.json index 7b299913c8e..1173cc48e04 100644 --- a/homeassistant/components/demo/.translations/it.json +++ b/homeassistant/components/demo/.translations/it.json @@ -7,7 +7,7 @@ "init": { "data": { "one": "uno", - "other": "altro" + "other": "altri" } }, "options_1": { diff --git a/homeassistant/components/directv/.translations/ca.json b/homeassistant/components/directv/.translations/ca.json new file mode 100644 index 00000000000..a88e1feb2e9 --- /dev/null +++ b/homeassistant/components/directv/.translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El receptor DirecTV ja est\u00e0 configurat", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "unknown": "Error inesperat" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Vols configurar {name}?", + "title": "Connexi\u00f3 amb el receptor DirecTV" + }, + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP" + }, + "title": "Connexi\u00f3 amb el receptor DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/de.json b/homeassistant/components/directv/.translations/de.json new file mode 100644 index 00000000000..6482216a67c --- /dev/null +++ b/homeassistant/components/directv/.translations/de.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "already_configured": "Der DirecTV-Empf\u00e4nger ist bereits konfiguriert", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "data": { + "one": "eins", + "other": "andere" + } + }, + "user": { + "data": { + "host": "Host oder IP-Adresse" + }, + "title": "Schlie\u00dfen Sie den DirecTV-Empf\u00e4nger an" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/en.json b/homeassistant/components/directv/.translations/en.json index 667d5168f8d..f5105477c45 100644 --- a/homeassistant/components/directv/.translations/en.json +++ b/homeassistant/components/directv/.translations/en.json @@ -5,12 +5,12 @@ "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again" + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" }, "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { - "data": {}, "description": "Do you want to set up {name}?", "title": "Connect to the DirecTV receiver" }, @@ -23,4 +23,4 @@ }, "title": "DirecTV" } -} +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/es.json b/homeassistant/components/directv/.translations/es.json new file mode 100644 index 00000000000..ef9edd6dd73 --- /dev/null +++ b/homeassistant/components/directv/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El receptor DirecTV ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "unknown": "Error inesperado" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "\u00bfQuieres configurar {name}?", + "title": "Conectar con el receptor DirecTV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "title": "Conectar con el receptor DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/fr.json b/homeassistant/components/directv/.translations/fr.json new file mode 100644 index 00000000000..6ba9237a3ad --- /dev/null +++ b/homeassistant/components/directv/.translations/fr.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Le r\u00e9cepteur DirecTV est d\u00e9j\u00e0 configur\u00e9", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Voulez-vous configurer {name} ?", + "title": "Connectez-vous au r\u00e9cepteur DirecTV" + }, + "user": { + "data": { + "host": "H\u00f4te ou adresse IP" + }, + "title": "Connectez-vous au r\u00e9cepteur DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/it.json b/homeassistant/components/directv/.translations/it.json new file mode 100644 index 00000000000..4fe98fc6024 --- /dev/null +++ b/homeassistant/components/directv/.translations/it.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "Il ricevitore DirecTV \u00e8 gi\u00e0 configurato", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "data": { + "one": "uno", + "other": "altri" + }, + "description": "Vuoi impostare {name} ?", + "title": "Connettersi al ricevitore DirecTV" + }, + "user": { + "data": { + "host": "Host o indirizzo IP" + }, + "title": "Collegamento al ricevitore DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/ko.json b/homeassistant/components/directv/.translations/ko.json new file mode 100644 index 00000000000..46ad9b15e49 --- /dev/null +++ b/homeassistant/components/directv/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "DirecTV \ub9ac\uc2dc\ubc84\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "DirecTV \ub9ac\uc2dc\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + }, + "title": "DirecTV \ub9ac\uc2dc\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/lb.json b/homeassistant/components/directv/.translations/lb.json new file mode 100644 index 00000000000..3cd7d7e20cd --- /dev/null +++ b/homeassistant/components/directv/.translations/lb.json @@ -0,0 +1,8 @@ +{ + "config": { + "abort": { + "already_configured": "DirecTV ass scho konfigur\u00e9iert", + "unknown": "Onerwaarte Feeler" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/no.json b/homeassistant/components/directv/.translations/no.json new file mode 100644 index 00000000000..50f46fbb7bd --- /dev/null +++ b/homeassistant/components/directv/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "DirecTV-mottaker er allerede konfigurert", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Vil du sette opp {name} ?", + "title": "Koble til DirecTV-mottakeren" + }, + "user": { + "data": { + "host": "Vert eller IP-adresse" + }, + "title": "Koble til DirecTV-mottakeren" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/pl.json b/homeassistant/components/directv/.translations/pl.json new file mode 100644 index 00000000000..81305324f5e --- /dev/null +++ b/homeassistant/components/directv/.translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "data": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "description": "Czy chcesz skonfigurowa\u0107 {name}?", + "title": "Po\u0142\u0105cz si\u0119 z odbiornikiem DirecTV" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "title": "Po\u0142\u0105cz si\u0119 z odbiornikiem DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/ru.json b/homeassistant/components/directv/.translations/ru.json new file mode 100644 index 00000000000..7fc53b8b8ea --- /dev/null +++ b/homeassistant/components/directv/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "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 {name}?", + "title": "DirecTV" + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + }, + "title": "DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/sl.json b/homeassistant/components/directv/.translations/sl.json new file mode 100644 index 00000000000..ce3d6fac9eb --- /dev/null +++ b/homeassistant/components/directv/.translations/sl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Sprejemnik DirecTV je \u017ee konfiguriran", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "unknown": "Nepri\u010dakovana napaka" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "data": { + "few": "nekaj", + "one": "ena", + "other": "drugo", + "two": "dva" + }, + "description": "Ali \u017eelite nastaviti {name} ?", + "title": "Pove\u017eite se s sprejemnikom DirecTV" + }, + "user": { + "data": { + "host": "Gostitelj ali IP naslov" + }, + "title": "Pove\u017eite se s sprejemnikom DirecTV" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/directv/.translations/zh-Hant.json b/homeassistant/components/directv/.translations/zh-Hant.json new file mode 100644 index 00000000000..38b89b729ad --- /dev/null +++ b/homeassistant/components/directv/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "DirectTV \u63a5\u6536\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "DirecTV\uff1a{name}", + "step": { + "ssdp_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", + "title": "\u9023\u7dda\u81f3 DirecTV \u63a5\u6536\u5668" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + }, + "title": "\u9023\u7dda\u81f3 DirecTV \u63a5\u6536\u5668" + } + }, + "title": "DirecTV" + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/ca.json b/homeassistant/components/doorbird/.translations/ca.json new file mode 100644 index 00000000000..488481f9614 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/ca.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu DoorBird ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 (adre\u00e7a IP)", + "name": "Nom del dispositiu", + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Connexi\u00f3 amb DoorBird" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Llista d'esdeveniments separats per comes." + }, + "description": "Afegeix el/s noms del/s esdeveniment/s que vulguis seguir separats per comes. Despr\u00e9s d\u2019introduir-los, utilitzeu l\u2019aplicaci\u00f3 de DoorBird per assignar-los a un esdeveniment espec\u00edfic. Consulta la documentaci\u00f3 a https://www.home-assistant.io/integrations/doorbird/#events.\nExemple: algu_ha_premut_el_boto, moviment_detectat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/da.json b/homeassistant/components/doorbird/.translations/da.json new file mode 100644 index 00000000000..3e66091d851 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index caf3177c681..dc7c2fd0cbe 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -1,34 +1,34 @@ { - "options" : { - "step" : { - "init" : { - "data" : { - "events" : "Comma separated list of events." - }, - "description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" - } - } - }, - "config" : { - "step" : { - "user" : { - "title" : "Connect to the DoorBird", - "data" : { - "password" : "Password", - "host" : "Host (IP Address)", - "name" : "Device Name", - "username" : "Username" + "config": { + "abort": { + "already_configured": "This DoorBird is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host (IP Address)", + "name": "Device Name", + "password": "Password", + "username": "Username" + }, + "title": "Connect to the DoorBird" } - } - }, - "abort" : { - "already_configured" : "This DoorBird is already configured" - }, - "title" : "DoorBird", - "error" : { - "invalid_auth" : "Invalid authentication", - "unknown" : "Unexpected error", - "cannot_connect" : "Failed to connect, please try again" - } - } -} + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Comma separated list of events." + }, + "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/es.json b/homeassistant/components/doorbird/.translations/es.json new file mode 100644 index 00000000000..a7cddd18582 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/es.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "DoorBird ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "host": "Host (Direcci\u00f3n IP)", + "name": "Nombre del dispositivo", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a DoorBird" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Lista de eventos separados por comas." + }, + "description": "Agregue un nombre de evento separado por comas para cada evento del que desee realizar un seguimiento. Despu\u00e9s de introducirlos aqu\u00ed, utilice la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulte la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, movimiento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/fr.json b/homeassistant/components/doorbird/.translations/fr.json new file mode 100644 index 00000000000..4090d94099b --- /dev/null +++ b/homeassistant/components/doorbird/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Ce DoorBird est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "host": "H\u00f4te (adresse IP)", + "name": "Nom de l'appareil", + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Connectez-vous au DoorBird" + } + }, + "title": "DoorBird" + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/ko.json b/homeassistant/components/doorbird/.translations/ko.json new file mode 100644 index 00000000000..121262065fd --- /dev/null +++ b/homeassistant/components/doorbird/.translations/ko.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 (IP \uc8fc\uc18c)", + "name": "\uae30\uae30 \uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "DoorBird \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "\uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \ubaa9\ub85d." + }, + "description": "\ucd94\uc801\ud558\ub824\ub294 \uac01 \uc774\ubca4\ud2b8\uc5d0 \ub300\ud574 \uc27c\ud45c\ub85c \uad6c\ubd84\ub41c \uc774\ubca4\ud2b8 \uc774\ub984\uc744 \ucd94\uac00\ud574\uc8fc\uc138\uc694. \uc5ec\uae30\uc5d0 \uc785\ub825\ud55c \ud6c4 DoorBird \uc571\uc744 \uc0ac\uc6a9\ud558\uc5ec \ud2b9\uc815 \uc774\ubca4\ud2b8\uc5d0 \ud560\ub2f9\ud574\uc8fc\uc138\uc694. \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 https://www.home-assistant.io/integrations/doorbird/#event \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694. \uc608: someone_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/lb.json b/homeassistant/components/doorbird/.translations/lb.json new file mode 100644 index 00000000000..e5a7322a59e --- /dev/null +++ b/homeassistant/components/doorbird/.translations/lb.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse DoorBird ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "host": "Numm (IP Adresse)", + "name": "Numm vum Apparat", + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mat DoorBird verbannen" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Komma getrennte L\u00ebscht vun Evenementer" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/no.json b/homeassistant/components/doorbird/.translations/no.json new file mode 100644 index 00000000000..91784f0b42a --- /dev/null +++ b/homeassistant/components/doorbird/.translations/no.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Denne DoorBird er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "host": "Vert (IP-adresse)", + "name": "Enhetsnavn", + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Koble til DoorBird" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Kommaseparert liste over hendelser." + }, + "description": "Legg til et kommaseparert hendelsesnavn for hvert arrangement du \u00f8nsker \u00e5 spore. Etter \u00e5 ha skrevet dem inn her, bruker du DoorBird-appen til \u00e5 tilordne dem til en bestemt hendelse. Se dokumentasjonen p\u00e5 https://www.home-assistant.io/integrations/doorbird/#events. Eksempel: noen_trykket_knappen, bevegelse" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/ru.json b/homeassistant/components/doorbird/.translations/ru.json new file mode 100644 index 00000000000..bca45c773b6 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/ru.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a DoorBird" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "\u0421\u043f\u0438\u0441\u043e\u043a \u0441\u043e\u0431\u044b\u0442\u0438\u0439 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e." + }, + "description": "\u0414\u043e\u0431\u0430\u0432\u044c\u0442\u0435 \u0447\u0435\u0440\u0435\u0437 \u0437\u0430\u043f\u044f\u0442\u0443\u044e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u044f \u0441\u043e\u0431\u044b\u0442\u0438\u0439, \u043a\u043e\u0442\u043e\u0440\u043e\u0435 \u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0442\u044c. \u041f\u043e\u0441\u043b\u0435 \u044d\u0442\u043e\u0433\u043e, \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0439\u0442\u0435 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0435 DoorBird, \u0447\u0442\u043e\u0431\u044b \u043d\u0430\u0437\u043d\u0430\u0447\u0438\u0442\u044c \u0438\u0445 \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u043e\u043c\u0443 \u0441\u043e\u0431\u044b\u0442\u0438\u044e. \u041f\u0440\u0438\u043c\u0435\u0440: somebody_pressed_the_button, motion. \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 \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: https://www.home-assistant.io/integrations/doorbird/#events." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/zh-Hant.json b/homeassistant/components/doorbird/.translations/zh-Hant.json new file mode 100644 index 00000000000..afeded494c6 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/zh-Hant.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64 DoorBird \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "host": "\u4e3b\u6a5f\uff08IP \u4f4d\u5740\uff09", + "name": "\u8a2d\u5099\u540d\u7a31", + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3 DoorBird" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "\u4ee5\u9017\u865f\u5206\u5225\u4e8b\u4ef6\u5217\u8868\u3002" + }, + "description": "\u4ee5\u9017\u865f\u5206\u5225\u6240\u8981\u8ffd\u8e64\u7684\u4e8b\u4ef6\u540d\u7a31\u3002\u65bc\u6b64\u8f38\u5165\u5f8c\uff0c\u4f7f\u7528 DoorBird App \u6307\u5b9a\u81f3\u7279\u5b9a\u4e8b\u4ef6\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\uff1ahttps://www.home-assistant.io/integrations/doorbird/#events\u3002\u4f8b\u5982\uff1asomebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/ca.json b/homeassistant/components/freebox/.translations/ca.json new file mode 100644 index 00000000000..0abfc0ef52b --- /dev/null +++ b/homeassistant/components/freebox/.translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat" + }, + "error": { + "connection_failed": "No s'ha pogut connectar, torna-ho a provar", + "register_failed": "No s'ha pogut registrar, torna-ho a provar", + "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard" + }, + "step": { + "link": { + "description": "Prem \"Envia\", a continuaci\u00f3, toca la fletxa dreta del router per registrar Freebox amb Home Assistant.\n\n![Ubicaci\u00f3 del boto del router](/static/images/config_freebox.png)", + "title": "Enlla\u00e7 amb router Freebox" + }, + "user": { + "data": { + "host": "Amfitri\u00f3", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/de.json b/homeassistant/components/freebox/.translations/de.json new file mode 100644 index 00000000000..7b8b417634a --- /dev/null +++ b/homeassistant/components/freebox/.translations/de.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Host bereits konfiguriert" + }, + "error": { + "connection_failed": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "register_failed": "Registrieren fehlgeschlagen, bitte versuche es erneut", + "unknown": "Unbekannter Fehler: Bitte versuchen Sie es sp\u00e4ter erneut" + }, + "step": { + "link": { + "title": "Link Freebox Router" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/es.json b/homeassistant/components/freebox/.translations/es.json new file mode 100644 index 00000000000..2c073e1d044 --- /dev/null +++ b/homeassistant/components/freebox/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "connection_failed": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "register_failed": "No se pudo registrar, int\u00e9ntalo de nuevo", + "unknown": "Error desconocido: por favor, int\u00e9ntalo de nuevo m\u00e1s" + }, + "step": { + "link": { + "description": "Pulsa \"Enviar\", despu\u00e9s pulsa en la flecha derecha en el router para registrar Freebox con Home Assistant\n\n![Localizaci\u00f3n del bot\u00f3n en el router](/static/images/config_freebox.png)", + "title": "Enlazar router Freebox" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/fr.json b/homeassistant/components/freebox/.translations/fr.json new file mode 100644 index 00000000000..6a91abc7076 --- /dev/null +++ b/homeassistant/components/freebox/.translations/fr.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "H\u00f4te d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connection_failed": "Impossible de se connecter, veuillez r\u00e9essayer", + "register_failed": "\u00c9chec de l'inscription, veuillez r\u00e9essayer", + "unknown": "Erreur inconnue: veuillez r\u00e9essayer plus tard" + }, + "step": { + "link": { + "description": "Cliquez sur \u00abSoumettre\u00bb, puis appuyez sur la fl\u00e8che droite du routeur pour enregistrer Freebox avec Home Assistant. \n\n ! [Emplacement du bouton sur le routeur](/static/images/config_freebox.png)" + }, + "user": { + "data": { + "host": "H\u00f4te", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/it.json b/homeassistant/components/freebox/.translations/it.json new file mode 100644 index 00000000000..6624167722b --- /dev/null +++ b/homeassistant/components/freebox/.translations/it.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Host gi\u00e0 configurato" + }, + "error": { + "connection_failed": "Impossibile connettersi, si prega di riprovare", + "register_failed": "Errore in fase di registrazione, si prega di riprovare", + "unknown": "Errore sconosciuto: riprovare pi\u00f9 tardi" + }, + "step": { + "link": { + "description": "Fare clic su \"Invia\", quindi toccare la freccia destra sul router per registrare Freebox con Home Assistant.\n\n![Posizione del pulsante sul router](/static/images/config_freebox.png)", + "title": "Collega il router Freebox" + }, + "user": { + "data": { + "host": "Host", + "port": "Porta" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/ko.json b/homeassistant/components/freebox/.translations/ko.json new file mode 100644 index 00000000000..56c91ad824c --- /dev/null +++ b/homeassistant/components/freebox/.translations/ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "connection_failed": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" + }, + "step": { + "link": { + "description": "\"Submit\" \uc744 \ud074\ub9ad\ud55c \ub2e4\uc74c \ub77c\uc6b0\ud130\uc758 \uc624\ub978\ucabd \ud654\uc0b4\ud45c\ub97c \ud130\uce58\ud558\uc5ec Home Assistant \uc5d0 Freebox \ub97c \ub4f1\ub85d\ud574\uc8fc\uc138\uc694.\n\n![\ub77c\uc6b0\ud130\uc758 \ubc84\ud2bc \uc704\uce58](/static/images/config_freebox.png)", + "title": "Freebox \ub77c\uc6b0\ud130 \uc5f0\uacb0" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8", + "port": "\ud3ec\ud2b8" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/no.json b/homeassistant/components/freebox/.translations/no.json new file mode 100644 index 00000000000..a87c902b70a --- /dev/null +++ b/homeassistant/components/freebox/.translations/no.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Verten er allerede konfigurert" + }, + "error": { + "connection_failed": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "register_failed": "Registrering feilet, vennligst pr\u00f8v igjen", + "unknown": "Ukjent feil: pr\u00f8v p\u00e5 nytt senere" + }, + "step": { + "link": { + "description": "Klikk p\u00e5 \"Submit\", deretter trykker du p\u00e5 den h\u00f8yre pilen p\u00e5 ruteren for \u00e5 registrere Freebox med Home Assistent.\n\n![Plasseringen av knappen p\u00e5 ruteren](/statisk/bilder/config_freebox.png)", + "title": "Link Freebox-ruter" + }, + "user": { + "data": { + "host": "Vert", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/pl.json b/homeassistant/components/freebox/.translations/pl.json new file mode 100644 index 00000000000..40fe7f097f1 --- /dev/null +++ b/homeassistant/components/freebox/.translations/pl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Host jest ju\u017c skonfigurowany." + }, + "error": { + "connection_failed": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "register_failed": "Nie uda\u0142o si\u0119 zarejestrowa\u0107. Spr\u00f3buj ponownie.", + "unknown": "Nieznany b\u0142\u0105d, spr\u00f3buj ponownie p\u00f3\u017aniej." + }, + "step": { + "link": { + "description": "Kliknij \"Prze\u015blij\", a nast\u0119pnie naci\u015bnij przycisk strza\u0142ki w prawo na routerze, aby zarejestrowa\u0107 Freebox w Home Assistan'cie. \n\n ![Lokalizacja przycisku na routerze] (/static/images/config_freebox.png)", + "title": "Po\u0142\u0105cz z routerem Freebox" + }, + "user": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/ru.json b/homeassistant/components/freebox/.translations/ru.json new file mode 100644 index 00000000000..4d4fdcc650d --- /dev/null +++ b/homeassistant/components/freebox/.translations/ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "connection_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "register_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0441\u043d\u043e\u0432\u0430.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435." + }, + "step": { + "link": { + "description": "\u041d\u0430\u0436\u043c\u0438\u0442\u0435 '\u041f\u043e\u0434\u0442\u0432\u0435\u0440\u0434\u0438\u0442\u044c', \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u0441\u043e \u0441\u0442\u0440\u0435\u043b\u043a\u043e\u0439 \u043d\u0430 \u043c\u0430\u0440\u0448\u0440\u0443\u0442\u0438\u0437\u0430\u0442\u043e\u0440\u0435, \u0447\u0442\u043e\u0431\u044b \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c Freebox \u0432 Home Assistant. \n\n ![\u0420\u0430\u0441\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u043a\u043d\u043e\u043f\u043a\u0438 \u043d\u0430 \u0440\u043e\u0443\u0442\u0435\u0440\u0435](/static/images/config_freebox.png)", + "title": "Freebox" + }, + "user": { + "data": { + "host": "\u0425\u043e\u0441\u0442", + "port": "\u041f\u043e\u0440\u0442" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/sl.json b/homeassistant/components/freebox/.translations/sl.json new file mode 100644 index 00000000000..e9865b9bf1e --- /dev/null +++ b/homeassistant/components/freebox/.translations/sl.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Gostitelj je \u017ee konfiguriran" + }, + "error": { + "connection_failed": "Povezava ni uspela, poskusite znova", + "register_failed": "Registracija ni uspela, poskusite znova", + "unknown": "Neznana napaka: poskusite pozneje" + }, + "step": { + "link": { + "description": "Kliknite \u00bbPo\u0161lji\u00ab, nato pa se dotaknite desne pu\u0161\u010dice na usmerjevalniku, \u010de \u017eelite registrirati Freebox pri programu Home Assistant. \n\n ! [Lokacija gumba na usmerjevalniku] (/static/images/config_freebox.png)", + "title": "Povezava usmerjevalnika Freebox" + }, + "user": { + "data": { + "host": "Gostitelj", + "port": "Vrata" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/zh-Hant.json b/homeassistant/components/freebox/.translations/zh-Hant.json new file mode 100644 index 00000000000..38da7b96e03 --- /dev/null +++ b/homeassistant/components/freebox/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "connection_failed": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "register_failed": "\u8a3b\u518a\u5931\u6557\uff0c\u8acb\u7a0d\u5f8c\u518d\u8a66", + "unknown": "\u672a\u77e5\u932f\u8aa4\uff1a\u8acb\u7a0d\u5f8c\u518d\u8a66" + }, + "step": { + "link": { + "description": "\u6309\u4e0b\u50b3\u9001 \"Submit\"\u3001\u63a5\u8457\u6309\u4e0b\u8def\u7531\u5668\u4e0a\u7684\u53f3\u7bad\u982d\u4ee5\u5c07 Freebox \u8a3b\u518a\u81f3 Home Assistant\u3002\n\n![Location of button on the router](/static/images/config_freebox.png)", + "title": "\u9023\u7d50 Freebox \u8def\u7531\u5668" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u7aef", + "port": "\u901a\u8a0a\u57e0" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/ko.json b/homeassistant/components/garmin_connect/.translations/ko.json index 018a0a8d923..eb354821d3d 100644 --- a/homeassistant/components/garmin_connect/.translations/ko.json +++ b/homeassistant/components/garmin_connect/.translations/ko.json @@ -16,9 +16,9 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" }, "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", - "title": "Garmin \uc5f0\uacb0" + "title": "Garmin Connect" } }, - "title": "Garmin \uc5f0\uacb0" + "title": "Garmin Connect" } } \ No newline at end of file diff --git a/homeassistant/components/geonetnz_quakes/.translations/ca.json b/homeassistant/components/geonetnz_quakes/.translations/ca.json index 57ce2b4ee81..7a88d3d2c72 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ca.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ca.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada." + }, "error": { "identifier_exists": "Ubicaci\u00f3 ja registrada" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/de.json b/homeassistant/components/geonetnz_quakes/.translations/de.json index e5c2acf352c..4f5a5cde750 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/de.json +++ b/homeassistant/components/geonetnz_quakes/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Der Standort ist bereits konfiguriert." + }, "error": { "identifier_exists": "Standort bereits registriert" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/en.json b/homeassistant/components/geonetnz_quakes/.translations/en.json index 4143efcdf96..ed83ab49436 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/en.json +++ b/homeassistant/components/geonetnz_quakes/.translations/en.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Location is already configured." + }, "error": { "identifier_exists": "Location already registered" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json index f6f592675ab..f50823186c3 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/es.json +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, "error": { "identifier_exists": "Ubicaci\u00f3n ya registrada" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/fr.json b/homeassistant/components/geonetnz_quakes/.translations/fr.json index 74ae5541754..39aee7a6694 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/fr.json +++ b/homeassistant/components/geonetnz_quakes/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9." + }, "error": { "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/it.json b/homeassistant/components/geonetnz_quakes/.translations/it.json index 2a019aa39d9..7b65c27f161 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/it.json +++ b/homeassistant/components/geonetnz_quakes/.translations/it.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La posizione \u00e8 gi\u00e0 configurata." + }, "error": { "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json index 66a216149dd..04adb36e5d2 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ko.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, "error": { "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/lb.json b/homeassistant/components/geonetnz_quakes/.translations/lb.json index 2499befecbb..ea9d1682eda 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/lb.json +++ b/homeassistant/components/geonetnz_quakes/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Standuert ass scho konfigu\u00e9iert." + }, "error": { "identifier_exists": "Standuert ass scho registr\u00e9iert" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/no.json b/homeassistant/components/geonetnz_quakes/.translations/no.json index 40b695d6f51..df69f6a3913 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/no.json +++ b/homeassistant/components/geonetnz_quakes/.translations/no.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Plasseringen er allerede konfigurert." + }, "error": { "identifier_exists": "Beliggenhet allerede er registrert" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index bdd8f152d39..5de41e72ef6 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Lokalizacja jest ju\u017c skonfigurowana." + }, "error": { "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index dddb5c47bb9..e8bf8499be6 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, "error": { "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/sl.json b/homeassistant/components/geonetnz_quakes/.translations/sl.json index bdd05d33953..1176c08f453 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/sl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Lokacija je \u017ee nastavljena." + }, "error": { "identifier_exists": "Lokacija je \u017ee registrirana" }, diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json index 487ac9ea8c0..3d312978bb2 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u4f4d\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, "error": { "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" }, diff --git a/homeassistant/components/griddy/.translations/ca.json b/homeassistant/components/griddy/.translations/ca.json new file mode 100644 index 00000000000..17c550636e1 --- /dev/null +++ b/homeassistant/components/griddy/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Aquesta zona de c\u00e0rrega ja est\u00e0 configurada" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "loadzone": "Zona de c\u00e0rrega (Load Zone)" + }, + "description": "La teva zona de c\u00e0rrega (Load Zone) est\u00e0 al teu compte de Griddy v\u00e9s a \"Account > Meter > Load Zone\".", + "title": "Configuraci\u00f3 de la zona de c\u00e0rrega (Load Zone) de Griddy" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/da.json b/homeassistant/components/griddy/.translations/da.json new file mode 100644 index 00000000000..9bb36f00ba6 --- /dev/null +++ b/homeassistant/components/griddy/.translations/da.json @@ -0,0 +1,5 @@ +{ + "config": { + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/de.json b/homeassistant/components/griddy/.translations/de.json new file mode 100644 index 00000000000..44ef7989afe --- /dev/null +++ b/homeassistant/components/griddy/.translations/de.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Diese Ladezone ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "title": "Richten Sie Ihre Griddy Ladezone ein" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/en.json b/homeassistant/components/griddy/.translations/en.json index bedd85e7508..20b3fbe21eb 100644 --- a/homeassistant/components/griddy/.translations/en.json +++ b/homeassistant/components/griddy/.translations/en.json @@ -1,21 +1,21 @@ { - "config" : { - "error" : { - "cannot_connect" : "Failed to connect, please try again", - "unknown" : "Unexpected error" - }, - "title" : "Griddy", - "step" : { - "user" : { - "description" : "Your Load Zone is in your Griddy account under “Account > Meter > Load Zone.”", - "data" : { - "loadzone" : "Load Zone (Settlement Point)" - }, - "title" : "Setup your Griddy Load Zone" - } - }, - "abort" : { - "already_configured" : "This Load Zone is already configured" - } - } -} + "config": { + "abort": { + "already_configured": "This Load Zone is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "loadzone": "Load Zone (Settlement Point)" + }, + "description": "Your Load Zone is in your Griddy account under \u201cAccount > Meter > Load Zone.\u201d", + "title": "Setup your Griddy Load Zone" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/es.json b/homeassistant/components/griddy/.translations/es.json new file mode 100644 index 00000000000..891564ea4ec --- /dev/null +++ b/homeassistant/components/griddy/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Esta Zona de Carga ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "loadzone": "Zona de Carga (Punto del Asentamiento)" + }, + "description": "Tu Zona de Carga est\u00e1 en tu cuenta de Griddy en \"Account > Meter > Load Zone\"", + "title": "Configurar tu Zona de Carga de Griddy" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/fr.json b/homeassistant/components/griddy/.translations/fr.json new file mode 100644 index 00000000000..1e0c8c3e9ae --- /dev/null +++ b/homeassistant/components/griddy/.translations/fr.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Cette zone de chargement est d\u00e9j\u00e0 configur\u00e9e" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "loadzone": "Zone de charge (point d'\u00e9tablissement)" + } + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/it.json b/homeassistant/components/griddy/.translations/it.json new file mode 100644 index 00000000000..2aacd9a4bab --- /dev/null +++ b/homeassistant/components/griddy/.translations/it.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Questa Zona di Carico \u00e8 gi\u00e0 configurata" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "loadzone": "Zona di Carico (Punto di insediamento)" + }, + "description": "La tua Zona di Carico si trova nel tuo account Griddy in \"Account > Meter > Load zone\".", + "title": "Configurazione della Zona di Carico Griddy" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/ko.json b/homeassistant/components/griddy/.translations/ko.json new file mode 100644 index 00000000000..cc86f0a1b45 --- /dev/null +++ b/homeassistant/components/griddy/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "loadzone": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed (\uc815\uc0b0\uc810)" + }, + "description": "\uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed\uc740 Griddy \uacc4\uc815\uc758 \"Account > Meter > Load Zone\"\uc5d0\uc11c \ud655\uc778\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "Griddy \uc804\ub825 \uacf5\uae09 \uc9c0\uc5ed \uc124\uc815" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/lb.json b/homeassistant/components/griddy/.translations/lb.json new file mode 100644 index 00000000000..3ed4a9c550a --- /dev/null +++ b/homeassistant/components/griddy/.translations/lb.json @@ -0,0 +1,9 @@ +{ + "config": { + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/no.json b/homeassistant/components/griddy/.translations/no.json new file mode 100644 index 00000000000..838c6c23668 --- /dev/null +++ b/homeassistant/components/griddy/.translations/no.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Denne Load Zone er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "loadzone": "Load Zone (settlingspunkt)" + }, + "description": "Din Load Zone er p\u00e5 din Griddy-konto under \"Konto > M\u00e5ler > Lastesone.\"", + "title": "Sett opp din Griddy Load Zone" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/pl.json b/homeassistant/components/griddy/.translations/pl.json new file mode 100644 index 00000000000..57484e84d9f --- /dev/null +++ b/homeassistant/components/griddy/.translations/pl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ta strefa obci\u0105\u017cenia jest ju\u017c skonfigurowana." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "loadzone": "Strefa obci\u0105\u017cenia (punkt rozliczenia)" + }, + "description": "Twoja strefa obci\u0105\u017cenia znajduje si\u0119 na twoim koncie Griddy w sekcji \"Konto > Licznik > Strefa obci\u0105\u017cenia\".", + "title": "Konfigurowanie strefy obci\u0105\u017cenia Griddy" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/ru.json b/homeassistant/components/griddy/.translations/ru.json new file mode 100644 index 00000000000..6f03fecd58a --- /dev/null +++ b/homeassistant/components/griddy/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0439 \u0437\u043e\u043d\u044b \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "loadzone": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 (\u0440\u0430\u0441\u0447\u0435\u0442\u043d\u0430\u044f \u0442\u043e\u0447\u043a\u0430)" + }, + "description": "\u0417\u043e\u043d\u0430 \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0438 \u043d\u0430\u0445\u043e\u0434\u0438\u0442\u0441\u044f \u0432 \u0412\u0430\u0448\u0435\u0439 \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Griddy \u0432 \u0440\u0430\u0437\u0434\u0435\u043b\u0435 Account > Meter > Load Zone.", + "title": "Griddy" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/sl.json b/homeassistant/components/griddy/.translations/sl.json new file mode 100644 index 00000000000..1adbbe39f38 --- /dev/null +++ b/homeassistant/components/griddy/.translations/sl.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ta obremenitvena cona je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "loadzone": "Obremenitvena cona (poselitvena to\u010dka)" + }, + "description": "Va\u0161a obremenitvena cona je v va\u0161em ra\u010dunu Griddy pod \"Ra\u010dun > Merilnik > Nalo\u017ei cono.\"", + "title": "Nastavite svojo Griddy Load Cono" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/tr.json b/homeassistant/components/griddy/.translations/tr.json new file mode 100644 index 00000000000..d887b148658 --- /dev/null +++ b/homeassistant/components/griddy/.translations/tr.json @@ -0,0 +1,8 @@ +{ + "config": { + "error": { + "cannot_connect": "Ba\u011flant\u0131 kurulamad\u0131, l\u00fctfen tekrar deneyin", + "unknown": "Beklenmeyen hata" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/griddy/.translations/zh-Hant.json b/homeassistant/components/griddy/.translations/zh-Hant.json new file mode 100644 index 00000000000..d3918269d13 --- /dev/null +++ b/homeassistant/components/griddy/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u8ca0\u8f09\u5340\u57df\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "loadzone": "\u8ca0\u8f09\u5340\u57df\uff08\u5c45\u4f4f\u9ede\uff09" + }, + "description": "\u8ca0\u8f09\u5340\u57df\u986f\u793a\u65bc Griddy \u5e33\u865f\uff0c\u4f4d\u65bc \u201cAccount > Meter > Load Zone\u201d\u3002", + "title": "\u8a2d\u5b9a Griddy \u8ca0\u8f09\u5340\u57df" + } + }, + "title": "Griddy" + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/ca.json b/homeassistant/components/harmony/.translations/ca.json new file mode 100644 index 00000000000..75fded469a8 --- /dev/null +++ b/homeassistant/components/harmony/.translations/ca.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Vols configurar {name} ({host})?", + "title": "Configuraci\u00f3 de Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nom de l'amfitri\u00f3 o adre\u00e7a IP", + "name": "Nom del Hub" + }, + "title": "Configuraci\u00f3 de Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Activitat predeterminada a executar quan no se n\u2019especifica cap.", + "delay_secs": "Retard entre l\u2019enviament d\u2019ordres." + }, + "description": "Ajusta les opcions de Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/de.json b/homeassistant/components/harmony/.translations/de.json new file mode 100644 index 00000000000..84187ef1d52 --- /dev/null +++ b/homeassistant/components/harmony/.translations/de.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "M\u00f6chten Sie {name} ({host}) einrichten?", + "title": "Richten Sie den Logitech Harmony Hub ein" + }, + "user": { + "data": { + "host": "Hostname oder IP-Adresse", + "name": "Hub-Name" + }, + "title": "Richten Sie den Logitech Harmony Hub ein" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Die Standardaktivit\u00e4t, die ausgef\u00fchrt werden soll, wenn keine angegeben ist.", + "delay_secs": "Die Verz\u00f6gerung zwischen dem Senden von Befehlen." + }, + "description": "Passen Sie die Harmony Hub-Optionen an" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/en.json b/homeassistant/components/harmony/.translations/en.json index 8af5a5ada1a..697d5572373 100644 --- a/homeassistant/components/harmony/.translations/en.json +++ b/homeassistant/components/harmony/.translations/en.json @@ -1,37 +1,38 @@ { - "config": { - "title": "Logitech Harmony Hub", - "flow_title": "Logitech Harmony Hub {name}", - "step": { - "user": { - "title": "Setup Logitech Harmony Hub", - "data": { - "host": "Hostname or IP Address", - "name": "Hub Name" - } - }, - "link": { - "title": "Setup Logitech Harmony Hub", - "description": "Do you want to setup {name} ({host})?" - } + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Do you want to setup {name} ({host})?", + "title": "Setup Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Hostname or IP Address", + "name": "Hub Name" + }, + "title": "Setup Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" - } - }, - "options": { - "step": { - "init": { - "description": "Adjust Harmony Hub Options", - "data": { - "activity": "The default activity to execute when none is specified.", - "delay_secs": "The delay between sending commands." + "options": { + "step": { + "init": { + "data": { + "activity": "The default activity to execute when none is specified.", + "delay_secs": "The delay between sending commands." + }, + "description": "Adjust Harmony Hub Options" + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/es.json b/homeassistant/components/harmony/.translations/es.json new file mode 100644 index 00000000000..f8e8bd9ea7e --- /dev/null +++ b/homeassistant/components/harmony/.translations/es.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "\u00bfQuiere configurar {name} ({host})?", + "title": "Configurar Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nombre del host o direcci\u00f3n IP", + "name": "Nombre del concentrador" + }, + "title": "Configurar Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "La actividad por defecto a ejecutar cuando no se especifica ninguna.", + "delay_secs": "El retraso entre el env\u00edo de comandos." + }, + "description": "Ajustar las opciones de Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/fr.json b/homeassistant/components/harmony/.translations/fr.json new file mode 100644 index 00000000000..e927254b9e2 --- /dev/null +++ b/homeassistant/components/harmony/.translations/fr.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Voulez-vous configurer {name} ( {host} ) ?", + "title": "Configuration de Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nom d'h\u00f4te ou adresse IP", + "name": "Nom du Hub" + }, + "title": "Configuration de Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Activit\u00e9 par d\u00e9faut \u00e0 ex\u00e9cuter lorsqu'aucune n'est sp\u00e9cifi\u00e9e.", + "delay_secs": "Le d\u00e9lai entre l'envoi des commandes." + }, + "description": "Ajuster les options du hub Harmony" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/it.json b/homeassistant/components/harmony/.translations/it.json new file mode 100644 index 00000000000..36d06ef565c --- /dev/null +++ b/homeassistant/components/harmony/.translations/it.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Vuoi impostare {name} ({host})?", + "title": "Impostazione di Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nome dell'host o indirizzo IP", + "name": "Nome Hub" + }, + "title": "Configurare Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "L'attivit\u00e0 predefinita da eseguire quando nessuna \u00e8 specificata.", + "delay_secs": "Il ritardo tra l'invio dei comandi." + }, + "description": "Regolare le opzioni di Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/ko.json b/homeassistant/components/harmony/.translations/ko.json new file mode 100644 index 00000000000..6106ce8a89d --- /dev/null +++ b/homeassistant/components/harmony/.translations/ko.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "{name} ({host}) \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Logitech Harmony Hub \uc124\uc815" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c", + "name": "Hub \uc774\ub984" + }, + "title": "Logitech Harmony Hub \uc124\uc815" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "\uc9c0\uc815\ub418\uc9c0 \uc54a\uc740 \uacbd\uc6b0 \uc2e4\ud589\ud560 \uae30\ubcf8 \uc561\uc158.", + "delay_secs": "\uba85\ub839 \uc804\uc1a1 \uc0ac\uc774\uc758 \uc9c0\uc5f0 \uc2dc\uac04." + }, + "description": "Harmony Hub \uc635\uc158 \uc870\uc815" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/lb.json b/homeassistant/components/harmony/.translations/lb.json new file mode 100644 index 00000000000..8401853fd57 --- /dev/null +++ b/homeassistant/components/harmony/.translations/lb.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Soll {name} ({host}) konfigur\u00e9iert ginn?", + "title": "Logitech Harmony Hub ariichten" + }, + "user": { + "data": { + "host": "Host Numm oder IP Adresse", + "name": "Numm vum Hub" + }, + "title": "Logitech Harmony Hub ariichten" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "delay_secs": "Delai zw\u00ebschen dem versch\u00e9cken vun Kommandoen" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/no.json b/homeassistant/components/harmony/.translations/no.json new file mode 100644 index 00000000000..2b102570d9a --- /dev/null +++ b/homeassistant/components/harmony/.translations/no.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Vil du konfigurere {name} ( {host} )?", + "title": "Oppsett Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Vertsnavn eller IP-adresse", + "name": "Navn p\u00e5 hub" + }, + "title": "Oppsett Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Standardaktiviteten som skal utf\u00f8res n\u00e5r ingen er angitt.", + "delay_secs": "Forsinkelsen mellom sending av kommandoer." + }, + "description": "Juster alternativene for harmonihub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/ru.json b/homeassistant/components/harmony/.translations/ru.json new file mode 100644 index 00000000000..cdeb809da12 --- /dev/null +++ b/homeassistant/components/harmony/.translations/ru.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name} ({host})?", + "title": "Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" + }, + "title": "Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "\u0410\u043a\u0442\u0438\u0432\u043d\u043e\u0441\u0442\u044c \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e, \u043a\u043e\u0433\u0434\u0430 \u043d\u0438 \u043e\u0434\u043d\u0430 \u0438\u0437 \u043d\u0438\u0445 \u043d\u0435 \u0443\u043a\u0430\u0437\u0430\u043d\u0430.", + "delay_secs": "\u0417\u0430\u0434\u0435\u0440\u0436\u043a\u0430 \u043c\u0435\u0436\u0434\u0443 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u043e\u0439 \u043a\u043e\u043c\u0430\u043d\u0434." + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u043e\u0432 Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/zh-Hant.json b/homeassistant/components/harmony/.translations/zh-Hant.json new file mode 100644 index 00000000000..7cdbad1a70d --- /dev/null +++ b/homeassistant/components/harmony/.translations/zh-Hant.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "\u7f85\u6280 Harmony Hub {name}", + "step": { + "link": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name} ({host})\uff1f", + "title": "\u8a2d\u5b9a\u7f85\u6280 Harmony Hub" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u540d\u6216 IP \u4f4d\u5740", + "name": "Hub \u540d\u7a31" + }, + "title": "\u8a2d\u5b9a\u7f85\u6280 Harmony Hub" + } + }, + "title": "\u7f85\u6280 Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "\u7576\u672a\u6307\u5b9a\u6642\u9810\u8a2d\u57f7\u884c\u6d3b\u52d5\u3002", + "delay_secs": "\u50b3\u9001\u547d\u4ee4\u9593\u9694\u79d2\u6578\u3002" + }, + "description": "\u8abf\u6574 Harmony Hub \u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/heos/.translations/ko.json b/homeassistant/components/heos/.translations/ko.json index 9237800bf48..e1cecbe35d9 100644 --- a/homeassistant/components/heos/.translations/ko.json +++ b/homeassistant/components/heos/.translations/ko.json @@ -13,7 +13,7 @@ "host": "\ud638\uc2a4\ud2b8" }, "description": "Heos \uae30\uae30\uc758 \ud638\uc2a4\ud2b8 \uc774\ub984 \ub610\ub294 IP \uc8fc\uc18c\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694. (\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c\ub85c \uc5f0\uacb0\ud558\ub294 \uac83\uc774 \uc88b\uc2b5\ub2c8\ub2e4)", - "title": "Heos \uc5f0\uacb0" + "title": "Heos \uc5d0 \uc5f0\uacb0\ud558\uae30" } }, "title": "HEOS" diff --git a/homeassistant/components/homekit_controller/.translations/sl.json b/homeassistant/components/homekit_controller/.translations/sl.json index 2af8a2a7ab5..aa7977f5bfe 100644 --- a/homeassistant/components/homekit_controller/.translations/sl.json +++ b/homeassistant/components/homekit_controller/.translations/sl.json @@ -6,7 +6,7 @@ "already_in_progress": "Konfiguracijski tok za to napravo je \u017ee v teku.", "already_paired": "Ta dodatna oprema je \u017ee povezana z drugo napravo. Ponastavite dodatno opremo in poskusite znova.", "ignored_model": "Podpora za HomeKit za ta model je blokirana, saj je na voljo ve\u010d funkcij popolne nativne integracije.", - "invalid_config_entry": "Ta naprava se prikazuje kot pripravljena za povezavo, vendar je konflikt v nastavitvah Home Assistant, ki ga je treba najprej odstraniti.", + "invalid_config_entry": "Ta naprava je prikazana kot pripravljena za seznanjanje, vendar je v programu Home Assistant zanj \u017ee vpisan konfliktni vnos konfiguracije, ki ga je treba najprej odstraniti.", "no_devices": "Ni bilo mogo\u010de najti neuparjenih naprav" }, "error": { @@ -14,7 +14,7 @@ "busy_error": "Naprava je zavrnila seznanjanje, saj se \u017ee povezuje z drugim krmilnikom.", "max_peers_error": "Naprava je zavrnila seznanjanje, saj nima prostega pomnilnika za seznanjanje.", "max_tries_error": "Napravaje zavrnila seznanjanje, saj je prejela ve\u010d kot 100 neuspe\u0161nih poskusov overjanja.", - "pairing_failed": "Pri poskusu seznanjanja s to napravo je pri\u0161lo do napake. To je lahko za\u010dasna napaka ali pa naprava trenutno ni podprta.", + "pairing_failed": "Med poskusom seznanitev s to napravo je pri\u0161lo do napake. To je lahko za\u010dasna napaka ali pa va\u0161a naprava trenutno ni podprta.", "unable_to_pair": "Ni mogo\u010de seznaniti. Poskusite znova.", "unknown_error": "Naprava je sporo\u010dila neznano napako. Seznanjanje ni uspelo." }, diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 99319f07ce4..7e837ca5ff9 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -12,7 +12,7 @@ }, "error": { "linking": "\uc54c \uc218 \uc5c6\ub294 \uc5f0\uacb0 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4.", - "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" + "register_failed": "\ub4f1\ub85d\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694" }, "step": { "init": { diff --git a/homeassistant/components/iaqualink/.translations/ko.json b/homeassistant/components/iaqualink/.translations/ko.json index 9b2519077e2..26bfa37d6be 100644 --- a/homeassistant/components/iaqualink/.translations/ko.json +++ b/homeassistant/components/iaqualink/.translations/ko.json @@ -13,7 +13,7 @@ "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 / \uc774\uba54\uc77c \uc8fc\uc18c" }, "description": "iAqualink \uacc4\uc815\uc758 \uc0ac\uc6a9\uc790 \uc774\ub984\uacfc \ube44\ubc00\ubc88\ud638\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", - "title": "iAqualink \uc5f0\uacb0" + "title": "iAqualink \uc5d0 \uc5f0\uacb0\ud558\uae30" } }, "title": "Jandy iAqualink" diff --git a/homeassistant/components/icloud/.translations/ca.json b/homeassistant/components/icloud/.translations/ca.json index aa8f8374124..c9e6f046d8a 100644 --- a/homeassistant/components/icloud/.translations/ca.json +++ b/homeassistant/components/icloud/.translations/ca.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "El compte ja ha estat configurat" + "already_configured": "El compte ja ha estat configurat", + "no_device": "Cap dels teus dispositius t\u00e9 activada la opci\u00f3 \"Troba el meu iPhone\"" }, "error": { "login": "Error d\u2019inici de sessi\u00f3: comprova el correu electr\u00f2nic i la contrasenya", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Contrasenya", - "username": "Correu electr\u00f2nic" + "username": "Correu electr\u00f2nic", + "with_family": "Amb fam\u00edlia" }, "description": "Introdueix les teves credencials", "title": "Credencials d'iCloud" diff --git a/homeassistant/components/icloud/.translations/da.json b/homeassistant/components/icloud/.translations/da.json index e60b5120a83..49d1a82a753 100644 --- a/homeassistant/components/icloud/.translations/da.json +++ b/homeassistant/components/icloud/.translations/da.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigureret" + "already_configured": "Kontoen er allerede konfigureret", + "no_device": "Ingen af dine enheder har aktiveret \"Find min iPhone\"" }, "error": { "login": "Loginfejl: Kontroller din email og adgangskode", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Adgangskode", - "username": "Email" + "username": "Email", + "with_family": "Med familien" }, "description": "Indtast dine legitimationsoplysninger", "title": "iCloud-legitimationsoplysninger" diff --git a/homeassistant/components/icloud/.translations/de.json b/homeassistant/components/icloud/.translations/de.json index c31f648a4ad..e317741a0a2 100644 --- a/homeassistant/components/icloud/.translations/de.json +++ b/homeassistant/components/icloud/.translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto bereits konfiguriert" + "already_configured": "Konto bereits konfiguriert", + "no_device": "Auf keinem Ihrer Ger\u00e4te ist \"Find my iPhone\" aktiviert" }, "error": { "login": "Login-Fehler: Bitte \u00fcberpr\u00fcfe deine E-Mail & Passwort", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Passwort", - "username": "E-Mail" + "username": "E-Mail", + "with_family": "Mit Familie" }, "description": "Gib deine Zugangsdaten ein", "title": "iCloud-Anmeldeinformationen" diff --git a/homeassistant/components/icloud/.translations/es.json b/homeassistant/components/icloud/.translations/es.json index 7a0d4b66047..02f07e5d492 100644 --- a/homeassistant/components/icloud/.translations/es.json +++ b/homeassistant/components/icloud/.translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Cuenta ya configurada" + "already_configured": "Cuenta ya configurada", + "no_device": "Ninguno de tus dispositivos tiene activado \"Buscar mi iPhone\"" }, "error": { "login": "Error de inicio de sesi\u00f3n: compruebe su direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Contrase\u00f1a", - "username": "Correo electr\u00f3nico" + "username": "Correo electr\u00f3nico", + "with_family": "Con la familia" }, "description": "Ingrese sus credenciales", "title": "Credenciales iCloud" diff --git a/homeassistant/components/icloud/.translations/fr.json b/homeassistant/components/icloud/.translations/fr.json index 91cff9912b6..e1a2517e4d7 100644 --- a/homeassistant/components/icloud/.translations/fr.json +++ b/homeassistant/components/icloud/.translations/fr.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9" + "already_configured": "Compte d\u00e9j\u00e0 configur\u00e9", + "no_device": "Aucun de vos appareils n'a activ\u00e9 \"Find my iPhone\"" }, "error": { "login": "Erreur de connexion: veuillez v\u00e9rifier votre e-mail et votre mot de passe", diff --git a/homeassistant/components/icloud/.translations/it.json b/homeassistant/components/icloud/.translations/it.json index 9d93a07565f..61cd4690179 100644 --- a/homeassistant/components/icloud/.translations/it.json +++ b/homeassistant/components/icloud/.translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account gi\u00e0 configurato" + "already_configured": "Account gi\u00e0 configurato", + "no_device": "Nessuno dei tuoi dispositivi ha attivato \"Trova il mio iPhone\"" }, "error": { "login": "Errore di accesso: si prega di controllare la tua e-mail e la password", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Password", - "username": "E-mail" + "username": "E-mail", + "with_family": "Con la famiglia" }, "description": "Inserisci le tue credenziali", "title": "Credenziali iCloud" diff --git a/homeassistant/components/icloud/.translations/ko.json b/homeassistant/components/icloud/.translations/ko.json index 10df5c4519c..8bc26c300e0 100644 --- a/homeassistant/components/icloud/.translations/ko.json +++ b/homeassistant/components/icloud/.translations/ko.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "already_configured": "\uacc4\uc815\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "no_device": "\"\ub098\uc758 iPhone \ucc3e\uae30\"\uac00 \ud65c\uc131\ud654\ub41c \uae30\uae30\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" }, "error": { "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc774\uba54\uc77c \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", @@ -19,7 +20,8 @@ "user": { "data": { "password": "\ube44\ubc00\ubc88\ud638", - "username": "\uc774\uba54\uc77c" + "username": "\uc774\uba54\uc77c", + "with_family": "\uac00\uc871\uc6a9" }, "description": "\uc790\uaca9 \uc99d\uba85\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694", "title": "iCloud \uc790\uaca9 \uc99d\uba85" diff --git a/homeassistant/components/icloud/.translations/no.json b/homeassistant/components/icloud/.translations/no.json index 589c220ec9c..3ba3207cc24 100644 --- a/homeassistant/components/icloud/.translations/no.json +++ b/homeassistant/components/icloud/.translations/no.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kontoen er allerede konfigurert" + "already_configured": "Kontoen er allerede konfigurert", + "no_device": "Ingen av enhetene dine har \"Finn min iPhone\" aktivert" }, "error": { "login": "Innloggingsfeil: vennligst sjekk e-postadressen og passordet ditt", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Passord", - "username": "E-post" + "username": "E-post", + "with_family": "Med familie" }, "description": "Angi legitimasjonsbeskrivelsen", "title": "iCloud-legitimasjon" diff --git a/homeassistant/components/icloud/.translations/pl.json b/homeassistant/components/icloud/.translations/pl.json index 41e182eceee..9c65891d261 100644 --- a/homeassistant/components/icloud/.translations/pl.json +++ b/homeassistant/components/icloud/.translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Konto jest ju\u017c skonfigurowane." + "already_configured": "Konto jest ju\u017c skonfigurowane.", + "no_device": "\u017badne z Twoich urz\u0105dze\u0144 nie ma aktywowanej funkcji \"Znajd\u017a m\u00f3j iPhone\"" }, "error": { "login": "B\u0142\u0105d logowania: sprawd\u017a adres e-mail i has\u0142o", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Has\u0142o", - "username": "E-mail" + "username": "E-mail", + "with_family": "Z rodzin\u0105" }, "description": "Wprowad\u017a dane uwierzytelniaj\u0105ce", "title": "Dane uwierzytelniaj\u0105ce iCloud" diff --git a/homeassistant/components/icloud/.translations/ru.json b/homeassistant/components/icloud/.translations/ru.json index b3a9578ad1e..b0869df14b1 100644 --- a/homeassistant/components/icloud/.translations/ru.json +++ b/homeassistant/components/icloud/.translations/ru.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430." + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430.", + "no_device": "\u041d\u0438 \u043d\u0430 \u043e\u0434\u043d\u043e\u043c \u0438\u0437 \u0412\u0430\u0448\u0438\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435 \u0430\u043a\u0442\u0438\u0432\u0438\u0440\u043e\u0432\u0430\u043d\u0430 \u0444\u0443\u043d\u043a\u0446\u0438\u044f \"\u041d\u0430\u0439\u0442\u0438 iPhone\"." }, "error": { "login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u0430\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", @@ -19,7 +20,8 @@ "user": { "data": { "password": "\u041f\u0430\u0440\u043e\u043b\u044c", - "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" + "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b", + "with_family": "\u0421 \u0441\u0435\u043c\u044c\u0451\u0439" }, "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0412\u0430\u0448\u0438 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", "title": "\u0423\u0447\u0435\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 iCloud" diff --git a/homeassistant/components/icloud/.translations/sl.json b/homeassistant/components/icloud/.translations/sl.json index 14d6168409c..6887eddde66 100644 --- a/homeassistant/components/icloud/.translations/sl.json +++ b/homeassistant/components/icloud/.translations/sl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Ra\u010dun \u017ee nastavljen" + "already_configured": "Ra\u010dun \u017ee nastavljen", + "no_device": "V nobeni od va\u0161ih naprav ni aktiviran \u00bbFind my iPhone\u00ab" }, "error": { "login": "Napaka pri prijavi: preverite svoj e-po\u0161tni naslov in geslo", @@ -19,7 +20,8 @@ "user": { "data": { "password": "Geslo", - "username": "E-po\u0161tni naslov" + "username": "E-po\u0161tni naslov", + "with_family": "Z dru\u017eino" }, "description": "Vnesite svoje poverilnice", "title": "iCloud poverilnice" diff --git a/homeassistant/components/icloud/.translations/tr.json b/homeassistant/components/icloud/.translations/tr.json new file mode 100644 index 00000000000..3d74852ce50 --- /dev/null +++ b/homeassistant/components/icloud/.translations/tr.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "no_device": "Hi\u00e7bir cihaz\u0131n\u0131zda \"iPhone'umu bul\" etkin de\u011fil" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/zh-Hant.json b/homeassistant/components/icloud/.translations/zh-Hant.json index a3f4e68e167..cdaff703be7 100644 --- a/homeassistant/components/icloud/.translations/zh-Hant.json +++ b/homeassistant/components/icloud/.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", + "no_device": "\u8a2d\u5099\u7686\u672a\u958b\u555f\u300c\u5c0b\u627e\u6211\u7684 iPhone\u300d\u529f\u80fd\u3002" }, "error": { "login": "\u767b\u5165\u932f\u8aa4\uff1a\u8acb\u78ba\u8a8d\u96fb\u5b50\u90f5\u4ef6\u8207\u79d8\u5bc6\u6b63\u78ba\u6027", @@ -19,7 +20,8 @@ "user": { "data": { "password": "\u5bc6\u78bc", - "username": "\u96fb\u5b50\u90f5\u4ef6" + "username": "\u96fb\u5b50\u90f5\u4ef6", + "with_family": "\u8207\u5bb6\u4eba\u5171\u4eab" }, "description": "\u8f38\u5165\u6191\u8b49", "title": "iCloud \u6191\u8b49" diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json index fa5b1f53dfb..ab29e9d1f08 100644 --- a/homeassistant/components/konnected/.translations/de.json +++ b/homeassistant/components/konnected/.translations/de.json @@ -11,10 +11,11 @@ }, "step": { "confirm": { - "description": "Modell: {model} \nHost: {host} \nPort: {port} \n\nSie k\u00f6nnen das I / O - und Bedienfeldverhalten in den Einstellungen der verbundenen Alarmzentrale konfigurieren.", + "description": "Modell: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nSie k\u00f6nnen das I / O - und Bedienfeldverhalten in den Einstellungen der verbundenen Alarmzentrale konfigurieren.", "title": "Konnected Device Bereit" }, "import_confirm": { + "description": "Ein Konnected Alarm Panel mit der ID {id} wurde in configuration.yaml entdeckt. Mit diesem Ablauf k\u00f6nnen Sie ihn in einen Konfigurationseintrag importieren.", "title": "Importieren von Konnected Ger\u00e4t" }, "user": { @@ -32,6 +33,10 @@ "abort": { "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t" }, + "error": { + "one": "eins", + "other": "andere" + }, "step": { "options_binary": { "data": { @@ -62,6 +67,7 @@ "7": "Zone 7", "out": "OUT" }, + "description": "Es wurde ein {model} bei {host} entdeckt. W\u00e4hlen Sie unten die Basiskonfiguration der einzelnen E / A aus. Je nach E / A k\u00f6nnen bin\u00e4re Sensoren (Kontakte \u00f6ffnen / schlie\u00dfen), digitale Sensoren (dht und ds18b20) oder umschaltbare Ausg\u00e4nge verwendet werden. In den n\u00e4chsten Schritten k\u00f6nnen Sie detaillierte Optionen konfigurieren.", "title": "Konfigurieren von I/O" }, "options_io_ext": { @@ -74,7 +80,9 @@ "alarm1": "ALARM1", "alarm2_out2": "OUT2/ALARM2", "out1": "OUT1" - } + }, + "description": "W\u00e4hlen Sie unten die Konfiguration der verbleibenden E / A. In den n\u00e4chsten Schritten k\u00f6nnen Sie detaillierte Optionen konfigurieren.", + "title": "Konfigurieren Sie Erweiterte I/O" }, "options_misc": { "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel" diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json index ed65b29a3b9..b6591b03d33 100644 --- a/homeassistant/components/konnected/.translations/es.json +++ b/homeassistant/components/konnected/.translations/es.json @@ -11,7 +11,7 @@ }, "step": { "confirm": { - "description": "Modelo: {model}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del panel de alarmas Konnected.", + "description": "Modelo: {model}\nID: {id}\nHost: {host}\nPuerto: {port}\n\nPuede configurar las E/S y el comportamiento del panel en los ajustes del Panel de Alarmas Konnected.", "title": "Dispositivo Konnected Listo" }, "import_confirm": { diff --git a/homeassistant/components/konnected/.translations/it.json b/homeassistant/components/konnected/.translations/it.json index 08b15e031a5..a79c2e0caf2 100644 --- a/homeassistant/components/konnected/.translations/it.json +++ b/homeassistant/components/konnected/.translations/it.json @@ -35,7 +35,7 @@ }, "error": { "one": "uno", - "other": "altro" + "other": "altri" }, "step": { "options_binary": { diff --git a/homeassistant/components/konnected/.translations/ko.json b/homeassistant/components/konnected/.translations/ko.json index fe196050766..0c5e213ea0d 100644 --- a/homeassistant/components/konnected/.translations/ko.json +++ b/homeassistant/components/konnected/.translations/ko.json @@ -11,7 +11,7 @@ }, "step": { "confirm": { - "description": "\ubaa8\ub378: {model}\n\ud638\uc2a4\ud2b8: {host}\n\ud3ec\ud2b8: {port}\n\nKonnected \uc54c\ub78c \ud328\ub110 \uc124\uc815\uc5d0\uc11c IO \uc640 \ud328\ub110 \ub3d9\uc791\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "description": "\ubaa8\ub378: {model}\nID: {id}\n\ud638\uc2a4\ud2b8: {host}\n\ud3ec\ud2b8: {port}\n\nKonnected \uc54c\ub78c \ud328\ub110 \uc124\uc815\uc5d0\uc11c IO \uc640 \ud328\ub110 \ub3d9\uc791\uc744 \uad6c\uc131\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "Konnected \uae30\uae30 \uc900\ube44" }, "import_confirm": { diff --git a/homeassistant/components/konnected/.translations/sl.json b/homeassistant/components/konnected/.translations/sl.json index 38396d0832d..2b2269bee5f 100644 --- a/homeassistant/components/konnected/.translations/sl.json +++ b/homeassistant/components/konnected/.translations/sl.json @@ -11,9 +11,13 @@ }, "step": { "confirm": { - "description": "Model: {model}\nGostitelj: {host}\nVrata: {port}\n\nV nastavitvah lahko nastavite vedenje I / O in plo\u0161\u010de Konnected alarma. ", + "description": "Model: {model}\nID: {id}\nGostitelj: {host}\nVrata: {port}\n\nV nastavitvah lahko nastavite vedenje I/O in plo\u0161\u010de Konnected alarma. ", "title": "Konnected naprava pripravljena" }, + "import_confirm": { + "description": "Konnected alarm panel z ID {id} je bil odkrit v konfiguraciji. YAML. To tok vam bo omogo\u010dil, da ga uvozite v va\u0161o konfiguracijo.", + "title": "Uvoz Konnected Naprave" + }, "user": { "data": { "host": "IP-naslov Konnected naprave", diff --git a/homeassistant/components/light/.translations/da.json b/homeassistant/components/light/.translations/da.json index eefa1e8bb6e..8115a3bfba9 100644 --- a/homeassistant/components/light/.translations/da.json +++ b/homeassistant/components/light/.translations/da.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Formindsk lysstyrken p\u00e5 {entity_name}", + "brightness_increase": "For\u00f8g lysstyrken p\u00e5 {entity_name}", "toggle": "Skift {entity_name}", "turn_off": "Sluk {entity_name}", "turn_on": "T\u00e6nd for {entity_name}" diff --git a/homeassistant/components/light/.translations/de.json b/homeassistant/components/light/.translations/de.json index be8966d9556..1984cf31d79 100644 --- a/homeassistant/components/light/.translations/de.json +++ b/homeassistant/components/light/.translations/de.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Helligkeit von {entity_name} verringern", + "brightness_increase": "Helligkeit von {entity_name} erh\u00f6hen", "toggle": "Schalte {entity_name} um.", "turn_off": "Schalte {entity_name} aus.", "turn_on": "Schalte {entity_name} ein." diff --git a/homeassistant/components/light/.translations/ko.json b/homeassistant/components/light/.translations/ko.json index b923fdb210e..c0c47dddfbb 100644 --- a/homeassistant/components/light/.translations/ko.json +++ b/homeassistant/components/light/.translations/ko.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "{entity_name} \uc744(\ub97c) \uc5b4\ub461\uac8c \ud558\uae30", + "brightness_increase": "{entity_name} \uc744(\ub97c) \ubc1d\uac8c \ud558\uae30", "toggle": "{entity_name} \ud1a0\uae00", "turn_off": "{entity_name} \ub044\uae30", "turn_on": "{entity_name} \ucf1c\uae30" diff --git a/homeassistant/components/light/.translations/pl.json b/homeassistant/components/light/.translations/pl.json index 05589210dba..1f2ff19f9c3 100644 --- a/homeassistant/components/light/.translations/pl.json +++ b/homeassistant/components/light/.translations/pl.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "zmniejsz jasno\u015b\u0107 {entity_name}", + "brightness_increase": "zwi\u0119ksz jasno\u015b\u0107 {entity_name}", "toggle": "prze\u0142\u0105cz {entity_name}", "turn_off": "wy\u0142\u0105cz {entity_name}", "turn_on": "w\u0142\u0105cz {entity_name}" diff --git a/homeassistant/components/light/.translations/sl.json b/homeassistant/components/light/.translations/sl.json index bef4f1583b6..5704ebb6826 100644 --- a/homeassistant/components/light/.translations/sl.json +++ b/homeassistant/components/light/.translations/sl.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "Zmanj\u0161ajte svetlost {entity_name}", + "brightness_increase": "Pove\u010dajte svetlost {entity_name}", "toggle": "Preklopite {entity_name}", "turn_off": "Izklopite {entity_name}", "turn_on": "Vklopite {entity_name}" diff --git a/homeassistant/components/media_player/.translations/ko.json b/homeassistant/components/media_player/.translations/ko.json index 49367eaf617..b7ebc93099d 100644 --- a/homeassistant/components/media_player/.translations/ko.json +++ b/homeassistant/components/media_player/.translations/ko.json @@ -1,7 +1,7 @@ { "device_automation": { "condition_type": { - "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734\uc0c1\ud0dc\uc774\uba74", + "is_idle": "{entity_name} \uc774(\uac00) \uc720\ud734 \uc0c1\ud0dc\uc774\uba74", "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", "is_paused": "{entity_name} \uc774(\uac00) \uc77c\uc2dc\uc911\uc9c0\ub418\uc5b4 \uc788\uc73c\uba74", diff --git a/homeassistant/components/melcloud/.translations/ko.json b/homeassistant/components/melcloud/.translations/ko.json index 1557abf5a32..428e2b1f994 100644 --- a/homeassistant/components/melcloud/.translations/ko.json +++ b/homeassistant/components/melcloud/.translations/ko.json @@ -15,7 +15,7 @@ "username": "MELCloud \ub85c\uadf8\uc778 \uc774\uba54\uc77c \uc8fc\uc18c\ub97c \ub123\uc5b4\uc8fc\uc138\uc694." }, "description": "MELCloud \uacc4\uc815\uc73c\ub85c \uc5f0\uacb0\ud558\uc138\uc694.", - "title": "MELCloud \uc5f0\uacb0" + "title": "MELCloud \uc5d0 \uc5f0\uacb0\ud558\uae30" } }, "title": "MELCloud" diff --git a/homeassistant/components/monoprice/.translations/ca.json b/homeassistant/components/monoprice/.translations/ca.json new file mode 100644 index 00000000000..b4cd7143fc9 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "port": "Port s\u00e8rie", + "source_1": "Nom de la font #1", + "source_2": "Nom de la font #2", + "source_3": "Nom de la font #3", + "source_4": "Nom de la font #4", + "source_5": "Nom de la font #5", + "source_6": "Nom de la font #6" + }, + "title": "Connexi\u00f3 amb el dispositiu" + } + }, + "title": "Amplificador Monoprice de 6 zones" + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/en.json b/homeassistant/components/monoprice/.translations/en.json new file mode 100644 index 00000000000..4ff655856f9 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/en.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "port": "Serial port", + "source_1": "Name of source #1", + "source_2": "Name of source #2", + "source_3": "Name of source #3", + "source_4": "Name of source #4", + "source_5": "Name of source #5", + "source_6": "Name of source #6" + }, + "title": "Connect to the device" + } + }, + "title": "Monoprice 6-Zone Amplifier" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Name of source #1", + "source_2": "Name of source #2", + "source_3": "Name of source #3", + "source_4": "Name of source #4", + "source_5": "Name of source #5", + "source_6": "Name of source #6" + }, + "title": "Configure sources" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/es.json b/homeassistant/components/monoprice/.translations/es.json new file mode 100644 index 00000000000..1996d116f76 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "port": "Puerto serie", + "source_1": "Nombre de la fuente #1", + "source_2": "Nombre de la fuente #2", + "source_3": "Nombre de la fuente #3", + "source_4": "Nombre de la fuente #4", + "source_5": "Nombre de la fuente #5", + "source_6": "Nombre de la fuente #6" + }, + "title": "Conectarse al dispositivo" + } + }, + "title": "Amplificador Monoprice de 6 zonas" + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/fr.json b/homeassistant/components/monoprice/.translations/fr.json new file mode 100644 index 00000000000..16ab039e347 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "port": "Port s\u00e9rie", + "source_1": "Nom de la source #1", + "source_2": "Nom de la source #2", + "source_3": "Nom de la source #3", + "source_4": "Nom de la source #4", + "source_5": "Nom de la source #5", + "source_6": "Nom de la source #6" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/ko.json b/homeassistant/components/monoprice/.translations/ko.json new file mode 100644 index 00000000000..dd5b44bf035 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/ko.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "port": "\uc2dc\ub9ac\uc5bc \ud3ec\ud2b8", + "source_1": "\uc785\ub825 \uc18c\uc2a4 1 \uc774\ub984", + "source_2": "\uc785\ub825 \uc18c\uc2a4 2 \uc774\ub984", + "source_3": "\uc785\ub825 \uc18c\uc2a4 3 \uc774\ub984", + "source_4": "\uc785\ub825 \uc18c\uc2a4 4 \uc774\ub984", + "source_5": "\uc785\ub825 \uc18c\uc2a4 5 \uc774\ub984", + "source_6": "\uc785\ub825 \uc18c\uc2a4 6 \uc774\ub984" + }, + "title": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Monoprice 6-Zone \uc570\ud504" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\uc785\ub825 \uc18c\uc2a4 1 \uc774\ub984", + "source_2": "\uc785\ub825 \uc18c\uc2a4 2 \uc774\ub984", + "source_3": "\uc785\ub825 \uc18c\uc2a4 3 \uc774\ub984", + "source_4": "\uc785\ub825 \uc18c\uc2a4 4 \uc774\ub984", + "source_5": "\uc785\ub825 \uc18c\uc2a4 5 \uc774\ub984", + "source_6": "\uc785\ub825 \uc18c\uc2a4 6 \uc774\ub984" + }, + "title": "\uc785\ub825 \uc18c\uc2a4 \uad6c\uc131" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/lb.json b/homeassistant/components/monoprice/.translations/lb.json new file mode 100644 index 00000000000..9b1ef75ef1d --- /dev/null +++ b/homeassistant/components/monoprice/.translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "port": "Serielle Port", + "source_1": "Numm vun der Quell #1", + "source_2": "Numm vun der Quell #2", + "source_3": "Numm vun der Quell #3", + "source_4": "Numm vun der Quell #4", + "source_5": "Numm vun der Quell #5", + "source_6": "Numm vun der Quell #6" + }, + "title": "Mam Apparat verbannen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/no.json b/homeassistant/components/monoprice/.translations/no.json new file mode 100644 index 00000000000..d4bf9a2ae9b --- /dev/null +++ b/homeassistant/components/monoprice/.translations/no.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "port": "Serial port", + "source_1": "Navn p\u00e5 kilden #1", + "source_2": "Navn p\u00e5 kilden #2", + "source_3": "Navn p\u00e5 kilden #3", + "source_4": "Navn p\u00e5 kilde #4", + "source_5": "Navn p\u00e5 kilde #5", + "source_6": "Navn p\u00e5 kilde #6" + }, + "title": "Koble til enheten" + } + }, + "title": "Monoprice 6-Zone Forsterker" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Navn p\u00e5 kilden #1", + "source_2": "Navn p\u00e5 kilden #2", + "source_3": "Navn p\u00e5 kilden #3", + "source_4": "Navn p\u00e5 kilde #4", + "source_5": "Navn p\u00e5 kilde #5", + "source_6": "Navn p\u00e5 kilde #6" + }, + "title": "Konfigurer kilder" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/ru.json b/homeassistant/components/monoprice/.translations/ru.json new file mode 100644 index 00000000000..25fa4ef7e64 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/ru.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "port": "\u041f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442", + "source_1": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #1", + "source_2": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #2", + "source_3": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #3", + "source_4": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #4", + "source_5": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #5", + "source_6": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #6" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + }, + "title": "Monoprice 6-Zone Amplifier" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #1", + "source_2": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #2", + "source_3": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #3", + "source_4": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #4", + "source_5": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #5", + "source_6": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u0430 #6" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/zh-Hant.json b/homeassistant/components/monoprice/.translations/zh-Hant.json new file mode 100644 index 00000000000..b37ab4158a0 --- /dev/null +++ b/homeassistant/components/monoprice/.translations/zh-Hant.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "port": "\u5e8f\u5217\u57e0", + "source_1": "\u4f86\u6e90 #1 \u540d\u7a31", + "source_2": "\u4f86\u6e90 #2 \u540d\u7a31", + "source_3": "\u4f86\u6e90 #3 \u540d\u7a31", + "source_4": "\u4f86\u6e90 #4 \u540d\u7a31", + "source_5": "\u4f86\u6e90 #5 \u540d\u7a31", + "source_6": "\u4f86\u6e90 #6 \u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3\u8a2d\u5099" + } + }, + "title": "Monoprice 6-Zone \u653e\u5927\u5668" + } +} \ No newline at end of file diff --git a/homeassistant/components/mqtt/.translations/sl.json b/homeassistant/components/mqtt/.translations/sl.json index 84553cc536a..86b72665b71 100644 --- a/homeassistant/components/mqtt/.translations/sl.json +++ b/homeassistant/components/mqtt/.translations/sl.json @@ -27,5 +27,27 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Prvi gumb", + "button_2": "Drugi gumb", + "button_3": "Tretji gumb", + "button_4": "\u010cetrti gumb", + "button_5": "Peti gumb", + "button_6": "\u0160esti gumb", + "turn_off": "Ugasni", + "turn_on": "Pri\u017egi" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" gumb dvakrat kliknjen", + "button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", + "button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", + "button_quadruple_press": "\"{subtype}\" gumb \u0161tirikrat kliknjen", + "button_quintuple_press": "\"{subtype}\" gumb petkrat kliknjen", + "button_short_press": "Pritisnjen \"{subtype}\" gumb", + "button_short_release": "Gumb \"{subtype}\" spro\u0161\u010den", + "button_triple_press": "Gumb \"{subtype}\" trikrat kliknjen" + } } } \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/ca.json b/homeassistant/components/myq/.translations/ca.json new file mode 100644 index 00000000000..ea8fd9c10de --- /dev/null +++ b/homeassistant/components/myq/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Connexi\u00f3 amb la passarel\u00b7la de MyQ" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/da.json b/homeassistant/components/myq/.translations/da.json new file mode 100644 index 00000000000..3e66091d851 --- /dev/null +++ b/homeassistant/components/myq/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/de.json b/homeassistant/components/myq/.translations/de.json new file mode 100644 index 00000000000..a345c05311c --- /dev/null +++ b/homeassistant/components/myq/.translations/de.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Stellen Sie eine Verbindung zum MyQ Gateway her" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/en.json b/homeassistant/components/myq/.translations/en.json index c31162b2894..c367873cbc9 100644 --- a/homeassistant/components/myq/.translations/en.json +++ b/homeassistant/components/myq/.translations/en.json @@ -1,22 +1,22 @@ { - "config": { - "title": "MyQ", - "step": { - "user": { - "title": "Connect to the MyQ Gateway", - "data": { - "username": "Username", - "password": "Password" - } - } - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "MyQ is already configured" + "config": { + "abort": { + "already_configured": "MyQ is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Connect to the MyQ Gateway" + } + }, + "title": "MyQ" } - } } \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/es.json b/homeassistant/components/myq/.translations/es.json new file mode 100644 index 00000000000..41db9de34a6 --- /dev/null +++ b/homeassistant/components/myq/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Conectar con el Gateway " + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/fr.json b/homeassistant/components/myq/.translations/fr.json new file mode 100644 index 00000000000..eacf5fc445a --- /dev/null +++ b/homeassistant/components/myq/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Connectez-vous \u00e0 la passerelle MyQ" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/ko.json b/homeassistant/components/myq/.translations/ko.json new file mode 100644 index 00000000000..db4ecc9ee4f --- /dev/null +++ b/homeassistant/components/myq/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ \uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "MyQ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/lb.json b/homeassistant/components/myq/.translations/lb.json new file mode 100644 index 00000000000..8556f60016f --- /dev/null +++ b/homeassistant/components/myq/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mat NuHeat Router verbannen" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/no.json b/homeassistant/components/myq/.translations/no.json new file mode 100644 index 00000000000..4d3ed384d75 --- /dev/null +++ b/homeassistant/components/myq/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Koble til MyQ Gateway" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/ru.json b/homeassistant/components/myq/.translations/ru.json new file mode 100644 index 00000000000..ec89856496c --- /dev/null +++ b/homeassistant/components/myq/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "MyQ" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/zh-Hant.json b/homeassistant/components/myq/.translations/zh-Hant.json new file mode 100644 index 00000000000..b7560ed40ce --- /dev/null +++ b/homeassistant/components/myq/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3 MyQ \u8def\u7531\u5668" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/ca.json b/homeassistant/components/nexia/.translations/ca.json new file mode 100644 index 00000000000..005edb82b59 --- /dev/null +++ b/homeassistant/components/nexia/.translations/ca.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Aquest dispositiu nexia home ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "username": "Nom d'usuari" + }, + "title": "Connexi\u00f3 amb mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/da.json b/homeassistant/components/nexia/.translations/da.json new file mode 100644 index 00000000000..3e66091d851 --- /dev/null +++ b/homeassistant/components/nexia/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/de.json b/homeassistant/components/nexia/.translations/de.json new file mode 100644 index 00000000000..123cfa26a67 --- /dev/null +++ b/homeassistant/components/nexia/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Stellen Sie eine Verbindung zu mynexia.com her" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/en.json b/homeassistant/components/nexia/.translations/en.json index d3fabfb0b4d..c6dcaed91f8 100644 --- a/homeassistant/components/nexia/.translations/en.json +++ b/homeassistant/components/nexia/.translations/en.json @@ -1,22 +1,22 @@ { - "config": { - "title": "Nexia", - "step": { - "user": { - "title": "Connect to mynexia.com", - "data": { - "username": "Username", - "password": "Password" - } - } - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "This nexia home is already configured" + "config": { + "abort": { + "already_configured": "This nexia home is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Username" + }, + "title": "Connect to mynexia.com" + } + }, + "title": "Nexia" } - } } \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/es.json b/homeassistant/components/nexia/.translations/es.json new file mode 100644 index 00000000000..836c6b514c2 --- /dev/null +++ b/homeassistant/components/nexia/.translations/es.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Este nexia home ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Usuario" + }, + "title": "Conectar con mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/fr.json b/homeassistant/components/nexia/.translations/fr.json new file mode 100644 index 00000000000..653cc0b3f04 --- /dev/null +++ b/homeassistant/components/nexia/.translations/fr.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Cette maison Nexia est d\u00e9j\u00e0 configur\u00e9e" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "username": "Nom d'utilisateur" + }, + "title": "Se connecter \u00e0 mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/ko.json b/homeassistant/components/nexia/.translations/ko.json new file mode 100644 index 00000000000..daabbe77ea7 --- /dev/null +++ b/homeassistant/components/nexia/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "nexia home \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "mynexia.com \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/lb.json b/homeassistant/components/nexia/.translations/lb.json new file mode 100644 index 00000000000..ae80d218786 --- /dev/null +++ b/homeassistant/components/nexia/.translations/lb.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Den Nexia Home ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "username": "Benotzernumm" + }, + "title": "Mat mynexia.com verbannen" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/no.json b/homeassistant/components/nexia/.translations/no.json new file mode 100644 index 00000000000..84dbcf5b503 --- /dev/null +++ b/homeassistant/components/nexia/.translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Dette nexia hjem er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "username": "Brukernavn" + }, + "title": "Koble til mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/ru.json b/homeassistant/components/nexia/.translations/ru.json new file mode 100644 index 00000000000..a294518a777 --- /dev/null +++ b/homeassistant/components/nexia/.translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/zh-Hant.json b/homeassistant/components/nexia/.translations/zh-Hant.json new file mode 100644 index 00000000000..7a768c1ed21 --- /dev/null +++ b/homeassistant/components/nexia/.translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Nexia home \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "title": "\u9023\u7dda\u81f3 mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/sl.json b/homeassistant/components/notion/.translations/sl.json index bbc87c6722a..c5577f52a24 100644 --- a/homeassistant/components/notion/.translations/sl.json +++ b/homeassistant/components/notion/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "To uporabni\u0161ko ime je \u017ee v uporabi." + }, "error": { "identifier_exists": "Uporabni\u0161ko ime je \u017ee registrirano", "invalid_credentials": "Neveljavno uporabni\u0161ko ime ali geslo", diff --git a/homeassistant/components/nuheat/.translations/ca.json b/homeassistant/components/nuheat/.translations/ca.json new file mode 100644 index 00000000000..6c2f739b94c --- /dev/null +++ b/homeassistant/components/nuheat/.translations/ca.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El term\u00f2stat ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "invalid_thermostat": "El n\u00famero de s\u00e8rie del term\u00f2stat no \u00e9s v\u00e0lid.", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "password": "Contrasenya", + "serial_number": "N\u00famero de s\u00e8rie del term\u00f2stat.", + "username": "Nom d'usuari" + }, + "description": "Has d\u2019obtenir el n\u00famero de s\u00e8rie o identificador del teu term\u00f2stat entrant a https://MyNuHeat.com i seleccionant el teu term\u00f2stat.", + "title": "Connexi\u00f3 amb NuHeat" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/da.json b/homeassistant/components/nuheat/.translations/da.json new file mode 100644 index 00000000000..3e66091d851 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/en.json b/homeassistant/components/nuheat/.translations/en.json index 4bfbb8ef62a..4b82319be62 100644 --- a/homeassistant/components/nuheat/.translations/en.json +++ b/homeassistant/components/nuheat/.translations/en.json @@ -1,25 +1,25 @@ { - "config" : { - "error" : { - "unknown" : "Unexpected error", - "cannot_connect" : "Failed to connect, please try again", - "invalid_auth" : "Invalid authentication", - "invalid_thermostat" : "The thermostat serial number is invalid." - }, - "title" : "NuHeat", - "abort" : { - "already_configured" : "The thermostat is already configured" - }, - "step" : { - "user" : { - "title" : "Connect to the NuHeat", - "description": "You will need to obtain your thermostat’s numeric serial number or ID by logging into https://MyNuHeat.com and selecting your thermostat(s).", - "data" : { - "username" : "Username", - "password" : "Password", - "serial_number" : "Serial number of the thermostat." + "config": { + "abort": { + "already_configured": "The thermostat is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "invalid_thermostat": "The thermostat serial number is invalid.", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "serial_number": "Serial number of the thermostat.", + "username": "Username" + }, + "description": "You will need to obtain your thermostat\u2019s numeric serial number or ID by logging into https://MyNuHeat.com and selecting your thermostat(s).", + "title": "Connect to the NuHeat" } - } - } - } -} + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/es.json b/homeassistant/components/nuheat/.translations/es.json new file mode 100644 index 00000000000..70e4b03ca70 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/es.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El termostato ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "invalid_thermostat": "El n\u00famero de serie del termostato no es v\u00e1lido.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "serial_number": "N\u00famero de serie del termostato.", + "username": "Nombre de usuario" + }, + "description": "Deber\u00e1 obtener el n\u00famero de serie o el ID de su termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando su(s) termostato(s).", + "title": "Con\u00e9ctese a NuHeat" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/fr.json b/homeassistant/components/nuheat/.translations/fr.json new file mode 100644 index 00000000000..21012de756b --- /dev/null +++ b/homeassistant/components/nuheat/.translations/fr.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Le thermostat est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "invalid_thermostat": "Le num\u00e9ro de s\u00e9rie du thermostat n'est pas valide.", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "password": "Mot de passe", + "serial_number": "Num\u00e9ro de s\u00e9rie du thermostat.", + "username": "Nom d'utilisateur" + }, + "title": "Connectez-vous au NuHeat" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/ko.json b/homeassistant/components/nuheat/.translations/ko.json new file mode 100644 index 00000000000..01db5835907 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/ko.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\uc628\ub3c4 \uc870\uc808\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "invalid_thermostat": "\uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "password": "\ube44\ubc00\ubc88\ud638", + "serial_number": "\uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "https://MyNuHeat.com \uc5d0 \ub85c\uadf8\uc778\ud558\uace0 \uc628\ub3c4 \uc870\uc808\uae30\ub97c \uc120\ud0dd\ud558\uc5ec \uc628\ub3c4 \uc870\uc808\uae30\uc758 \uc2dc\ub9ac\uc5bc \ubc88\ud638 \ub610\ub294 \ub610\ub294 ID \ub97c \uc5bb\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "title": "NuHeat \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/lb.json b/homeassistant/components/nuheat/.translations/lb.json new file mode 100644 index 00000000000..cba8bb91597 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/lb.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Den Thermostat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "invalid_thermostat": "Seriennummer vum Thermostat ass ong\u00eblteg", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert", + "serial_number": "Seriennummer vum Thermostat", + "username": "Benotzernumm" + }, + "title": "Mat NuHeat verbannen" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/no.json b/homeassistant/components/nuheat/.translations/no.json new file mode 100644 index 00000000000..74c0b8a8f54 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/no.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Termostaten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "invalid_thermostat": "Termostatens serienummer er ugyldig.", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "serial_number": "Termostatenes serienummer.", + "username": "Brukernavn" + }, + "description": "Du m\u00e5 skaffe termostats numeriske serienummer eller ID ved \u00e5 logge inn p\u00e5 https://MyNuHeat.com og velge termostaten (e).", + "title": "Koble til NuHeat" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/ru.json b/homeassistant/components/nuheat/.translations/ru.json new file mode 100644 index 00000000000..9a2ff139dd2 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/ru.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", + "invalid_thermostat": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430 \u043d\u0435\u0434\u0435\u0439\u0441\u0442\u0432\u0438\u0442\u0435\u043b\u0435\u043d.", + "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", + "serial_number": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430.", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0412\u044b \u0434\u043e\u043b\u0436\u043d\u044b \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0438\u043b\u0438 ID \u0412\u0430\u0448\u0435\u0433\u043e \u0442\u0435\u0440\u043c\u043e\u0441\u0442\u0430\u0442\u0430, \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 https://MyNuHeat.com.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/zh-Hant.json b/homeassistant/components/nuheat/.translations/zh-Hant.json new file mode 100644 index 00000000000..e228abeecd9 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/zh-Hant.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "\u6eab\u63a7\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "invalid_thermostat": "\u6eab\u63a7\u5668\u5e8f\u865f\u7121\u6548\u3002", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "serial_number": "\u6eab\u63a7\u5668\u5e8f\u865f\u3002", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u9700\u8981\u67e5\u770b\u60a8\u7684\u6eab\u63a7\u5668\u5e8f\u865f\u6216\u767b\u5165 https://MyNuHeat.com \u4e4b ID \u4e26\u9078\u64c7\u60a8\u7684\u6eab\u63a7\u5668\u3002", + "title": "\u9023\u7dda\u81f3 NuHeat" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 1ff93cff650..40ba84b9f41 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -4,7 +4,7 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", - "discovery_no_file": "Podatkovne konfiguracijske datoteke ni bilo mogo\u010de najti", + "discovery_no_file": "Podedovane konfiguracijske datoteke ni bilo", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", "non-interactive": "Neinteraktivni uvoz", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorirajte nove upravljane/deljene uporabnike", + "monitored_users": "Nadzorovani uporabniki", "show_all_controls": "Poka\u017ei vse kontrole", "use_episode_art": "Uporabi naslovno sliko epizode" }, diff --git a/homeassistant/components/powerwall/.translations/ca.json b/homeassistant/components/powerwall/.translations/ca.json new file mode 100644 index 00000000000..6b375c93ad8 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/ca.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El Powerwall ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "ip_address": "Adre\u00e7a IP" + }, + "title": "Connexi\u00f3 amb el Powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/de.json b/homeassistant/components/powerwall/.translations/de.json new file mode 100644 index 00000000000..1a442e7fbb6 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Die Powerwall ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "ip_address": "IP-Adresse" + }, + "title": "Stellen Sie eine Verbindung zur Powerwall her" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/en.json b/homeassistant/components/powerwall/.translations/en.json new file mode 100644 index 00000000000..583a88e5623 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/en.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "The powerwall is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Address" + }, + "title": "Connect to the powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/es.json b/homeassistant/components/powerwall/.translations/es.json new file mode 100644 index 00000000000..f0d0c6dab6c --- /dev/null +++ b/homeassistant/components/powerwall/.translations/es.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El powerwall ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + }, + "title": "Conectarse al powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/fr.json b/homeassistant/components/powerwall/.translations/fr.json new file mode 100644 index 00000000000..b907b5d429c --- /dev/null +++ b/homeassistant/components/powerwall/.translations/fr.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Le Powerwall est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "ip_address": "Adresse IP" + }, + "title": "Connectez-vous au Powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/it.json b/homeassistant/components/powerwall/.translations/it.json new file mode 100644 index 00000000000..0031ea5a9e2 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/it.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Il Powerwall \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "ip_address": "Indirizzo IP" + }, + "title": "Connessione al Powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/ko.json b/homeassistant/components/powerwall/.translations/ko.json new file mode 100644 index 00000000000..d7fcd8bfe76 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/ko.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "powerwall \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \uc8fc\uc18c" + }, + "title": "powerwall \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/lb.json b/homeassistant/components/powerwall/.translations/lb.json new file mode 100644 index 00000000000..c86cf73ba18 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/lb.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Powerwall ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "ip_address": "IP Adresse" + }, + "title": "Mat der Powerwall verbannen" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/no.json b/homeassistant/components/powerwall/.translations/no.json new file mode 100644 index 00000000000..63ce7b0da30 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/no.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Powerwall er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "ip_address": "IP adresse" + }, + "title": "Koble til powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/ru.json b/homeassistant/components/powerwall/.translations/ru.json new file mode 100644 index 00000000000..4b162ed8c55 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/ru.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "ip_address": "IP-\u0430\u0434\u0440\u0435\u0441" + }, + "title": "Tesla Powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/.translations/zh-Hant.json b/homeassistant/components/powerwall/.translations/zh-Hant.json new file mode 100644 index 00000000000..b85ce09eff1 --- /dev/null +++ b/homeassistant/components/powerwall/.translations/zh-Hant.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Powerwall \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "ip_address": "IP \u4f4d\u5740" + }, + "title": "\u9023\u7dda\u81f3 Powerwall" + } + }, + "title": "Tesla Powerwall" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/ca.json b/homeassistant/components/pvpc_hourly_pricing/.translations/ca.json new file mode 100644 index 00000000000..0a7af79fab3 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/ca.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3 ja configurada amb un sensor amb aquesta tarifa" + }, + "step": { + "user": { + "data": { + "name": "Nom del sensor", + "tariff": "Tarifa contractada (1, 2 o 3 per\u00edodes)" + }, + "description": "Aquest sensor utilitza l'API oficial de la xarxa el\u00e8ctrica espanyola (REE) per obtenir els [preus per hora de l\u2019electricitat (PVPC)](https://www.esios.ree.es/es/pvpc) a Espanya.\nPer a m\u00e9s informaci\u00f3, consulta la [documentaci\u00f3 de la integraci\u00f3](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSelecciona la tarifa contractada, en funci\u00f3 del nombre de per\u00edodes que t\u00e9: \n - 1 per\u00edode: normal (sense discriminaci\u00f3)\n - 2 per\u00edodes: discriminaci\u00f3 (tarifa nocturna) \n - 3 per\u00edodes: cotxe el\u00e8ctric (tarifa nocturna de 3 per\u00edodes)", + "title": "Selecci\u00f3 de tarifa" + } + }, + "title": "Preu per hora de l'electricitat a Espanya (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/es.json b/homeassistant/components/pvpc_hourly_pricing/.translations/es.json index 746edf3043d..53617a3c83d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/.translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/es.json @@ -7,12 +7,12 @@ "user": { "data": { "name": "Nombre del sensor", - "tariff": "Tarifa contratada (1, 2, o 3 periodos)" + "tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)" }, - "description": "Este sensor utiliza la API oficial de REE para obtener el [precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\\u00f1a.\nPara instrucciones detalladas consulte la [documentación de la integración](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSeleccione la tarifa contratada en base al número de periodos de facturación al día:\n- 1 periodo: normal\n- 2 periodos: discriminación (tarifa nocturna)\n- 3 periodos: coche eléctrico (tarifa nocturna de 3 periodos)", + "description": "Este sensor utiliza la API oficial para obtener [precios por hora de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visite [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSeleccione la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", "title": "Selecci\u00f3n de tarifa" } }, - "title": "Precio horario de la electricidad en Espa\u00f1a (PVPC)" + "title": "Precio por hora de la electricidad en Espa\u00f1a (PVPC)" } } \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/fr.json b/homeassistant/components/pvpc_hourly_pricing/.translations/fr.json new file mode 100644 index 00000000000..5c615c5f757 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "L'int\u00e9gration est d\u00e9j\u00e0 configur\u00e9e avec un capteur existant avec ce tarif" + }, + "step": { + "user": { + "data": { + "name": "Nom du capteur" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json b/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json new file mode 100644 index 00000000000..4fa14c495de --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Numm vum Sensor" + }, + "title": "Auswiel vum Tarif" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/no.json b/homeassistant/components/pvpc_hourly_pricing/.translations/no.json new file mode 100644 index 00000000000..0a7f93dda8a --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/no.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "Integrasjon er allerede konfigurert med en eksisterende sensor med den tariffen" + }, + "step": { + "user": { + "data": { + "name": "Sensornavn", + "tariff": "Avtaletariff (1, 2 eller 3 perioder)" + }, + "description": "Denne sensoren bruker offisiell API for \u00e5 f\u00e5 [timeprising av elektrisitet (PVPC)](https://www.esios.ree.es/es/pvpc) i Spania.\nFor mer presis forklaring, bes\u00f8k [integrasjonsdokumenter](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nVelg den avtalte satsen basert p\u00e5 antall faktureringsperioder per dag:\n- 1 periode: normal\n- 2 perioder: diskriminering (nattlig rate)\n- 3 perioder: elbil (per natt rate p\u00e5 3 perioder)", + "title": "Tariffvalg" + } + }, + "title": "Timepris p\u00e5 elektrisitet i Spania (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/ru.json b/homeassistant/components/pvpc_hourly_pricing/.translations/ru.json new file mode 100644 index 00000000000..aaa10fa21b7 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/ru.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "user": { + "data": { + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "tariff": "\u041a\u043e\u043d\u0442\u0440\u0430\u043a\u0442\u043d\u044b\u0439 \u0442\u0430\u0440\u0438\u0444 (1, 2 \u0438\u043b\u0438 3 \u043f\u0435\u0440\u0438\u043e\u0434\u0430)" + }, + "description": "\u042d\u0442\u043e\u0442 \u0441\u0435\u043d\u0441\u043e\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u043e\u0444\u0438\u0446\u0438\u0430\u043b\u044c\u043d\u044b\u0439 API \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f [\u043f\u043e\u0447\u0430\u0441\u043e\u0432\u043e\u0439 \u0446\u0435\u043d\u044b \u0437\u0430 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044e (PVPC)](https://www.esios.ree.es/es/pvpc) \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438.\n\u0414\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, \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](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\n\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u0430\u0440\u0438\u0444, \u043e\u0441\u043d\u043e\u0432\u0430\u043d\u043d\u044b\u0439 \u043d\u0430 \u043a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u0435 \u0440\u0430\u0441\u0447\u0435\u0442\u043d\u044b\u0445 \u043f\u0435\u0440\u0438\u043e\u0434\u043e\u0432 \u0432 \u0434\u0435\u043d\u044c:\n- 1 \u043f\u0435\u0440\u0438\u043e\u0434: normal\n- 2 \u043f\u0435\u0440\u0438\u043e\u0434\u0430: discrimination (nightly rate)\n- 3 \u043f\u0435\u0440\u0438\u043e\u0434\u0430: electric car (nightly rate of 3 periods)", + "title": "\u0412\u044b\u0431\u043e\u0440 \u0442\u0430\u0440\u0438\u0444\u0430" + } + }, + "title": "\u042d\u043b\u0435\u043a\u0442\u0440\u043e\u044d\u043d\u0435\u0440\u0433\u0438\u044f \u0432 \u0418\u0441\u043f\u0430\u043d\u0438\u0438 (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/zh-Hant.json b/homeassistant/components/pvpc_hourly_pricing/.translations/zh-Hant.json new file mode 100644 index 00000000000..a10c499dd59 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/zh-Hant.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\u6574\u5408\u5df2\u7d93\u8a2d\u5b9a\u4e26\u6709\u73fe\u6709\u50b3\u611f\u5668\u4f7f\u7528\u76f8\u540c\u8cbb\u7387" + }, + "step": { + "user": { + "data": { + "name": "\u50b3\u611f\u5668\u540d\u7a31", + "tariff": "\u5408\u7d04\u8cbb\u7387\uff081\u30012 \u6216 3 \u9031\u671f\uff09" + }, + "description": "\u6b64\u50b3\u611f\u5668\u4f7f\u7528\u4e86\u975e\u5b98\u65b9 API \u4ee5\u53d6\u5f97\u897f\u73ed\u7259 [\u8a08\u6642\u96fb\u50f9\uff08PVPC\uff09](https://www.esios.ree.es/es/pvpc)\u3002\n\u95dc\u65bc\u66f4\u8a73\u7d30\u7684\u8aaa\u660e\uff0c\u8acb\u53c3\u95b1 [\u6574\u5408\u6587\u4ef6](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\u3002\n\n\u57fa\u65bc\u6bcf\u5929\u7684\u5e33\u55ae\u9031\u671f\u9078\u64c7\u5408\u7d04\u8cbb\u7387\uff1a\n- 1 \u9031\u671f\uff1a\u4e00\u822c\n- 2 \u9031\u671f\uff1a\u5dee\u5225\u8cbb\u7387\uff08\u591c\u9593\u8cbb\u7387\uff09\n- 3 \u9031\u671f\uff1a\u96fb\u52d5\u8eca\uff08\u591c\u9593\u8cbb\u7387 3 \u9031\u671f\uff09", + "title": "\u8cbb\u7387\u9078\u64c7" + } + }, + "title": "\u897f\u73ed\u7259\u6642\u8a08\u96fb\u50f9\uff08PVPC\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/ca.json b/homeassistant/components/rachio/.translations/ca.json new file mode 100644 index 00000000000..468ab0b3f5c --- /dev/null +++ b/homeassistant/components/rachio/.translations/ca.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "api_key": "Clau API del compte Rachio." + }, + "description": "Necessitar\u00e0s la clau API de https://app.rach.io/. Selecciona 'Configuraci\u00f3 del compte' (Account Settings) i, a continuaci\u00f3, clica 'Obtenir clau API' (GET API KEY).", + "title": "Connexi\u00f3 amb dispositiu Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Durant quant de temps (en minuts) mantenir engegada una estaci\u00f3 quan l\u2019interruptor s'activa." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/de.json b/homeassistant/components/rachio/.translations/de.json new file mode 100644 index 00000000000..79c15a43dc4 --- /dev/null +++ b/homeassistant/components/rachio/.translations/de.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "api_key": "Der API-Schl\u00fcssel f\u00fcr das Rachio-Konto." + }, + "title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her" + } + }, + "title": "Rachio" + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/en.json b/homeassistant/components/rachio/.translations/en.json index 391320289db..bc87c370068 100644 --- a/homeassistant/components/rachio/.translations/en.json +++ b/homeassistant/components/rachio/.translations/en.json @@ -1,31 +1,31 @@ { - "config": { - "title": "Rachio", - "step": { - "user": { - "title": "Connect to your Rachio device", - "description" : "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.", - "data": { - "api_key": "The API key for the Rachio account." - } - } + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "api_key": "The API key for the Rachio account." + }, + "description": "You will need the API Key from https://app.rach.io/. Select 'Account Settings, and then click on 'GET API KEY'.", + "title": "Connect to your Rachio device" + } + }, + "title": "Rachio" }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" - } - }, - "options": { - "step": { - "init": { - "data": { - "manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled." + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "For how long, in minutes, to turn on a station when the switch is enabled." + } + } } - } } - } -} +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/es.json b/homeassistant/components/rachio/.translations/es.json new file mode 100644 index 00000000000..e938c78677e --- /dev/null +++ b/homeassistant/components/rachio/.translations/es.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "La clave API para la cuenta Rachio." + }, + "description": "Necesitar\u00e1s la clave API de https://app.rach.io/. Selecciona 'Account Settings' y luego haz clic en 'GET API KEY'.", + "title": "Conectar a tu dispositivo Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Durante cu\u00e1nto tiempo, en minutos, permanece encendida una estaci\u00f3n cuando el interruptor est\u00e1 activado." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/fr.json b/homeassistant/components/rachio/.translations/fr.json new file mode 100644 index 00000000000..a7fd606b310 --- /dev/null +++ b/homeassistant/components/rachio/.translations/fr.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "api_key": "La cl\u00e9 API pour le compte Rachio." + }, + "description": "Vous aurez besoin de la cl\u00e9 API de https://app.rach.io/. S\u00e9lectionnez \"Param\u00e8tres du compte, puis cliquez sur \"GET API KEY \".", + "title": "Connectez-vous \u00e0 votre appareil Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Le temps, en minutes, n\u00e9cessaire pour allumer une station lorsque l'interrupteur est activ\u00e9." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/it.json b/homeassistant/components/rachio/.translations/it.json new file mode 100644 index 00000000000..fe05d236e8a --- /dev/null +++ b/homeassistant/components/rachio/.translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "api_key": "Chiave API per l'account Rachio." + }, + "description": "\u00c8 necessaria la chiave API di https://app.rach.io/. Selezionare 'Impostazioni Account', quindi fare clic su 'GET API KEY'.", + "title": "Connettiti al tuo dispositivo Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Per quanto tempo, in minuti, accendere una stazione quando l'interruttore \u00e8 abilitato." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/ko.json b/homeassistant/components/rachio/.translations/ko.json new file mode 100644 index 00000000000..d52aac4bf4a --- /dev/null +++ b/homeassistant/components/rachio/.translations/ko.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "api_key": "Rachio \uacc4\uc815\uc758 API \ud0a4." + }, + "description": "https://app.rach.io/ \uc758 API \ud0a4\uac00 \ud544\uc694\ud569\ub2c8\ub2e4. \uacc4\uc815 \uc124\uc815\uc744 \uc120\ud0dd\ud55c \ub2e4\uc74c 'GET API KEY ' \ub97c \ud074\ub9ad\ud574\uc8fc\uc138\uc694.", + "title": "Rachio \uae30\uae30\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\uc2a4\uc704\uce58\uac00 \ud65c\uc131\ud654\ub41c \uacbd\uc6b0 \uc2a4\ud14c\uc774\uc158\uc744 \ucf1c\ub294 \uc2dc\uac04(\ubd84) \uc785\ub2c8\ub2e4." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/lb.json b/homeassistant/components/rachio/.translations/lb.json new file mode 100644 index 00000000000..b6180e4ffce --- /dev/null +++ b/homeassistant/components/rachio/.translations/lb.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Mam Rachio Apparat verbannen" + } + }, + "title": "Rachio" + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/no.json b/homeassistant/components/rachio/.translations/no.json new file mode 100644 index 00000000000..eb93f749766 --- /dev/null +++ b/homeassistant/components/rachio/.translations/no.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "api_key": "API-n\u00f8kkelen for Rachio-kontoen." + }, + "description": "Du trenger API-n\u00f8kkelen fra https://app.rach.io/. Velg \"Kontoinnstillinger\", og klikk deretter p\u00e5 \"GET API KEY\".", + "title": "Koble til Rachio-enheten din" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Hvor lenge, i minutter, for \u00e5 sl\u00e5 p\u00e5 en stasjon n\u00e5r bryteren er aktivert." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/ru.json b/homeassistant/components/rachio/.translations/ru.json new file mode 100644 index 00000000000..619f5d4bb0e --- /dev/null +++ b/homeassistant/components/rachio/.translations/ru.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": { + "api_key": "\u041a\u043b\u044e\u0447 API \u0443\u0447\u0435\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 Rachio." + }, + "description": "\u0414\u043b\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043d\u0443\u0436\u0435\u043d \u043a\u043b\u044e\u0447 API \u043e\u0442 https://app.rach.io/. \u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 'Account Settings', \u0430 \u0437\u0430\u0442\u0435\u043c \u043d\u0430\u0436\u043c\u0438\u0442\u0435 'GET API KEY'.", + "title": "Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\u041d\u0430 \u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0432\u043a\u043b\u044e\u0447\u0430\u0442\u044c \u0441\u0442\u0430\u043d\u0446\u0438\u044e, \u043a\u043e\u0433\u0434\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044c \u0432\u043a\u043b\u044e\u0447\u0435\u043d (\u0432 \u043c\u0438\u043d\u0443\u0442\u0430\u0445)." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/sl.json b/homeassistant/components/rachio/.translations/sl.json new file mode 100644 index 00000000000..80e5f3ff99c --- /dev/null +++ b/homeassistant/components/rachio/.translations/sl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "api_key": "Klju\u010d API za ra\u010dun Rachio." + }, + "description": "Potrebovali boste API klju\u010d iz https://app.rach.io/. Izberite ' nastavitve ra\u010duna in kliknite 'get API KEY'.", + "title": "Pove\u017eite se z napravo Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Kako dolgo, v minutah, da vklopite postajo, ko je stikalo omogo\u010deno." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/zh-Hant.json b/homeassistant/components/rachio/.translations/zh-Hant.json new file mode 100644 index 00000000000..0eabf0ed574 --- /dev/null +++ b/homeassistant/components/rachio/.translations/zh-Hant.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "api_key": "Rachio \u5e33\u865f API \u91d1\u9470\u3002" + }, + "description": "\u5c07\u6703\u9700\u8981\u7531 https://app.rach.io/ \u53d6\u5f97 App \u5bc6\u9470\u3002\u9078\u64c7\u5e33\u865f\u8a2d\u5b9a\uff08Account Settings\uff09\u3001\u4e26\u9078\u64c7\u7372\u5f97\u5bc6\u9470\uff08GET API KEY\uff09\u3002", + "title": "\u9023\u7dda\u81f3 Rachio \u8a2d\u5099" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "\u7576\u958b\u95dc\u958b\u555f\u5f8c\u3001\u5de5\u4f5c\u7ad9\u6240\u8981\u958b\u555f\u7684\u5206\u9418\u6578\u3002" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/sl.json b/homeassistant/components/rainmachine/.translations/sl.json index 10d05fadf93..68c466150f1 100644 --- a/homeassistant/components/rainmachine/.translations/sl.json +++ b/homeassistant/components/rainmachine/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ta regulator RainMachine je \u017ee konfiguriran." + }, "error": { "identifier_exists": "Ra\u010dun \u017ee registriran", "invalid_credentials": "Neveljavne poverilnice" diff --git a/homeassistant/components/rainmachine/.translations/sv.json b/homeassistant/components/rainmachine/.translations/sv.json index 03f9c671c35..864c1105446 100644 --- a/homeassistant/components/rainmachine/.translations/sv.json +++ b/homeassistant/components/rainmachine/.translations/sv.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Denna RainMachine-enhet \u00e4r redan konfigurerad" + }, "error": { "identifier_exists": "Kontot \u00e4r redan registrerat", "invalid_credentials": "Ogiltiga autentiseringsuppgifter" diff --git a/homeassistant/components/ring/.translations/es.json b/homeassistant/components/ring/.translations/es.json index 6bdd20d361b..f5598c56d70 100644 --- a/homeassistant/components/ring/.translations/es.json +++ b/homeassistant/components/ring/.translations/es.json @@ -19,9 +19,9 @@ "password": "Contrase\u00f1a", "username": "Usuario" }, - "title": "Iniciar sesi\u00f3n con cuenta de Anillo" + "title": "Iniciar sesi\u00f3n con cuenta de Ring" } }, - "title": "Anillo" + "title": "Ring" } } \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/ca.json b/homeassistant/components/roku/.translations/ca.json new file mode 100644 index 00000000000..727c4e79d73 --- /dev/null +++ b/homeassistant/components/roku/.translations/ca.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu Roku ja est\u00e0 configurat", + "unknown": "Error inesperat" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "unknown": "Error inesperat" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Vols configurar {name}? Es substituiran les configuracions manuals d'aquest dispositiu en els arxius yaml.", + "title": "Roku" + }, + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP" + }, + "description": "Introdueix la teva informaci\u00f3 de Roku.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/cs.json b/homeassistant/components/roku/.translations/cs.json new file mode 100644 index 00000000000..3b814303e69 --- /dev/null +++ b/homeassistant/components/roku/.translations/cs.json @@ -0,0 +1,7 @@ +{ + "config": { + "abort": { + "unknown": "Neo\u010dek\u00e1van\u00e1 chyba" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/de.json b/homeassistant/components/roku/.translations/de.json new file mode 100644 index 00000000000..d3c02cc1373 --- /dev/null +++ b/homeassistant/components/roku/.translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Das Roku-Ger\u00e4t ist bereits konfiguriert", + "unknown": "Unerwarteter Fehler" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "data": { + "one": "eins", + "other": "andere" + }, + "description": "M\u00f6chten Sie {name} einrichten?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host oder IP-Adresse" + }, + "description": "Geben Sie Ihre Roku-Informationen ein.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/en.json b/homeassistant/components/roku/.translations/en.json index 45a2dd8dcba..a92570c7019 100644 --- a/homeassistant/components/roku/.translations/en.json +++ b/homeassistant/components/roku/.translations/en.json @@ -5,13 +5,13 @@ "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again" + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" }, "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { - "data": {}, - "description": "Do you want to set up {name}? Manual configurations for this device in the yaml files will be overwritten.", + "description": "Do you want to set up {name}?", "title": "Roku" }, "user": { @@ -24,4 +24,4 @@ }, "title": "Roku" } -} +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/es.json b/homeassistant/components/roku/.translations/es.json new file mode 100644 index 00000000000..a472d079efb --- /dev/null +++ b/homeassistant/components/roku/.translations/es.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo Roku ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", + "unknown": "Error inesperado" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "\u00bfQuieres configurar {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "description": "Introduce tu informaci\u00f3n de Roku.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/fr.json b/homeassistant/components/roku/.translations/fr.json new file mode 100644 index 00000000000..ff24f46e921 --- /dev/null +++ b/homeassistant/components/roku/.translations/fr.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Le p\u00e9riph\u00e9rique Roku est d\u00e9j\u00e0 configur\u00e9", + "unknown": "Erreur inattendue" + }, + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "unknown": "Erreur inattendue" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Voulez-vous configurer {name} ?", + "title": "Roku" + }, + "user": { + "data": { + "host": "H\u00f4te ou adresse IP" + }, + "description": "Entrez vos informations Roku.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/it.json b/homeassistant/components/roku/.translations/it.json new file mode 100644 index 00000000000..37567913700 --- /dev/null +++ b/homeassistant/components/roku/.translations/it.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo Roku \u00e8 gi\u00e0 configurato", + "unknown": "Errore imprevisto" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "data": { + "one": "uno", + "other": "altri" + }, + "description": "Vuoi impostare {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host o indirizzo IP" + }, + "description": "Inserisci le tue informazioni Roku.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/ko.json b/homeassistant/components/roku/.translations/ko.json new file mode 100644 index 00000000000..75045d14865 --- /dev/null +++ b/homeassistant/components/roku/.translations/ko.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Roku \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "{name} \uc744(\ub97c) \uc124\uc815\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "Roku" + }, + "user": { + "data": { + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c" + }, + "description": "Roku \uc815\ubcf4\ub97c \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/lb.json b/homeassistant/components/roku/.translations/lb.json new file mode 100644 index 00000000000..9808f207f30 --- /dev/null +++ b/homeassistant/components/roku/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Roku Apparat ass scho konfigur\u00e9iert", + "unknown": "Onerwaarte Feeler" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "title": "Roku" + }, + "user": { + "data": { + "host": "Numm oder IP Adresse" + }, + "description": "F\u00ebll d\u00e9ng Roku Informatiounen aus.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/no.json b/homeassistant/components/roku/.translations/no.json new file mode 100644 index 00000000000..cabc68de3f7 --- /dev/null +++ b/homeassistant/components/roku/.translations/no.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Roku-enheten er allerede konfigurert", + "unknown": "Uventet feil" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "Vil du sette opp {name} ?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Vert eller IP-adresse" + }, + "description": "Skriv inn Roku-informasjonen din.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/pl.json b/homeassistant/components/roku/.translations/pl.json new file mode 100644 index 00000000000..db3ef261f07 --- /dev/null +++ b/homeassistant/components/roku/.translations/pl.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie Roku jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "data": { + "few": "kilka", + "many": "wiele", + "one": "jeden", + "other": "inne" + }, + "description": "Czy chcesz skonfigurowa\u0107 {name}? R\u0119czne konfiguracje tego urz\u0105dzenia zostan\u0105 zast\u0105pione.", + "title": "Roku" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP" + }, + "description": "Wprowad\u017a dane Roku.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/ru.json b/homeassistant/components/roku/.translations/ru.json new file mode 100644 index 00000000000..b1825654c9d --- /dev/null +++ b/homeassistant/components/roku/.translations/ru.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441" + }, + "description": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e Roku.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/sl.json b/homeassistant/components/roku/.translations/sl.json new file mode 100644 index 00000000000..f47067b3392 --- /dev/null +++ b/homeassistant/components/roku/.translations/sl.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava roku je \u017ee konfigurirana", + "unknown": "Nepri\u010dakovana napaka" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "unknown": "Nepri\u010dakovana napaka" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "data": { + "few": "nekaj", + "one": "ena", + "other": "drugo", + "two": "dva" + }, + "description": "Ali \u017eelite nastaviti {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Gostitelj ali IP naslov" + }, + "description": "Vnesite va\u0161e Roku podatke.", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/zh-Hant.json b/homeassistant/components/roku/.translations/zh-Hant.json new file mode 100644 index 00000000000..2d6a606ef77 --- /dev/null +++ b/homeassistant/components/roku/.translations/zh-Hant.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "already_configured": "Roku \u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "flow_title": "Roku\uff1a{name}", + "step": { + "ssdp_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {name}\uff1f", + "title": "Roku" + }, + "user": { + "data": { + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740" + }, + "description": "\u8f38\u5165 Roku \u8cc7\u8a0a\u3002", + "title": "Roku" + } + }, + "title": "Roku" + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json index 27b9ecc37df..e5e8611362c 100644 --- a/homeassistant/components/samsungtv/.translations/de.json +++ b/homeassistant/components/samsungtv/.translations/de.json @@ -11,7 +11,7 @@ "flow_title": "Samsung TV: {model}", "step": { "confirm": { - "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Authentifizierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", + "description": "M\u00f6chtest du Samsung TV {model} einrichten? Wenn du noch nie eine Verbindung zum Home Assistant hergestellt hast, solltest du ein Popup-Fenster auf deinem Fernseher sehen, das nach einer Autorisierung fragt. Manuelle Konfigurationen f\u00fcr dieses Fernsehger\u00e4t werden \u00fcberschrieben.", "title": "Samsung TV" }, "user": { diff --git a/homeassistant/components/sense/.translations/da.json b/homeassistant/components/sense/.translations/da.json new file mode 100644 index 00000000000..5085cdbce9e --- /dev/null +++ b/homeassistant/components/sense/.translations/da.json @@ -0,0 +1,11 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/.translations/fr.json b/homeassistant/components/sense/.translations/fr.json new file mode 100644 index 00000000000..999ac2a0ac7 --- /dev/null +++ b/homeassistant/components/sense/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "invalid_auth": "Authentification invalide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "email": "Adresse e-mail", + "password": "Mot de passe" + }, + "title": "Connectez-vous \u00e0 votre moniteur d'\u00e9nergie Sense" + } + }, + "title": "Sense" + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/.translations/ko.json b/homeassistant/components/sense/.translations/ko.json new file mode 100644 index 00000000000..6041992e56d --- /dev/null +++ b/homeassistant/components/sense/.translations/ko.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "email": "\uc774\uba54\uc77c \uc8fc\uc18c", + "password": "\ube44\ubc00\ubc88\ud638" + }, + "title": "Sense Energy Monitor \uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Sense" + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/.translations/pl.json b/homeassistant/components/sense/.translations/pl.json new file mode 100644 index 00000000000..0e0e7f5da66 --- /dev/null +++ b/homeassistant/components/sense/.translations/pl.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "email": "Adres e-mail", + "password": "Has\u0142o" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sense/.translations/sl.json b/homeassistant/components/sense/.translations/sl.json new file mode 100644 index 00000000000..9f7568ef249 --- /dev/null +++ b/homeassistant/components/sense/.translations/sl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Naprava je \u017ee konfigurirana" + }, + "error": { + "cannot_connect": "Povezava ni uspela, poskusite znova", + "invalid_auth": "Neveljavna avtentikacija", + "unknown": "Nepri\u010dakovana napaka" + }, + "step": { + "user": { + "data": { + "email": "E-po\u0161tni naslov", + "password": "Geslo" + }, + "title": "Pove\u017eite se s svojim Sense Energy monitor-jem" + } + }, + "title": "Sense" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/ca.json b/homeassistant/components/shopping_list/.translations/ca.json new file mode 100644 index 00000000000..541ee0c0e9c --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/ca.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "La llista de compres ja est\u00e0 configurada." + }, + "step": { + "user": { + "description": "Vols configurar la llista de compres?", + "title": "Llista de compres" + } + }, + "title": "Llista de compres" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/de.json b/homeassistant/components/shopping_list/.translations/de.json new file mode 100644 index 00000000000..13638985ee2 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Die Einkaufsliste ist bereits konfiguriert." + }, + "step": { + "user": { + "description": "M\u00f6chten Sie die Einkaufsliste konfigurieren?", + "title": "Einkaufsliste" + } + }, + "title": "Einkaufsliste" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/es.json b/homeassistant/components/shopping_list/.translations/es.json new file mode 100644 index 00000000000..a2c89f0032f --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/es.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "La lista de la compra ya est\u00e1 configurada." + }, + "step": { + "user": { + "description": "\u00bfQuieres configurar la lista de la compra?", + "title": "Lista de la compra" + } + }, + "title": "Lista de la compra" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/fr.json b/homeassistant/components/shopping_list/.translations/fr.json new file mode 100644 index 00000000000..05034e3e58e --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/fr.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "La liste d'achats est d\u00e9j\u00e0 configur\u00e9e." + }, + "step": { + "user": { + "description": "Voulez-vous configurer la liste d'achats ?", + "title": "Liste d'achats" + } + }, + "title": "Liste d\u2019achats" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/it.json b/homeassistant/components/shopping_list/.translations/it.json new file mode 100644 index 00000000000..ffd1c1d7f67 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/it.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "La lista della spesa \u00e8 gi\u00e0 configurata." + }, + "step": { + "user": { + "description": "Vuoi configurare la lista della spesa?", + "title": "Lista della Spesa" + } + }, + "title": "Lista della Spesa" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/ko.json b/homeassistant/components/shopping_list/.translations/ko.json new file mode 100644 index 00000000000..7885890d8b4 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/ko.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\uc7a5\ubcf4\uae30\ubaa9\ub85d\uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "step": { + "user": { + "description": "\uc7a5\ubcf4\uae30\ubaa9\ub85d\uc744 \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\uc7a5\ubcf4\uae30\ubaa9\ub85d" + } + }, + "title": "\uc7a5\ubcf4\uae30\ubaa9\ub85d" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/lb.json b/homeassistant/components/shopping_list/.translations/lb.json new file mode 100644 index 00000000000..46f26637689 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/lb.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Akafsl\u00ebscht ass scho konfigur\u00e9iert." + }, + "step": { + "user": { + "description": "Soll Akafsl\u00ebscht konfigur\u00e9iert ginn?", + "title": "Akafsl\u00ebscht" + } + }, + "title": "Akafsl\u00ebscht" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/no.json b/homeassistant/components/shopping_list/.translations/no.json new file mode 100644 index 00000000000..7945f3b0d3f --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/no.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Handlelisten er allerede konfigurert." + }, + "step": { + "user": { + "description": "\u00d8nsker du \u00e5 konfigurere handleliste?", + "title": "Handleliste" + } + }, + "title": "Handleliste" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/pl.json b/homeassistant/components/shopping_list/.translations/pl.json new file mode 100644 index 00000000000..d16122d0df9 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/pl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Lista zakup\u00f3w jest ju\u017c skonfigurowana." + }, + "step": { + "user": { + "description": "Czy chcesz skonfigurowa\u0107 list\u0119 zakup\u00f3w?", + "title": "Lista zakup\u00f3w" + } + }, + "title": "Lista zakup\u00f3w" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/ru.json b/homeassistant/components/shopping_list/.translations/ru.json new file mode 100644 index 00000000000..e230421909d --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/ru.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0439 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "step": { + "user": { + "description": "\u0412\u044b \u0445\u043e\u0442\u0438\u0442\u0435 \u0441\u043e\u0437\u0434\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a?", + "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a" + } + }, + "title": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u043e\u043a\u0443\u043f\u043e\u043a" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/sk.json b/homeassistant/components/shopping_list/.translations/sk.json new file mode 100644 index 00000000000..857ef6488e5 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/sk.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "N\u00e1kupn\u00fd zoznam je u\u017e nakonfigurovan\u00fd." + }, + "step": { + "user": { + "description": "Chcete nakonfigurova\u0165 n\u00e1kupn\u00fd zoznam?", + "title": "N\u00e1kupn\u00fd zoznam" + } + }, + "title": "N\u00e1kupn\u00fd zoznam" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/sl.json b/homeassistant/components/shopping_list/.translations/sl.json new file mode 100644 index 00000000000..f5d594ed6f5 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/sl.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "Nakupovalni seznam je \u017ee konfiguriran." + }, + "step": { + "user": { + "description": "Ali \u017eelite konfigurirati nakupovalni seznam?", + "title": "Nakupovalni seznam" + } + }, + "title": "Nakupovalni seznam" + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/.translations/zh-Hant.json b/homeassistant/components/shopping_list/.translations/zh-Hant.json new file mode 100644 index 00000000000..aea7a9b6409 --- /dev/null +++ b/homeassistant/components/shopping_list/.translations/zh-Hant.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "\u8cfc\u7269\u6e05\u55ae\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" + }, + "step": { + "user": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a\u8cfc\u7269\u6e05\u55ae\uff1f", + "title": "\u8cfc\u7269\u6e05\u55ae" + } + }, + "title": "\u8cfc\u7269\u6e05\u55ae" + } +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ca.json b/homeassistant/components/simplisafe/.translations/ca.json index a89e4c753cb..1f772071b59 100644 --- a/homeassistant/components/simplisafe/.translations/ca.json +++ b/homeassistant/components/simplisafe/.translations/ca.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Codi (per la UI de Home Assistant)" + }, + "title": "Configuraci\u00f3 de SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/de.json b/homeassistant/components/simplisafe/.translations/de.json index 4d5eefc480b..8c615a80c3f 100644 --- a/homeassistant/components/simplisafe/.translations/de.json +++ b/homeassistant/components/simplisafe/.translations/de.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Code (wird in der Benutzeroberfl\u00e4che von Home Assistant verwendet)" + }, + "title": "Konfigurieren Sie SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json index 60c3784ee9d..c9d92c9e445 100644 --- a/homeassistant/components/simplisafe/.translations/en.json +++ b/homeassistant/components/simplisafe/.translations/en.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Code (for Home Assistant)", "password": "Password", "username": "Email Address" }, diff --git a/homeassistant/components/simplisafe/.translations/es.json b/homeassistant/components/simplisafe/.translations/es.json index 815aa6be742..dfd87be2721 100644 --- a/homeassistant/components/simplisafe/.translations/es.json +++ b/homeassistant/components/simplisafe/.translations/es.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "C\u00f3digo (utilizado en el interfaz de usuario de Home Assistant)" + }, + "title": "Configurar SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/fr.json b/homeassistant/components/simplisafe/.translations/fr.json index de05edea8c9..576c66ab970 100644 --- a/homeassistant/components/simplisafe/.translations/fr.json +++ b/homeassistant/components/simplisafe/.translations/fr.json @@ -15,5 +15,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Code (utilis\u00e9 dans l'interface Home Assistant)" + }, + "title": "Configurer SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/it.json b/homeassistant/components/simplisafe/.translations/it.json index f153ec36959..80f684cff7c 100644 --- a/homeassistant/components/simplisafe/.translations/it.json +++ b/homeassistant/components/simplisafe/.translations/it.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Codice (utilizzato nell'Interfaccia Utente di Home Assistant)" + }, + "title": "Configurare SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ko.json b/homeassistant/components/simplisafe/.translations/ko.json index 3327ddf9ab1..17d50bc1508 100644 --- a/homeassistant/components/simplisafe/.translations/ko.json +++ b/homeassistant/components/simplisafe/.translations/ko.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "\ucf54\ub4dc (Home Assistant UI \uc5d0\uc11c \uc0ac\uc6a9\ub428)" + }, + "title": "SimpliSafe \uad6c\uc131" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/no.json b/homeassistant/components/simplisafe/.translations/no.json index 4c25893791b..83a3f8cbaf9 100644 --- a/homeassistant/components/simplisafe/.translations/no.json +++ b/homeassistant/components/simplisafe/.translations/no.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Kode (brukt i home assistant ui)" + }, + "title": "Konfigurer SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index 2d8b63c4bab..070ac3f3425 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "\u041a\u043e\u0434 (\u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f \u0432 \u0438\u043d\u0442\u0435\u0440\u0444\u0435\u0439\u0441\u0435 Home Assistant)" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/sl.json b/homeassistant/components/simplisafe/.translations/sl.json index 7fe0adad2df..fde16021d69 100644 --- a/homeassistant/components/simplisafe/.translations/sl.json +++ b/homeassistant/components/simplisafe/.translations/sl.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ta ra\u010dun SimpliSafe je \u017ee v uporabi." + }, "error": { "identifier_exists": "Ra\u010dun je \u017ee registriran", "invalid_credentials": "Neveljavne poverilnice" @@ -15,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Koda (uporablja se v uporabni\u0161kem vmesniku Home Assistant)" + }, + "title": "Konfigurirajte SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/zh-Hant.json b/homeassistant/components/simplisafe/.translations/zh-Hant.json index b456bde33c7..981fa5b59cf 100644 --- a/homeassistant/components/simplisafe/.translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/.translations/zh-Hant.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "\u9a57\u8b49\u78bc\uff08Home Assistant UI \u4f7f\u7528\uff09" + }, + "title": "\u8a2d\u5b9a SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/ko.json b/homeassistant/components/soma/.translations/ko.json index ae4d84671a3..ea79a455924 100644 --- a/homeassistant/components/soma/.translations/ko.json +++ b/homeassistant/components/soma/.translations/ko.json @@ -16,7 +16,7 @@ "host": "\ud638\uc2a4\ud2b8", "port": "\ud3ec\ud2b8" }, - "description": "SOMA Connect \uc640\uc758 \uc5f0\uacb0 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "description": "SOMA Connect \uc5f0\uacb0 \uc124\uc815\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", "title": "SOMA Connect" } }, diff --git a/homeassistant/components/tesla/.translations/ca.json b/homeassistant/components/tesla/.translations/ca.json index cb4840dea7a..2f0257d47a4 100644 --- a/homeassistant/components/tesla/.translations/ca.json +++ b/homeassistant/components/tesla/.translations/ca.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "For\u00e7a el despertar del cotxe en la posada en marxa", "scan_interval": "Segons entre escanejos" } } diff --git a/homeassistant/components/tesla/.translations/en.json b/homeassistant/components/tesla/.translations/en.json index 3c8017a7d76..4dbee73717e 100644 --- a/homeassistant/components/tesla/.translations/en.json +++ b/homeassistant/components/tesla/.translations/en.json @@ -1,31 +1,31 @@ { - "config": { - "error": { - "connection_error": "Error connecting; check network and retry", - "identifier_exists": "Email already registered", - "invalid_credentials": "Invalid credentials", - "unknown_error": "Unknown error, please report log info" - }, - "step": { - "user": { - "data": { - "username": "Email Address", - "password": "Password" + "config": { + "error": { + "connection_error": "Error connecting; check network and retry", + "identifier_exists": "Email already registered", + "invalid_credentials": "Invalid credentials", + "unknown_error": "Unknown error, please report log info" }, - "description": "Please enter your information.", - "title": "Tesla - Configuration" - } + "step": { + "user": { + "data": { + "password": "Password", + "username": "Email Address" + }, + "description": "Please enter your information.", + "title": "Tesla - Configuration" + } + }, + "title": "Tesla" }, - "title": "Tesla" - }, - "options": { - "step": { - "init": { - "data": { - "scan_interval": "Seconds between scans", - "enable_wake_on_start": "Force cars awake on startup" + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "Force cars awake on startup", + "scan_interval": "Seconds between scans" + } + } } - } } - } } \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/es.json b/homeassistant/components/tesla/.translations/es.json index 64bab24ee3f..ad456dd28b6 100644 --- a/homeassistant/components/tesla/.translations/es.json +++ b/homeassistant/components/tesla/.translations/es.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Forzar autom\u00f3viles despiertos al inicio", "scan_interval": "Segundos entre escaneos" } } diff --git a/homeassistant/components/tesla/.translations/no.json b/homeassistant/components/tesla/.translations/no.json index 0d73908f417..78311a46c26 100644 --- a/homeassistant/components/tesla/.translations/no.json +++ b/homeassistant/components/tesla/.translations/no.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Tving biler til \u00e5 v\u00e5kne ved oppstart", "scan_interval": "Sekunder mellom skanninger" } } diff --git a/homeassistant/components/tesla/.translations/ru.json b/homeassistant/components/tesla/.translations/ru.json index 15eeabf6136..5354e4e6390 100644 --- a/homeassistant/components/tesla/.translations/ru.json +++ b/homeassistant/components/tesla/.translations/ru.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "\u041f\u0440\u0438\u043d\u0443\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e \u0440\u0430\u0437\u0431\u0443\u0434\u0438\u0442\u044c \u043c\u0430\u0448\u0438\u043d\u0443 \u043f\u0440\u0438 \u0437\u0430\u043f\u0443\u0441\u043a\u0435", "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043c\u0435\u0436\u0434\u0443 \u0441\u043a\u0430\u043d\u0438\u0440\u043e\u0432\u0430\u043d\u0438\u044f\u043c\u0438 (\u0441\u0435\u043a.)" } } diff --git a/homeassistant/components/tesla/.translations/zh-Hant.json b/homeassistant/components/tesla/.translations/zh-Hant.json index 776a80da7fb..c35cbfb944a 100644 --- a/homeassistant/components/tesla/.translations/zh-Hant.json +++ b/homeassistant/components/tesla/.translations/zh-Hant.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "\u65bc\u555f\u52d5\u6642\u5f37\u5236\u559a\u9192\u6c7d\u8eca", "scan_interval": "\u6383\u63cf\u9593\u9694\u79d2\u6578" } } diff --git a/homeassistant/components/toon/.translations/sl.json b/homeassistant/components/toon/.translations/sl.json index 18c1a739e5a..8fb71b80acc 100644 --- a/homeassistant/components/toon/.translations/sl.json +++ b/homeassistant/components/toon/.translations/sl.json @@ -5,7 +5,7 @@ "client_secret": "Skrivnost iz konfiguracije odjemalca ni veljaven.", "no_agreements": "Ta ra\u010dun nima prikazov Toon.", "no_app": "Toon morate konfigurirati, preden ga boste lahko uporabili za overitev. [Preberite navodila] (https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Pri preverjanju pristnosti je pri\u0161lo do nepri\u010dakovane napake." + "unknown_auth_fail": "Med preverjanjem pristnosti je pri\u0161lo do nepri\u010dakovane napake." }, "error": { "credentials": "Navedene poverilnice niso veljavne.", diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 89d299a2857..3a6e147e867 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Credencials d'usuari incorrectes", - "service_unavailable": "Servei no disponible" + "service_unavailable": "Servei no disponible", + "unknown_client_mac": "No hi ha cap client disponible en aquesta adre\u00e7a MAC" }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "Controlador UniFi" }, + "error": { + "unknown_client_mac": "No hi ha cap client disponible a UniFi en aquesta adre\u00e7a MAC" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Clients controlats amb acc\u00e9s a la xarxa", + "new_client": "Afegeix un client nou per al control d\u2019acc\u00e9s a la xarxa" + }, + "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" + }, "device_tracker": { "data": { "detection_time": "Temps (en segons) des de s'ha vist per \u00faltima vegada fins que es considera a fora", @@ -36,12 +48,6 @@ "description": "Configuraci\u00f3 de seguiment de dispositius", "title": "Opcions d'UniFi" }, - "init": { - "data": { - "one": "un", - "other": "altre" - } - }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Crea sensors d'\u00fas d'ample de banda per a clients de la xarxa" diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 2f3db9d9b89..655000662ec 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Ung\u00fcltige Anmeldeinformationen", - "service_unavailable": "Kein Dienst verf\u00fcgbar" + "service_unavailable": "Kein Dienst verf\u00fcgbar", + "unknown_client_mac": "Unter dieser MAC-Adresse ist kein Client verf\u00fcgbar." }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "UniFi-Controller" }, + "error": { + "unknown_client_mac": "Unter dieser MAC-Adresse ist in UniFi kein Client verf\u00fcgbar" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Clients mit Netzwerkzugriffskontrolle", + "new_client": "F\u00fcgen Sie einen neuen Client f\u00fcr die Netzwerkzugangskontrolle hinzu" + }, + "description": "Konfigurieren Sie Client-Steuerelemente \n\nErstellen Sie Switches f\u00fcr Seriennummern, f\u00fcr die Sie den Netzwerkzugriff steuern m\u00f6chten.", + "title": "UniFi-Optionen 2/3" + }, "device_tracker": { "data": { "detection_time": "Zeit in Sekunden vom letzten Gesehenen bis zur Entfernung", @@ -34,7 +46,7 @@ "track_wired_clients": "Einbinden von kabelgebundenen Netzwerk-Clients" }, "description": "Konfigurieren Sie die Ger\u00e4teverfolgung", - "title": "UniFi-Optionen" + "title": "UniFi-Optionen 1/3" }, "init": { "data": { @@ -44,10 +56,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Erstellen von Bandbreiten-Nutzungssensoren f\u00fcr Netzwerk-Clients" + "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients" }, "description": "Konfigurieren Sie Statistiksensoren", - "title": "UniFi-Optionen" + "title": "UniFi-Optionen 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 9ac01e514bf..8fdde34470b 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -24,8 +24,19 @@ }, "title": "UniFi Controller" }, + "error": { + "unknown_client_mac": "No client available in UniFi on that MAC address" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Network access controlled clients", + "new_client": "Add new client for network access control" + }, + "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", + "title": "UniFi options 2/3" + }, "device_tracker": { "data": { "detection_time": "Time in seconds from last seen until considered away", @@ -37,14 +48,6 @@ "description": "Configure device tracking", "title": "UniFi options 1/3" }, - "client_control": { - "data": { - "block_client": "Network access controlled clients", - "new_client": "Add new client (MAC) for network access control" - }, - "description": "Configure client controls\n\nCreate switches for serial numbers you want to control network access for.", - "title": "UniFi options 2/3" - }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandwidth usage sensors for network clients" @@ -52,9 +55,6 @@ "description": "Configure statistics sensors", "title": "UniFi options 3/3" } - }, - "error": { - "unknown_client_mac": "No client available in UniFi on that MAC address" } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index 6c5e9d677c2..b6713bb09bb 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Credenciales de usuario incorrectas", - "service_unavailable": "Servicio No disponible" + "service_unavailable": "Servicio No disponible", + "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "Controlador UniFi" }, + "error": { + "unknown_client_mac": "Ning\u00fan cliente disponible en UniFi en esa direcci\u00f3n MAC" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Clientes con acceso controlado a la red", + "new_client": "A\u00f1adir nuevo cliente para el control de acceso a la red" + }, + "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", + "title": "Opciones UniFi 2/3" + }, "device_tracker": { "data": { "detection_time": "Tiempo en segundos desde la \u00faltima vez que se vio hasta considerarlo desconectado", @@ -34,7 +46,7 @@ "track_wired_clients": "Incluir clientes de red cableada" }, "description": "Configurar dispositivo de seguimiento", - "title": "Opciones UniFi" + "title": "Opciones UniFi 1/3" }, "init": { "data": { @@ -47,7 +59,7 @@ "allow_bandwidth_sensors": "Crear sensores para monitorizar uso de ancho de banda de clientes de red" }, "description": "Configurar estad\u00edsticas de los sensores", - "title": "Opciones UniFi" + "title": "Opciones UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 0a100be0a11..3b8a2996887 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Mauvaises informations d'identification de l'utilisateur", - "service_unavailable": "Aucun service disponible" + "service_unavailable": "Aucun service disponible", + "unknown_client_mac": "Aucun client disponible sur cette adresse MAC" }, "step": { "user": { @@ -23,8 +24,18 @@ }, "title": "Contr\u00f4leur UniFi" }, + "error": { + "unknown_client_mac": "Aucun client disponible dans UniFi sur cette adresse MAC" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Clients contr\u00f4l\u00e9s par acc\u00e8s r\u00e9seau", + "new_client": "Ajouter un nouveau client pour le contr\u00f4le d'acc\u00e8s au r\u00e9seau" + }, + "title": "Options UniFi 2/3" + }, "device_tracker": { "data": { "detection_time": "Temps en secondes depuis la derni\u00e8re vue avant de consid\u00e9rer comme absent", diff --git a/homeassistant/components/unifi/.translations/it.json b/homeassistant/components/unifi/.translations/it.json index c1aa9afe54f..9439715fe79 100644 --- a/homeassistant/components/unifi/.translations/it.json +++ b/homeassistant/components/unifi/.translations/it.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Credenziali utente non valide", - "service_unavailable": "Servizio non disponibile" + "service_unavailable": "Servizio non disponibile", + "unknown_client_mac": "Nessun client disponibile su quell'indirizzo MAC" }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "UniFi Controller" }, + "error": { + "unknown_client_mac": "Nessun client disponibile in UniFi su quell'indirizzo MAC" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Client controllati per l'accesso alla rete", + "new_client": "Aggiungere un nuovo client per il controllo dell'accesso alla rete" + }, + "description": "Configurare i controlli client \n\nCreare interruttori per i numeri di serie dei quali si desidera controllare l'accesso alla rete.", + "title": "Opzioni UniFi 2/3" + }, "device_tracker": { "data": { "detection_time": "Tempo in secondi dall'ultima volta che viene visto fino a quando non \u00e8 considerato lontano", @@ -34,12 +46,12 @@ "track_wired_clients": "Includi i client di rete cablata" }, "description": "Configurare il tracciamento del dispositivo", - "title": "Opzioni UniFi" + "title": "Opzioni UniFi 1/3" }, "init": { "data": { "one": "uno", - "other": "altro" + "other": "altri" } }, "statistics_sensors": { @@ -47,7 +59,7 @@ "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete" }, "description": "Configurare i sensori delle statistiche", - "title": "Opzioni UniFi" + "title": "Opzioni UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index dbcd4d7feee..5c45e272e91 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "\uc0ac\uc6a9\uc790 \uc790\uaca9\uc99d\uba85\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "service_unavailable": "\uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4" + "service_unavailable": "\uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \uc11c\ube44\uc2a4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", + "unknown_client_mac": "\ud574\ub2f9 MAC \uc8fc\uc18c\uc5d0\uc11c \uc0ac\uc6a9 \uac00\ub2a5\ud55c \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "UniFi \ucee8\ud2b8\ub864\ub7ec" }, + "error": { + "unknown_client_mac": "\ud574\ub2f9 MAC \uc8fc\uc18c\uc758 UniFi \uc5d0\uc11c \uc0ac\uc6a9\ud560 \uc218 \uc788\ub294 \ud074\ub77c\uc774\uc5b8\ud2b8\uac00 \uc5c6\uc2b5\ub2c8\ub2e4." + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", + "new_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4\ub97c \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uac00" + }, + "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4\ub97c \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.", + "title": "UniFi \uc635\uc158 2/3" + }, "device_tracker": { "data": { "detection_time": "\ub9c8\uc9c0\ub9c9\uc73c\ub85c \ud655\uc778\ub41c \uc2dc\uac04\ubd80\ud130 \uc678\ucd9c \uc0c1\ud0dc\ub85c \uac04\uc8fc\ub418\ub294 \uc2dc\uac04 (\ucd08)", @@ -33,15 +45,15 @@ "track_devices": "\ub124\ud2b8\uc6cc\ud06c \uae30\uae30 \ucd94\uc801 (Ubiquiti \uae30\uae30)", "track_wired_clients": "\uc720\uc120 \ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ud3ec\ud568" }, - "description": "\uc7a5\uce58 \ucd94\uc801 \uad6c\uc131", - "title": "UniFi \uc635\uc158" + "description": "\uae30\uae30 \ucd94\uc801 \uad6c\uc131", + "title": "UniFi \uc635\uc158 1/3" }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\ub124\ud2b8\uc6cc\ud06c \ud074\ub77c\uc774\uc5b8\ud2b8 \ub300\uc5ed\ud3ed \uc0ac\uc6a9\ub7c9 \uc13c\uc11c" }, "description": "\ud1b5\uacc4 \uc13c\uc11c \uad6c\uc131", - "title": "UniFi \uc635\uc158" + "title": "UniFi \uc635\uc158 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 9707432540d..13bb40dd25e 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -25,6 +25,9 @@ }, "options": { "step": { + "client_control": { + "title": "UniFi Optiounen 2/3" + }, "device_tracker": { "data": { "detection_time": "Z\u00e4it a Sekonne vum leschten Z\u00e4itpunkt un bis den Apparat als \u00ebnnerwee consider\u00e9iert g\u00ebtt", diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index 65730c7ab8b..f6d3d250e31 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Ugyldig brukerlegitimasjon", - "service_unavailable": "Ingen tjeneste tilgjengelig" + "service_unavailable": "Ingen tjeneste tilgjengelig", + "unknown_client_mac": "Ingen klient tilgjengelig p\u00e5 den MAC-adressen" }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "UniFi kontroller" }, + "error": { + "unknown_client_mac": "Ingen klient tilgjengelig i UniFi p\u00e5 den MAC-adressen" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Nettverkskontrollerte klienter", + "new_client": "Legg til ny klient for nettverkstilgangskontroll" + }, + "description": "Konfigurere klient-kontroller\n\nOpprette brytere for serienumre du \u00f8nsker \u00e5 kontrollere tilgang til nettverk for.", + "title": "UniFi-alternativ 2/3" + }, "device_tracker": { "data": { "detection_time": "Tid i sekunder fra sist sett til den ble ansett borte", @@ -34,14 +46,14 @@ "track_wired_clients": "Inkluder kablede nettverksklienter" }, "description": "Konfigurere enhetssporing", - "title": "UniFi-alternativer" + "title": "UniFi-alternativ 1/3" }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "B\u00e5ndbreddebrukssensorer for nettverksklienter" }, "description": "Konfigurer statistikk sensorer", - "title": "UniFi-alternativer" + "title": "UniFi-alternativ 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/pl.json b/homeassistant/components/unifi/.translations/pl.json index e016fbc7cce..08329aed574 100644 --- a/homeassistant/components/unifi/.translations/pl.json +++ b/homeassistant/components/unifi/.translations/pl.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "B\u0142\u0119dne dane uwierzytelniaj\u0105ce", - "service_unavailable": "Brak dost\u0119pnych us\u0142ug" + "service_unavailable": "Brak dost\u0119pnych us\u0142ug", + "unknown_client_mac": "Brak klienta z tym adresem MAC" }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "Kontroler UniFi" }, + "error": { + "unknown_client_mac": "Brak klienta w UniFi z tym adresem MAC" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Klienci z kontrol\u0105 dost\u0119pu do sieci", + "new_client": "Dodaj nowego klienta do kontroli dost\u0119pu do sieci" + }, + "description": "Konfigurowanie kontroli klienta\n\nUtw\u00f3rz prze\u0142\u0105czniki dla numer\u00f3w seryjnych, dla kt\u00f3rych chcesz kontrolowa\u0107 dost\u0119p do sieci.", + "title": "UniFi opcje 2/3" + }, "device_tracker": { "data": { "detection_time": "Czas w sekundach od momentu, kiedy ostatnio widziano, a\u017c do momentu, kiedy uznano go za nieobecny.", diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index 0080474cf64..b73c6b9c2fb 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0443\u0447\u0451\u0442\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435.", - "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430." + "service_unavailable": "\u0421\u043b\u0443\u0436\u0431\u0430 \u043d\u0435 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u0430.", + "unknown_client_mac": "\u041d\u0435\u0442 \u0434\u043e\u0441\u0442\u0443\u043f\u043d\u044b\u0445 \u043a\u043b\u0438\u0435\u043d\u0442\u043e\u0432 \u043d\u0430 \u044d\u0442\u043e\u043c MAC-\u0430\u0434\u0440\u0435\u0441\u0435." }, "step": { "user": { @@ -25,6 +26,13 @@ }, "options": { "step": { + "client_control": { + "data": { + "block_client": "\u041a\u043b\u0438\u0435\u043d\u0442\u044b \u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "new_client": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + }, + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 2" + }, "device_tracker": { "data": { "detection_time": "\u0412\u0440\u0435\u043c\u044f \u043e\u0442 \u043f\u043e\u0441\u043b\u0435\u0434\u043d\u0435\u0433\u043e \u0441\u0435\u0430\u043d\u0441\u0430 \u0441\u0432\u044f\u0437\u0438 \u0441 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c (\u0441\u0435\u043a.), \u043f\u043e \u0438\u0441\u0442\u0435\u0447\u0435\u043d\u0438\u044e \u043a\u043e\u0442\u043e\u0440\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442 \u0441\u0442\u0430\u0442\u0443\u0441 \"\u041d\u0435 \u0434\u043e\u043c\u0430\".", @@ -34,7 +42,7 @@ "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" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 1" }, "init": { "data": { @@ -49,7 +57,7 @@ "allow_bandwidth_sensors": "\u0421\u0435\u043d\u0441\u043e\u0440\u044b \u043f\u043e\u043b\u043e\u0441\u044b \u043f\u0440\u043e\u043f\u0443\u0441\u043a\u0430\u043d\u0438\u044f \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" + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 3" } } } diff --git a/homeassistant/components/unifi/.translations/sl.json b/homeassistant/components/unifi/.translations/sl.json index 7084c4609c5..a2c37f027b2 100644 --- a/homeassistant/components/unifi/.translations/sl.json +++ b/homeassistant/components/unifi/.translations/sl.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Napa\u010dni uporabni\u0161ki podatki", - "service_unavailable": "Nobena storitev ni na voljo" + "service_unavailable": "Nobena storitev ni na voljo", + "unknown_client_mac": "Na tem MAC naslovu ni na voljo nobenega odjemalca" }, "step": { "user": { @@ -23,15 +24,29 @@ }, "title": "UniFi Krmilnik" }, + "error": { + "unknown_client_mac": "V UniFi na tem naslovu MAC ni na voljo nobenega odjemalca" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "Odjemalci pod nadzorom dostopa do omre\u017eja", + "new_client": "Dodajte novega odjemalca za nadzor dostopa do omre\u017eja" + }, + "description": "Konfigurirajte nadzor odjemalcev \n\n Ustvarite stikala za serijske \u0161tevilke, za katere \u017eelite nadzirati dostop do omre\u017eja.", + "title": "Mo\u017enosti UniFi 2/3" + }, "device_tracker": { "data": { "detection_time": "\u010cas v sekundah od zadnjega videnja na omre\u017eju do odsotnosti", + "ssid_filter": "Izberite SSID-e za sledenje brez\u017ei\u010dnim odjemalcem", "track_clients": "Sledite odjemalcem omre\u017eja", "track_devices": "Sledite omre\u017enim napravam (naprave Ubiquiti)", "track_wired_clients": "Vklju\u010dite kliente iz o\u017ei\u010denega omre\u017eja" - } + }, + "description": "Konfigurirajte sledenje napravam", + "title": "Mo\u017enosti UniFi 1/3" }, "init": { "data": { @@ -43,8 +58,10 @@ }, "statistics_sensors": { "data": { - "allow_bandwidth_sensors": "Ustvarite senzorje porabe pasovne \u0161irine za omre\u017ene odjemalce" - } + "allow_bandwidth_sensors": "Senzorji uporabe pasovne \u0161irine za omre\u017ene odjemalce" + }, + "description": "Konfigurirajte statisti\u010dne senzorje", + "title": "Mo\u017enosti UniFi 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index cce150a6765..ca920abd492 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "\u4f7f\u7528\u8005\u6191\u8b49\u7121\u6548", - "service_unavailable": "\u7121\u670d\u52d9\u53ef\u7528" + "service_unavailable": "\u7121\u670d\u52d9\u53ef\u7528", + "unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528\u5ba2\u6236\u7aef" }, "step": { "user": { @@ -23,8 +24,19 @@ }, "title": "UniFi \u63a7\u5236\u5668" }, + "error": { + "unknown_client_mac": "\u8a72 Mac \u4f4d\u5740\u7121\u53ef\u7528 UniFi \u5ba2\u6236\u7aef" + }, "options": { "step": { + "client_control": { + "data": { + "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", + "new_client": "\u65b0\u589e\u9396\u8981\u63a7\u5236\u7db2\u8def\u5b58\u53d6\u7684\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" + }, "device_tracker": { "data": { "detection_time": "\u6700\u7d42\u51fa\u73fe\u5f8c\u8996\u70ba\u96e2\u958b\u7684\u6642\u9593\uff08\u4ee5\u79d2\u70ba\u55ae\u4f4d\uff09", @@ -34,14 +46,14 @@ "track_wired_clients": "\u5305\u542b\u6709\u7dda\u7db2\u8def\u5ba2\u6236\u7aef" }, "description": "\u8a2d\u5b9a\u8a2d\u5099\u8ffd\u8e64", - "title": "UniFi \u9078\u9805" + "title": "UniFi \u9078\u9805 1/3" }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "\u7db2\u8def\u5ba2\u6236\u7aef\u983b\u5bec\u7528\u91cf\u611f\u61c9\u5668" }, "description": "\u8a2d\u5b9a\u7d71\u8a08\u6578\u64da\u611f\u61c9\u5668", - "title": "UniFi \u9078\u9805" + "title": "UniFi \u9078\u9805 3/3" } } } diff --git a/homeassistant/components/vilfo/.translations/de.json b/homeassistant/components/vilfo/.translations/de.json index 9c0f938b679..fed2265def2 100644 --- a/homeassistant/components/vilfo/.translations/de.json +++ b/homeassistant/components/vilfo/.translations/de.json @@ -14,6 +14,7 @@ "access_token": "Zugriffstoken f\u00fcr die Vilfo Router-API", "host": "Router-Hostname oder IP" }, + "description": "Richten Sie die Vilfo Router-Integration ein. Sie ben\u00f6tigen Ihren Vilfo Router-Hostnamen / Ihre IP-Adresse und ein API-Zugriffstoken. Weitere Informationen zu dieser Integration und wie Sie diese Details erhalten, finden Sie unter: https://www.home-assistant.io/integrations/vilfo", "title": "Stellen Sie eine Verbindung zum Vilfo Router her" } }, diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index 007834a08e3..c8cfa563779 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -22,13 +22,24 @@ "data": { "pin": "PIN" }, - "description": "El televisor hauria d'estar mostrant un codi. Introdueix aquest codi al formulari i segueix amb els seg\u00fcents passos per completar l'emparellament." + "description": "El televisor hauria d'estar mostrant un codi. Introdueix aquest codi al formulari i segueix amb els seg\u00fcents passos per completar l'emparellament.", + "title": "Proc\u00e9s d'aparellament complet" }, "pairing_complete": { - "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant." + "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant.", + "title": "Emparellament completat" }, "pairing_complete_import": { - "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant.\n\nEl testimoni d'acc\u00e9s (Access Token) \u00e9s '**{access_token}**'." + "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant.\n\nEl testimoni d'acc\u00e9s (Access Token) \u00e9s '**{access_token}**'.", + "title": "Emparellament completat" + }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Aplicacions a incloure o excloure", + "include_or_exclude": "Incloure o excloure aplicacions?" + }, + "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista. Pots ometre aquest pas si el teu televisor no admet aplicacions.", + "title": "Configuraci\u00f3 d'Aplicacions per a Smart TV" }, "user": { "data": { @@ -37,7 +48,16 @@ "host": ":", "name": "Nom" }, + "description": "Nom\u00e9s es necessita testimoni d'acc\u00e9s per als televisors. Si est\u00e0s configurant un televisor i encara no tens un testimoni d'acc\u00e9s, deixeu-ho en blanc per poder fer el proc\u00e9s d'emparellament.", "title": "Configuraci\u00f3 del client de Vizio SmartCast" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Aplicacions a incloure o excloure", + "include_or_exclude": "Incloure o excloure aplicacions?" + }, + "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista. Pots ometre aquest pas si el teu televisor no admet aplicacions.", + "title": "Configuraci\u00f3 d'Aplicacions per a Smart TV" } }, "title": "Vizio SmartCast" @@ -46,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Aplicacions a incloure o excloure", + "include_or_exclude": "Incloure o excloure aplicacions?", "timeout": "Temps d'espera de les sol\u00b7licituds API (en segons)", "volume_step": "Mida del pas de volum" }, + "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista.", "title": "Actualitzaci\u00f3 de les opcions de Vizo SmartCast" } }, diff --git a/homeassistant/components/vizio/.translations/de.json b/homeassistant/components/vizio/.translations/de.json index 6162a27805e..f7c0916ef7b 100644 --- a/homeassistant/components/vizio/.translations/de.json +++ b/homeassistant/components/vizio/.translations/de.json @@ -3,15 +3,18 @@ "abort": { "already_in_progress": "Konfigurationsablauf f\u00fcr die Vizio-Komponente wird bereits ausgef\u00fchrt.", "already_setup": "Dieser Eintrag wurde bereits eingerichtet.", + "already_setup_with_diff_host_and_name": "Dieser Eintrag scheint bereits mit einem anderen Host und Namen basierend auf seiner Seriennummer eingerichtet worden zu sein. Bitte entfernen Sie alle alten Eintr\u00e4ge aus Ihrer configuration.yaml und aus dem Men\u00fc Integrationen, bevor Sie erneut versuchen, dieses Ger\u00e4t hinzuzuf\u00fcgen.", "host_exists": "Vizio-Komponent mit bereits konfiguriertem Host.", "name_exists": "Vizio-Komponent mit bereits konfiguriertem Namen.", + "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde.", "updated_options": "Dieser Eintrag wurde bereits eingerichtet, aber die in der Konfiguration definierten Optionen stimmen nicht mit den zuvor importierten Optionswerten \u00fcberein, daher wurde der Konfigurationseintrag entsprechend aktualisiert.", "updated_volume_step": "Dieser Eintrag wurde bereits eingerichtet, aber die Lautst\u00e4rken-Schrittgr\u00f6\u00dfe in der Konfiguration stimmt nicht mit dem Konfigurationseintrag \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { "cant_connect": "Es konnte keine Verbindung zum Ger\u00e4t hergestellt werden. [\u00dcberpr\u00fcfen Sie die Dokumentation] (https://www.home-assistant.io/integrations/vizio/) und \u00fcberpr\u00fcfen Sie Folgendes erneut:\n- Das Ger\u00e4t ist eingeschaltet\n- Das Ger\u00e4t ist mit dem Netzwerk verbunden\n- Die von Ihnen eingegebenen Werte sind korrekt\nbevor sie versuchen, erneut zu \u00fcbermitteln.", - "host_exists": "Host bereits konfiguriert.", - "name_exists": "Name bereits konfiguriert.", + "complete_pairing failed": "Das Pairing kann nicht abgeschlossen werden. Stellen Sie sicher, dass die von Ihnen angegebene PIN korrekt ist und das Fernsehger\u00e4t weiterhin mit Strom versorgt und mit dem Netzwerk verbunden ist, bevor Sie es erneut versuchen.", + "host_exists": "Vizio-Ger\u00e4t mit angegebenem Host bereits konfiguriert.", + "name_exists": "Vizio-Ger\u00e4t mit angegebenem Namen bereits konfiguriert.", "tv_needs_token": "Wenn der Ger\u00e4tetyp \"TV\" ist, wird ein g\u00fcltiger Zugriffstoken ben\u00f6tigt." }, "step": { @@ -19,14 +22,25 @@ "data": { "pin": "PIN" }, + "description": "Ihr Fernseher sollte einen Code anzeigen. Geben Sie diesen Code in das Formular ein und fahren Sie mit dem n\u00e4chsten Schritt fort, um die Kopplung abzuschlie\u00dfen.", "title": "Schlie\u00dfen Sie den Pairing-Prozess ab" }, "pairing_complete": { + "description": "Ihr Vizio SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.", "title": "Kopplung abgeschlossen" }, "pairing_complete_import": { + "description": "Ihr Vizio SmartCast-Fernseher ist jetzt mit Home Assistant verbunden. \n\n Ihr Zugriffstoken ist '**{access_token}**'.", "title": "Kopplung abgeschlossen" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Apps zum Einschlie\u00dfen oder Ausschlie\u00dfen", + "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?" + }, + "description": "Wenn Sie \u00fcber ein Smart TV verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen. Sie k\u00f6nnen diesen Schritt f\u00fcr Fernsehger\u00e4te \u00fcberspringen, die keine Apps unterst\u00fctzen.", + "title": "Konfigurieren Sie Apps f\u00fcr Ihr Smart TV" + }, "user": { "data": { "access_token": "Zugangstoken", @@ -34,7 +48,16 @@ "host": ":", "name": "Name" }, - "title": "Richten Sie den Vizio SmartCast-Client ein" + "description": "Ein Zugriffstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn Sie ein Fernsehger\u00e4t konfigurieren und noch kein Zugriffstoken haben, lassen Sie es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", + "title": "Richten Sie das Vizio SmartCast-Ger\u00e4t ein" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Apps zum Einschlie\u00dfen oder Ausschlie\u00dfen", + "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?" + }, + "description": "Wenn Sie \u00fcber ein Smart TV verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen. Sie k\u00f6nnen diesen Schritt f\u00fcr Fernsehger\u00e4te \u00fcberspringen, die keine Apps unterst\u00fctzen.", + "title": "Konfigurieren Sie Apps f\u00fcr Ihr Smart TV" } }, "title": "Vizio SmartCast" @@ -43,6 +66,8 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Apps zum Einschlie\u00dfen oder Ausschlie\u00dfen", + "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?", "timeout": "API Request Timeout (Sekunden)", "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index 294025fddc8..ec82f41c079 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", "host_exists": "Vizio component with host already configured.", "name_exists": "Vizio component with name already configured.", - "updated_entry": "This entry has already been setup but the name and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly.", + "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly.", "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.", "updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly." }, @@ -30,9 +30,17 @@ "title": "Pairing Complete" }, "pairing_complete_import": { - "description": "Your Vizio SmartCast device is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'.", + "description": "Your Vizio SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'.", "title": "Pairing Complete" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Apps to Include or Exclude", + "include_or_exclude": "Include or Exclude Apps?" + }, + "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list. You can skip this step for TVs that don't support apps.", + "title": "Configure Apps for Smart TV" + }, "user": { "data": { "access_token": "Access Token", @@ -40,8 +48,16 @@ "host": ":", "name": "Name" }, - "description": "All fields are required except Access Token. If you choose not to provide an Access Token, and your Device Type is 'tv', you will go through a pairing process with your device so an Access Token can be retrieved.\n\nTo go through the pairing process, before clicking Submit, ensure your TV is powered on and connected to the network. You also need to be able to see the screen.", + "description": "An Access Token is only needed for TVs. If you are configuring a TV and do not have an Access Token yet, leave it blank to go through a pairing process.", "title": "Setup Vizio SmartCast Device" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Apps to Include or Exclude", + "include_or_exclude": "Include or Exclude Apps?" + }, + "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list. You can skip this step for TVs that don't support apps.", + "title": "Configure Apps for Smart TV" } }, "title": "Vizio SmartCast" @@ -50,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Apps to Include or Exclude", + "include_or_exclude": "Include or Exclude Apps?", "timeout": "API Request Timeout (seconds)", "volume_step": "Volume Step Size" }, + "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list.", "title": "Update Vizo SmartCast Options" } }, diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json index af3cc1750ab..68e855fa5a8 100644 --- a/homeassistant/components/vizio/.translations/es.json +++ b/homeassistant/components/vizio/.translations/es.json @@ -30,9 +30,17 @@ "title": "Emparejamiento Completado" }, "pairing_complete_import": { - "description": "Su dispositivo Vizio SmartCast ahora est\u00e1 conectado a Home Assistant.\n\nTu Token de Acceso es '**{access_token}**'.", + "description": "Su dispositivo Vizio SmartCast TV ahora est\u00e1 conectado a Home Assistant.\n\nEl Token de Acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", + "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?" + }, + "description": "Si tiene un Smart TV, opcionalmente puede filtrar su lista de origen eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de origen. Puede omitir este paso para televisores que no admiten aplicaciones.", + "title": "Configurar aplicaciones para Smart TV" + }, "user": { "data": { "access_token": "Token de acceso", @@ -42,6 +50,14 @@ }, "description": "Todos los campos son obligatorios excepto el Token de Acceso. Si decides no proporcionar un Token de Acceso y tu Tipo de Dispositivo es \"tv\", se te llevar\u00e1 por un proceso de emparejamiento con tu dispositivo para que se pueda recuperar un Token de Acceso.\n\nPara pasar por el proceso de emparejamiento, antes de pulsar en Enviar, aseg\u00farese de que tu TV est\u00e9 encendida y conectada a la red. Tambi\u00e9n es necesario poder ver la pantalla.", "title": "Configurar el cliente de Vizio SmartCast" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", + "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?" + }, + "description": "Si tienes un Smart TV, puedes opcionalmente filtrar tu lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en tu lista de fuentes. Puedes omitir este paso para televisores que no admiten aplicaciones.", + "title": "Configurar aplicaciones para Smart TV" } }, "title": "Vizio SmartCast" @@ -50,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", + "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?", "timeout": "Tiempo de espera de solicitud de API (segundos)", "volume_step": "Tama\u00f1o del paso de volumen" }, + "description": "Si tienes un Smart TV, opcionalmente puedes filtrar su lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de fuentes.", "title": "Actualizar las opciones de SmartCast de Vizo" } }, diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index cf0cdea787f..344fadd00f8 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -12,11 +12,31 @@ }, "error": { "cant_connect": "Impossible de se connecter \u00e0 l'appareil. [V\u00e9rifier les documents](https://www.home-assistant.io/integrations/vizio/) et rev\u00e9rifier que:\n- L'appareil est sous tension\n- L'appareil est connect\u00e9 au r\u00e9seau\n- Les valeurs que vous avez saisies sont exactes\navant d'essayer de le soumettre \u00e0 nouveau.", + "complete_pairing failed": "Impossible de terminer l'appariement. Assurez-vous que le code PIN que vous avez fourni est correct et que le t\u00e9l\u00e9viseur est toujours aliment\u00e9 et connect\u00e9 au r\u00e9seau avant de soumettre \u00e0 nouveau.", "host_exists": "H\u00f4te d\u00e9j\u00e0 configur\u00e9.", "name_exists": "Nom d\u00e9j\u00e0 configur\u00e9.", "tv_needs_token": "Lorsque le type de p\u00e9riph\u00e9rique est \" TV \", un jeton d'acc\u00e8s valide est requis." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + } + }, + "pairing_complete": { + "description": "Votre appareil Vizio SmartCast est maintenant connect\u00e9 \u00e0 Home Assistant.", + "title": "Appairage termin\u00e9" + }, + "pairing_complete_import": { + "title": "Appairage termin\u00e9" + }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Applications \u00e0 inclure ou \u00e0 exclure", + "include_or_exclude": "Inclure ou exclure des applications?" + }, + "title": "Configurer les applications pour Smart TV" + }, "user": { "data": { "access_token": "Jeton d'acc\u00e8s", @@ -24,7 +44,15 @@ "host": ":", "name": "Nom" }, + "description": "Un jeton d'acc\u00e8s n'est n\u00e9cessaire que pour les t\u00e9l\u00e9viseurs. Si vous configurez un t\u00e9l\u00e9viseur et que vous n'avez pas encore de jeton d'acc\u00e8s, laissez-le vide pour passer par un processus de couplage.", "title": "Configurer le client Vizio SmartCast" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Applications \u00e0 inclure ou \u00e0 exclure", + "include_or_exclude": "Inclure ou exclure des applications?" + }, + "title": "Configurer les applications pour Smart TV" } }, "title": "Vizio SmartCast" @@ -33,9 +61,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Applications \u00e0 inclure ou \u00e0 exclure", + "include_or_exclude": "Inclure ou exclure des applications?", "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)", "volume_step": "Taille du pas de volume" }, + "description": "Si vous avez une Smart TV, vous pouvez \u00e9ventuellement filtrer votre liste de sources en choisissant les applications \u00e0 inclure ou \u00e0 exclure dans votre liste de sources.", "title": "Mettre \u00e0 jour les options de Vizo SmartCast" } }, diff --git a/homeassistant/components/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index 77c905d7cf5..eef86bf78cb 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "Sembra che questa voce sia gi\u00e0 stata configurata con un host e un nome diversi in base al suo numero seriale. Rimuovere eventuali voci precedenti da configuration.yaml e dal menu Integrazioni prima di tentare nuovamente di aggiungere questo dispositivo.", "host_exists": "Componente Vizio con host gi\u00e0 configurato.", "name_exists": "Componente Vizio con nome gi\u00e0 configurato.", - "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza.", + "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome, le app e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza.", "updated_options": "Questa voce \u00e8 gi\u00e0 stata impostata, ma le opzioni definite nella configurazione non corrispondono ai valori delle opzioni importate in precedenza, quindi la voce di configurazione \u00e8 stata aggiornata di conseguenza.", "updated_volume_step": "Questa voce \u00e8 gi\u00e0 stata impostata, ma la dimensione del passo del volume nella configurazione non corrisponde alla voce di configurazione, quindi \u00e8 stata aggiornata di conseguenza." }, @@ -30,9 +30,17 @@ "title": "Associazione completata" }, "pairing_complete_import": { - "description": "Il dispositivo Vizio SmartCast \u00e8 ora connesso a Home Assistant. \n\n Il token di accesso \u00e8 '**{access_token}**'.", + "description": "Il dispositivo Vizio SmartCast TV \u00e8 ora connesso a Home Assistant. \n\nIl tuo Token di Accesso \u00e8 '**{access_token}**'.", "title": "Associazione completata" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "App da includere o escludere", + "include_or_exclude": "Includere o Escludere le App?" + }, + "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare facoltativamente l'elenco di origine scegliendo le app da includere o escludere in esso. \u00c8 possibile saltare questo passaggio per i televisori che non supportano le app.", + "title": "Configura le app per Smart TV" + }, "user": { "data": { "access_token": "Token di accesso", @@ -40,8 +48,16 @@ "host": "< Host / IP >: ", "name": "Nome" }, - "description": "Tutti i campi sono obbligatori tranne il token di accesso. Se si sceglie di non fornire un token di accesso e il tipo di dispositivo \u00e8 \"tv\", si passer\u00e0 attraverso un processo di associazione con il dispositivo in modo da poter recuperare un token di accesso. \n\n Per completare il processo di associazione, prima di fare clic su Invia, assicurarsi che il televisore sia acceso e collegato alla rete. Devi anche essere in grado di vedere lo schermo.", + "description": "Un Token di Accesso \u00e8 necessario solo per i televisori. Se si sta configurando un televisore e non si dispone ancora di un Token di Accesso, lasciarlo vuoto per passare attraverso un processo di associazione.", "title": "Configurazione del dispositivo SmartCast Vizio" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "App da Includere o Escludere", + "include_or_exclude": "Includere o Escludere le App?" + }, + "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare facoltativamente l'elenco di origine scegliendo le app da includere o escludere in esso. \u00c8 possibile saltare questo passaggio per i televisori che non supportano le app.", + "title": "Configura le app per Smart TV" } }, "title": "Vizio SmartCast" @@ -50,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "App da includere o escludere", + "include_or_exclude": "Includere o escludere app?", "timeout": "Timeout richiesta API (secondi)", "volume_step": "Dimensione del passo del volume" }, + "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare l'elenco di origine scegliendo le app da includere o escludere in esso.", "title": "Aggiornamento delle opzioni di Vizo SmartCast" } }, diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json index 64c0887b3f8..33edb72733a 100644 --- a/homeassistant/components/vizio/.translations/ko.json +++ b/homeassistant/components/vizio/.translations/ko.json @@ -6,17 +6,41 @@ "already_setup_with_diff_host_and_name": "\uc774 \ud56d\ubaa9\uc740 \uc2dc\ub9ac\uc5bc \ubc88\ud638\ub85c \ub2e4\ub978 \ud638\uc2a4\ud2b8 \ubc0f \uc774\ub984\uc73c\ub85c \uc774\ubbf8 \uc124\uc815\ub418\uc5b4\uc788\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4. \uc774 \uae30\uae30\ub97c \ucd94\uac00\ud558\uae30 \uc804\uc5d0 configuration.yaml \ubc0f \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uba54\ub274\uc5d0\uc11c \uc774\uc804 \ud56d\ubaa9\uc744 \uc81c\uac70\ud574\uc8fc\uc138\uc694.", "host_exists": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "name_exists": "\ud574\ub2f9 \uc774\ub984\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984\uc774\ub098 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "updated_options": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uc635\uc158 \uac12\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "updated_volume_step": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc758 \ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30\uac00 \uad6c\uc131 \ud56d\ubaa9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "cant_connect": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. [\uc548\ub0b4\ub97c \ucc38\uace0] (https://www.home-assistant.io/integrations/vizio/)\ud558\uace0 \uc591\uc2dd\uc744 \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \ub2e4\uc74c\uc744 \ub2e4\uc2dc \ud655\uc778\ud574\uc8fc\uc138\uc694.\n- \uae30\uae30 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uc2b5\ub2c8\uae4c?\n- \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc2b5\ub2c8\uae4c?\n- \uc785\ub825\ud55c \ub0b4\uc6a9\uc774 \uc62c\ubc14\ub985\ub2c8\uae4c?", + "complete_pairing failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc74c \uacfc\uc815\uc744 \uc9c4\ud589\ud558\uae30 \uc804\uc5d0 TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "tv_needs_token": "\uae30\uae30 \uc720\ud615\uc774 'tv' \uc778 \uacbd\uc6b0 \uc720\ud6a8\ud55c \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "description": "TV \uc5d0 \ucf54\ub4dc\uac00 \ud45c\uc2dc\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \ud574\ub2f9 \ucf54\ub4dc\ub97c \uc591\uc2dd\uc5d0 \uc785\ub825\ud55c \ud6c4 \ub2e4\uc74c \ub2e8\uacc4\ub97c \uacc4\uc18d\ud558\uc5ec \ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud574\uc8fc\uc138\uc694.", + "title": "\ud398\uc5b4\ub9c1 \uacfc\uc815 \uc644\ub8cc" + }, + "pairing_complete": { + "description": "Vizio SmartCast \uae30\uae30\uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" + }, + "pairing_complete_import": { + "description": "Vizio SmartCast TV \uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.", + "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" + }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", + "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc571\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 TV\uc758 \uacbd\uc6b0 \uc774 \ub2e8\uacc4\ub97c \uac74\ub108\ub6f8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\uc2a4\ub9c8\ud2b8 TV \uc6a9 \uc571 \uad6c\uc131" + }, "user": { "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", @@ -24,7 +48,16 @@ "host": "<\ud638\uc2a4\ud2b8/ip>:", "name": "\uc774\ub984" }, + "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV \uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV \ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.", "title": "Vizio SmartCast \uae30\uae30 \uc124\uc815" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", + "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" + }, + "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc571\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 TV\uc758 \uacbd\uc6b0 \uc774 \ub2e8\uacc4\ub97c \uac74\ub108\ub6f8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", + "title": "\uc2a4\ub9c8\ud2b8 TV \uc6a9 \uc571 \uad6c\uc131" } }, "title": "Vizio SmartCast" @@ -33,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", + "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "timeout": "API \uc694\uccad \uc2dc\uac04 \ucd08\uacfc (\ucd08)", "volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30" }, + "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", "title": "Vizo SmartCast \uc635\uc158 \uc5c5\ub370\uc774\ud2b8" } }, diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index dababdd53f2..17812db5d9c 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.", "host_exists": "Vizio komponent med vert allerede konfigurert.", "name_exists": "Vizio-komponent med navn som allerede er konfigurert.", - "updated_entry": "Denne oppf\u00f8ringen er allerede konfigurert, men navnet og / eller alternativene som er definert i konfigurasjonen samsvarer ikke med den tidligere importerte konfigurasjonen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", + "updated_entry": "Dette innlegget har allerede v\u00e6rt oppsett, men navnet, apps, og/eller alternativer som er definert i konfigurasjon som ikke stemmer med det som tidligere er importert konfigurasjon, s\u00e5 konfigurasjonen innlegget har blitt oppdatert i henhold til dette.", "updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", "updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter." }, @@ -30,9 +30,17 @@ "title": "Sammenkoblingen Er Fullf\u00f8rt" }, "pairing_complete_import": { - "description": "Vizio SmartCast-enheten er n\u00e5 koblet til Home Assistant.\n\nTilgangstokenet er **{access_token}**.", + "description": "Din Vizio SmartCast TV er n\u00e5 koblet til Home Assistant.\n\nTilgangstokenet er **{access_token}**.", "title": "Sammenkoblingen Er Fullf\u00f8rt" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Apper \u00e5 inkludere eller ekskludere", + "include_or_exclude": "Inkluder eller ekskludere apper?" + }, + "description": "Hvis du har en Smart TV, kan du eventuelt filtrere kildelisten din ved \u00e5 velge hvilke apper du vil inkludere eller ekskludere i kildelisten. Du kan hoppe over dette trinnet for TV-er som ikke st\u00f8tter apper.", + "title": "Konfigurere Apper for Smart TV" + }, "user": { "data": { "access_token": "Tilgangstoken", @@ -40,8 +48,16 @@ "host": ":", "name": "Navn" }, - "description": "Alle felt er obligatoriske unntatt Access Token. Hvis du velger \u00e5 ikke oppgi et Access-token, og enhetstypen din er \u00abtv\u00bb, g\u00e5r du gjennom en sammenkoblingsprosess med enheten slik at et Tilgangstoken kan hentes.\n\nHvis du vil g\u00e5 gjennom paringsprosessen, m\u00e5 du kontrollere at TV-en er sl\u00e5tt p\u00e5 og koblet til nettverket f\u00f8r du klikker p\u00e5 Send. Du m\u00e5 ogs\u00e5 kunne se skjermen.", + "description": "En tilgangstoken er bare n\u00f8dvendig for TV-er. Hvis du konfigurerer en TV og ikke har tilgangstoken enn\u00e5, m\u00e5 du la den st\u00e5 tom for \u00e5 g\u00e5 gjennom en sammenkoblingsprosess.", "title": "Sett opp Vizio SmartCast-enhet" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Apper \u00e5 inkludere eller ekskludere", + "include_or_exclude": "Inkluder eller ekskludere apper?" + }, + "description": "Hvis du har en Smart TV, kan du eventuelt filtrere kildelisten din ved \u00e5 velge hvilke apper du vil inkludere eller ekskludere i kildelisten. Du kan hoppe over dette trinnet for TV-er som ikke st\u00f8tter apper.", + "title": "Konfigurere Apper for Smart TV" } }, "title": "Vizio SmartCast" @@ -50,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Apper \u00e5 inkludere eller ekskludere", + "include_or_exclude": "Inkluder eller ekskludere apper?", "timeout": "Tidsavbrudd for API-foresp\u00f8rsel (sekunder)", "volume_step": "St\u00f8rrelse p\u00e5 volum trinn" }, + "description": "Hvis du har en Smart-TV, kan du eventuelt filtrere kildelisten ved \u00e5 velge hvilke apper som skal inkluderes eller utelates i kildelisten.", "title": "Oppdater Vizo SmartCast alternativer" } }, diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index cba9f4319f5..1538ad09567 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -17,6 +17,25 @@ "tv_needs_token": "Gdy typem urz\u0105dzenia jest `tv` potrzebny jest prawid\u0142owy token dost\u0119pu." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + } + }, + "pairing_complete": { + "title": "Parowanie zako\u0144czone" + }, + "pairing_complete_import": { + "title": "Parowanie zako\u0144czone" + }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", + "include_or_exclude": "Do\u0142\u0105cz lub wyklucz aplikacje?" + }, + "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", + "title": "Konfigurowanie aplikacji dla smart TV" + }, "user": { "data": { "access_token": "Token dost\u0119pu", @@ -25,6 +44,14 @@ "name": "Nazwa" }, "title": "Konfiguracja klienta Vizio SmartCast" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", + "include_or_exclude": "Do\u0142\u0105czy\u0107 czy wykluczy\u0107 aplikacje?" + }, + "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", + "title": "Skonfiguruj aplikacje dla Smart TV" } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/vizio/.translations/ru.json b/homeassistant/components/vizio/.translations/ru.json index 3e14dd3d750..9162b2b0fe6 100644 --- a/homeassistant/components/vizio/.translations/ru.json +++ b/homeassistant/components/vizio/.translations/ru.json @@ -33,6 +33,14 @@ "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Vizio SmartCast \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a Home Assistant. \n\n\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 - '**{access_token}**'.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", + "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f?" + }, + "description": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c Smart TV, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432, \u0432\u043a\u043b\u044e\u0447\u0438\u0432 \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u0448\u0430\u0433 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0434\u043b\u044f Smart TV" + }, "user": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", @@ -40,8 +48,16 @@ "host": "<\u0425\u043e\u0441\u0442/IP>:<\u041f\u043e\u0440\u0442>", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, - "description": "\u0412\u0441\u0435 \u043f\u043e\u043b\u044f \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b \u0434\u043b\u044f \u0437\u0430\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f, \u043a\u0440\u043e\u043c\u0435 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430. \u0415\u0441\u043b\u0438 \u0432\u044b \u0440\u0435\u0448\u0438\u0442\u0435 \u043d\u0435 \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u043b\u044f\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u0430 \u0442\u0438\u043f \u0432\u0430\u0448\u0435\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 - 'tv', \u0412\u044b \u043f\u0440\u043e\u0439\u0434\u0435\u0442\u0435 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f \u0441 \u0432\u0430\u0448\u0438\u043c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c, \u0447\u0442\u043e\u0431\u044b \u043c\u043e\u0436\u043d\u043e \u0431\u044b\u043b\u043e \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430. \n\n\u0427\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0439\u0442\u0438 \u0447\u0435\u0440\u0435\u0437 \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f, \u043f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043d\u0430\u0436\u0430\u0442\u044c '\u041e\u0442\u043f\u0440\u0430\u0432\u0438\u0442\u044c', \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438. \u0412\u044b \u0442\u0430\u043a\u0436\u0435 \u0434\u043e\u043b\u0436\u043d\u044b \u0438\u043c\u0435\u0442\u044c \u0434\u043e\u0441\u0442\u0443\u043f \u043a \u044d\u043a\u0440\u0430\u043d\u0443 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", + "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432. \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u0443 \u0412\u0430\u0441 \u0435\u0449\u0435 \u043d\u0435\u0442 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", "title": "Vizio SmartCast" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", + "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f?" + }, + "description": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c Smart TV, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432, \u0432\u043a\u043b\u044e\u0447\u0438\u0432 \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u0448\u0430\u0433 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0434\u043b\u044f Smart TV" } }, "title": "Vizio SmartCast" @@ -50,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", + "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f?", "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 API (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "volume_step": "\u0428\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438" }, + "description": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c Smart TV, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432, \u0432\u043a\u043b\u044e\u0447\u0438\u0432 \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Vizio SmartCast" } }, diff --git a/homeassistant/components/vizio/.translations/sk.json b/homeassistant/components/vizio/.translations/sk.json new file mode 100644 index 00000000000..e0c0076ddc2 --- /dev/null +++ b/homeassistant/components/vizio/.translations/sk.json @@ -0,0 +1,22 @@ +{ + "config": { + "step": { + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Aplik\u00e1cie, ktor\u00e9 chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165", + "include_or_exclude": "Zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 aplik\u00e1cie?" + }, + "description": "Ak m\u00e1te Smart TV, m\u00f4\u017eete volite\u013ene filtrova\u0165 svoj zoznam zdrojov v\u00fdberom aplik\u00e1ci\u00ed, ktor\u00e9 chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 do zoznamu zdrojov. Tento krok m\u00f4\u017eete presko\u010di\u0165 pre telev\u00edzory, ktor\u00e9 nepodporuj\u00fa aplik\u00e1cie.", + "title": "Konfigur\u00e1cia aplik\u00e1ci\u00ed pre Smart TV" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Aplik\u00e1cie, ktor\u00e9 chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165", + "include_or_exclude": "Zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 aplik\u00e1cie?" + }, + "description": "Ak m\u00e1te Smart TV, m\u00f4\u017eete volite\u013ene filtrova\u0165 svoj zoznam zdrojov v\u00fdberom aplik\u00e1ci\u00ed, ktor\u00e9 chcete zahrn\u00fa\u0165 alebo vyl\u00fa\u010di\u0165 do zoznamu zdrojov. Tento krok m\u00f4\u017eete presko\u010di\u0165 pre telev\u00edzory, ktor\u00e9 nepodporuj\u00fa aplik\u00e1cie.", + "title": "Konfigur\u00e1cia aplik\u00e1ci\u00ed pre Smart TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/sl.json b/homeassistant/components/vizio/.translations/sl.json index 55faaaf26a8..8163846aec0 100644 --- a/homeassistant/components/vizio/.translations/sl.json +++ b/homeassistant/components/vizio/.translations/sl.json @@ -6,17 +6,41 @@ "already_setup_with_diff_host_and_name": "Zdi se, da je bil ta vnos \u017ee nastavljen z drugim gostiteljem in imenom glede na njegovo serijsko \u0161tevilko. Pred ponovnim poskusom dodajanja te naprave, odstranite vse stare vnose iz config.yaml in iz menija Integrations.", "host_exists": "VIZIO komponenta z gostiteljem \u017ee nastavljen.", "name_exists": "Vizio komponenta z imenom je \u017ee konfigurirana.", - "updated_entry": "Ta vnos je \u017ee nastavljen, vendar se ime in / ali mo\u017enosti, opredeljene v config, ne ujemajo s predhodno uvo\u017eenim configom, zato je bil vnos konfiguracije ustrezno posodobljen.", + "updated_entry": "Ta vnos je bil \u017ee nastavljen, vendar se ime, aplikacije in/ali mo\u017enosti, dolo\u010dene v konfiguraciji, ne ujemajo s predhodno uvo\u017eeno konfiguracijo, zato je bil konfiguracijski vnos ustrezno posodobljen.", "updated_options": "Ta vnos je \u017ee nastavljen, vendar se mo\u017enosti, definirane v config-u, ne ujemajo s predhodno uvo\u017eenimi vrednostmi, zato je bil vnos konfiguracije ustrezno posodobljen.", "updated_volume_step": "Ta vnos je \u017ee nastavljen, vendar velikost koraka glasnosti v config-u ne ustreza vnosu konfiguracije, zato je bil vnos konfiguracije ustrezno posodobljen." }, "error": { "cant_connect": "Ni bilo mogo\u010de povezati z napravo. [Preglejte dokumente] (https://www.home-assistant.io/integrations/vizio/) in ponovno preverite, ali: \n \u2013 Naprava je vklopljena \n \u2013 Naprava je povezana z omre\u017ejem \n \u2013 Vrednosti, ki ste jih izpolnili, so to\u010dne \nnato poskusite ponovno.", + "complete_pairing failed": "Seznanjanja ni mogo\u010de dokon\u010dati. Zagotovite, da je PIN, ki ste ga vnesli, pravilen in da je televizor \u0161e vedno vklopljen in priklju\u010den na omre\u017eje, preden ponovno poizkusite.", "host_exists": "Naprava Vizio z dolo\u010denim gostiteljem je \u017ee konfigurirana.", "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana.", "tv_needs_token": "Ko je vrsta naprave\u00bb TV \u00ab, je potreben veljaven \u017eeton za dostop." }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "description": "Va\u0161 TV naj bi prikazoval kodo. Vnesite to kodo v obrazec in nato nadaljujte z naslednjim korakom za dokon\u010danje zdru\u017eevanja.", + "title": "Dokon\u010dajte proces zdru\u017eevanja" + }, + "pairing_complete": { + "description": "Va\u0161a naprava Vizio SmartCast je zdaj povezana s Home Assistant-om.", + "title": "Seznanjanje je kon\u010dano" + }, + "pairing_complete_import": { + "description": "Va\u0161 VIZIO SmartCast TV je zdaj priklju\u010den na Home Assistant.\n\n\u017deton za dostop je '**{access_token}**'.", + "title": "Seznanjanje je kon\u010dano" + }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Aplikacije za vklju\u010ditev ali izklju\u010ditev", + "include_or_exclude": "Vklju\u010di ali Izklju\u010di Aplikacije?" + }, + "description": "\u010ce imate pametni TV, lahko po izbiri filtrirate seznam virov tako, da izberete, katere aplikacije \u017eelite vklju\u010diti ali izklju\u010diti na seznamu virov. Ta korak lahko presko\u010dite za televizorje, ki ne podpirajo aplikacij.", + "title": "Konfigurirajte aplikacije za pametno televizijo" + }, "user": { "data": { "access_token": "\u017deton za dostop", @@ -24,7 +48,16 @@ "host": ":", "name": "Ime" }, - "title": "Nastavite odjemalec Vizio SmartCast" + "description": "Dostopni \u017eeton je potreben samo za televizorje. \u010ce konfigurirate televizor in \u0161e nimate \u017eetona za dostop, ga pustite prazno in boste \u0161li, da bo \u0161el skozi postopek seznanjanja.", + "title": "Namestite Vizio SmartCast napravo" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Aplikacije za vklju\u010ditev ali izklju\u010ditev", + "include_or_exclude": "Vklju\u010di ali Izklju\u010di Aplikacije?" + }, + "description": "\u010ce imate pametni TV, lahko po izbiri filtrirate seznam virov tako, da izberete, katere aplikacije \u017eelite vklju\u010diti ali izklju\u010diti na seznamu virov. Ta korak lahko presko\u010dite za televizorje, ki ne podpirajo aplikacij.", + "title": "Konfigurirajte aplikacije za pametno televizijo" } }, "title": "Vizio SmartCast" @@ -33,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Aplikacije za vklju\u010ditev ali izklju\u010ditev", + "include_or_exclude": "Vklju\u010di ali Izklju\u010di Aplikacije?", "timeout": "\u010casovna omejitev zahteve za API (sekunde)", "volume_step": "Velikost koraka glasnosti" }, + "description": "\u010ce imate pametni TV, lahko po izbiri filtrirate seznam virov tako, da izberete, katere aplikacije \u017eelite vklju\u010diti ali izklju\u010diti na seznamu virov.", "title": "Posodobite mo\u017enosti Vizo SmartCast" } }, diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json index d2404a80620..4d826a287f6 100644 --- a/homeassistant/components/vizio/.translations/zh-Hant.json +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -6,7 +6,7 @@ "already_setup_with_diff_host_and_name": "\u6839\u64da\u6240\u63d0\u4f9b\u7684\u5e8f\u865f\uff0c\u6b64\u7269\u4ef6\u4f3c\u4e4e\u5df2\u7d93\u4f7f\u7528\u4e0d\u540c\u7684\u4e3b\u6a5f\u7aef\u8207\u540d\u7a31\u9032\u884c\u8a2d\u5b9a\u3002\u8acb\u5f9e\u6574\u5408\u9078\u55ae Config.yaml \u4e2d\u79fb\u9664\u820a\u7269\u4ef6\uff0c\u7136\u5f8c\u518d\u65b0\u589e\u6b64\u8a2d\u5099\u3002", "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u9078\u9805\u540d\u7a31\u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", + "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", "updated_options": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u9078\u9805\u5b9a\u7fa9\u8207\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", "updated_volume_step": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u97f3\u91cf\u5927\u5c0f\u8207\u7269\u4ef6\u8a2d\u5b9a\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, @@ -30,9 +30,17 @@ "title": "\u914d\u5c0d\u5b8c\u6210" }, "pairing_complete_import": { - "description": "Vizio SmartCast \u8a2d\u5099\u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba\u300c**{access_token}**\u300d\u3002", + "description": "Vizio SmartCast TV \u8a2d\u5099\u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba\u300c**{access_token}**\u300d\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684 App", + "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f" + }, + "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u4ee5\u65bc\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6216\u6392\u9664\u904e\u6ffe App\u3002\u5047\u5982\u96fb\u8996\u4e0d\u652f\u63f4 App\u3001\u5247\u53ef\u8df3\u904e\u6b64\u6b65\u9a5f\u3002", + "title": "Smart TV \u8a2d\u5b9a App" + }, "user": { "data": { "access_token": "\u5b58\u53d6\u5bc6\u9470", @@ -40,8 +48,16 @@ "host": "<\u4e3b\u6a5f\u7aef/IP>:", "name": "\u540d\u7a31" }, - "description": "\u9664\u4e86\u5b58\u53d6\u5bc6\u9470\u5916\u3001\u8207\u8a2d\u5099\u985e\u5225\u70ba\u300cTV\u300d\u5916\u3001\u6240\u6709\u6b04\u4f4d\u90fd\u70ba\u5fc5\u586b\u3002\u5c07\u6703\u4ee5\u8a2d\u5099\u9032\u884c\u914d\u5c0d\u904e\u7a0b\uff0c\u56e0\u6b64\u5b58\u53d6\u5bc6\u9470\u53ef\u4ee5\u6536\u56de\u3002\n\n\u6b32\u5b8c\u6210\u914d\u5c0d\u904e\u7a0b\uff0c\u50b3\u9001\u524d\u3001\u8acb\u5148\u78ba\u5b9a\u96fb\u8996\u5df2\u7d93\u958b\u6a5f\u3001\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002\u540c\u6642\u3001\u4f60\u4e5f\u5fc5\u9808\u80fd\u770b\u5230\u96fb\u8996\u756b\u9762\u3002", + "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u3002\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5bc6\u9470\uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", "title": "\u8a2d\u5b9a Vizio SmartCast \u8a2d\u5099" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684 App", + "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f" + }, + "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u4ee5\u65bc\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6216\u6392\u9664\u904e\u6ffe App\u3002\u5047\u5982\u96fb\u8996\u4e0d\u652f\u63f4 App\u3001\u5247\u53ef\u8df3\u904e\u6b64\u6b65\u9a5f\u3002", + "title": "Smart TV \u8a2d\u5b9a App" } }, "title": "Vizio SmartCast" @@ -50,9 +66,12 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684 App", + "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f", "timeout": "API \u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09", "volume_step": "\u97f3\u91cf\u5927\u5c0f" }, + "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", "title": "\u66f4\u65b0 Vizo SmartCast \u9078\u9805" } }, diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index 600b2dbf450..faa76ac9333 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -6,7 +6,7 @@ "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." }, "create_entry": { - "default": "Uspe\u0161no overjen z Withings za izbrani profil." + "default": "Uspe\u0161no overjen z Withings." }, "step": { "pick_implementation": { diff --git a/homeassistant/components/wwlln/.translations/ca.json b/homeassistant/components/wwlln/.translations/ca.json index acf8ec7c518..736689c34d5 100644 --- a/homeassistant/components/wwlln/.translations/ca.json +++ b/homeassistant/components/wwlln/.translations/ca.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Aquesta ubicaci\u00f3 ja est\u00e0 registrada.", + "window_too_small": "Una finestra massa petita pot provocar que Home Assistant perdi esdeveniments." + }, "error": { "identifier_exists": "Ubicaci\u00f3 ja registrada" }, diff --git a/homeassistant/components/wwlln/.translations/de.json b/homeassistant/components/wwlln/.translations/de.json index 651e2e6fa0f..c02da263f89 100644 --- a/homeassistant/components/wwlln/.translations/de.json +++ b/homeassistant/components/wwlln/.translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Dieser Standort ist bereits registriert.", + "window_too_small": "Ein zu kleines Fenster f\u00fchrt dazu, dass Home Assistant Ereignisse verpasst." + }, "error": { "identifier_exists": "Standort bereits registriert" }, diff --git a/homeassistant/components/wwlln/.translations/en.json b/homeassistant/components/wwlln/.translations/en.json index 48896cc8682..a12a5079f9b 100644 --- a/homeassistant/components/wwlln/.translations/en.json +++ b/homeassistant/components/wwlln/.translations/en.json @@ -1,7 +1,11 @@ { "config": { "abort": { - "already_configured": "This location is already registered." + "already_configured": "This location is already registered.", + "window_too_small": "A too-small window will cause Home Assistant to miss events." + }, + "error": { + "identifier_exists": "Location already registered" }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/es.json b/homeassistant/components/wwlln/.translations/es.json index 869e8d07994..ee377673181 100644 --- a/homeassistant/components/wwlln/.translations/es.json +++ b/homeassistant/components/wwlln/.translations/es.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada.", + "window_too_small": "Una ventana demasiado peque\u00f1a provocar\u00e1 que Home Assistant se pierda eventos." + }, "error": { "identifier_exists": "Ubicaci\u00f3n ya registrada" }, diff --git a/homeassistant/components/wwlln/.translations/fr.json b/homeassistant/components/wwlln/.translations/fr.json index d76582e4127..ad16a7e3a8d 100644 --- a/homeassistant/components/wwlln/.translations/fr.json +++ b/homeassistant/components/wwlln/.translations/fr.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Cet emplacement est d\u00e9j\u00e0 enregistr\u00e9.", + "window_too_small": "Une fen\u00eatre trop petite emp\u00eachera Home Assistant de manquer des \u00e9v\u00e9nements." + }, "error": { "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" }, diff --git a/homeassistant/components/wwlln/.translations/it.json b/homeassistant/components/wwlln/.translations/it.json index f0fc3263607..35cbb8b9bc0 100644 --- a/homeassistant/components/wwlln/.translations/it.json +++ b/homeassistant/components/wwlln/.translations/it.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Questa posizione \u00e8 gi\u00e0 registrata.", + "window_too_small": "Una finestra troppo piccola far\u00e0 s\u00ec che Home Assistant perda gli eventi." + }, "error": { "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" }, diff --git a/homeassistant/components/wwlln/.translations/ko.json b/homeassistant/components/wwlln/.translations/ko.json index e5831f5af29..bc4a483a077 100644 --- a/homeassistant/components/wwlln/.translations/ko.json +++ b/homeassistant/components/wwlln/.translations/ko.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\uc774 \uc704\uce58\ub294 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "window_too_small": "\ucc3d\uc774 \ub108\ubb34 \uc791\uc73c\uba74 Home Assistant \uac00 \uc774\ubca4\ud2b8\ub97c \ub193\uce60 \uc218 \uc788\uc2b5\ub2c8\ub2e4." + }, "error": { "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/wwlln/.translations/no.json b/homeassistant/components/wwlln/.translations/no.json index ea3b5cd1056..ca9822d2733 100644 --- a/homeassistant/components/wwlln/.translations/no.json +++ b/homeassistant/components/wwlln/.translations/no.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Denne plasseringen er allerede registrert.", + "window_too_small": "Et for lite vindu vil f\u00f8re til at Home Assistant g\u00e5r glipp av hendelser." + }, "error": { "identifier_exists": "Lokasjon allerede registrert" }, diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index 658dbebbe45..a202c611086 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Ta lokalizacja jest ju\u017c zarejestrowana.", + "window_too_small": "Zbyt ma\u0142e okno spowoduje, \u017ce Home Assistant przegapi wydarzenia." + }, "error": { "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." }, diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index 3bdaf85498b..b0e39a51898 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, "error": { "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." }, diff --git a/homeassistant/components/wwlln/.translations/sl.json b/homeassistant/components/wwlln/.translations/sl.json index d6562a2a247..396180249e2 100644 --- a/homeassistant/components/wwlln/.translations/sl.json +++ b/homeassistant/components/wwlln/.translations/sl.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "Ta lokacija je \u017ee registrirana.", + "window_too_small": "Premajhno okno bo povzro\u010dilo, da bo Home Assistant zamudil dogodke." + }, "error": { "identifier_exists": "Lokacija je \u017ee registrirana" }, diff --git a/homeassistant/components/wwlln/.translations/zh-Hant.json b/homeassistant/components/wwlln/.translations/zh-Hant.json index 710ee882a9c..b75c07a0813 100644 --- a/homeassistant/components/wwlln/.translations/zh-Hant.json +++ b/homeassistant/components/wwlln/.translations/zh-Hant.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "already_configured": "\u6b64\u4f4d\u7f6e\u5df2\u8a3b\u518a\u3002", + "window_too_small": "\u904e\u5c0f\u7684\u8996\u7a97\u5c07\u5c0e\u81f4 Home Assistant \u932f\u904e\u4e8b\u4ef6\u3002" + }, "error": { "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" }, diff --git a/homeassistant/components/zha/.translations/ca.json b/homeassistant/components/zha/.translations/ca.json index e5181fb5106..9ad486f5041 100644 --- a/homeassistant/components/zha/.translations/ca.json +++ b/homeassistant/components/zha/.translations/ca.json @@ -54,6 +54,14 @@ "device_shaken": "Dispositiu sacsejat", "device_slid": "Dispositiu lliscat a \"{subtype}\"", "device_tilted": "Dispositiu inclinat", + "remote_button_alt_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades (mode alternatiu)", + "remote_button_alt_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament (mode alternatiu)", + "remote_button_alt_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut (mode alternatiu", + "remote_button_alt_quadruple_press": "Bot\u00f3 \"{subtype}\" clicat quatre vegades (mode alternatiu)", + "remote_button_alt_quintuple_press": "Bot\u00f3 \"{subtype}\" clicat cinc vegades (mode alternatiu)", + "remote_button_alt_short_press": "Bot\u00f3 \"{subtype}\" premut (mode alternatiu)", + "remote_button_alt_short_release": "Bot\u00f3 \"{subtype}\" alliberat (mode alternatiu)", + "remote_button_alt_triple_press": "Bot\u00f3 \"{subtype}\" clicat tres vegades (mode alternatiu)", "remote_button_double_press": "Bot\u00f3 \"{subtype}\" clicat dues vegades", "remote_button_long_press": "Bot\u00f3 \"{subtype}\" premut cont\u00ednuament", "remote_button_long_release": "Bot\u00f3 \"{subtype}\" alliberat despr\u00e9s d'una estona premut", diff --git a/homeassistant/components/zha/.translations/de.json b/homeassistant/components/zha/.translations/de.json index 3329eafa1c6..f7a00fcfa7f 100644 --- a/homeassistant/components/zha/.translations/de.json +++ b/homeassistant/components/zha/.translations/de.json @@ -54,6 +54,14 @@ "device_shaken": "Ger\u00e4t ersch\u00fcttert", "device_slid": "Ger\u00e4t gerutscht \"{subtype}\"", "device_tilted": "Ger\u00e4t gekippt", + "remote_button_alt_double_press": "\"{subtype}\" Taste doppelt geklickt (Alternativer Modus)", + "remote_button_alt_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen (Alternativer Modus)", + "remote_button_alt_quadruple_press": "\"{subtype}\" Taste vierfach geklickt (Alternativer Modus)", + "remote_button_alt_quintuple_press": "\"{subtype}\" Taste f\u00fcnffach geklickt (Alternativer Modus)", + "remote_button_alt_short_press": "\"{subtype}\" Taste gedr\u00fcckt (Alternativer Modus)", + "remote_button_alt_short_release": "\"{subtype}\" Taste losgelassen (Alternativer Modus)", + "remote_button_alt_triple_press": "\"{subtype}\" Taste dreimal geklickt (Alternativer Modus)", "remote_button_double_press": "\"{subtype}\" Taste doppelt angeklickt", "remote_button_long_press": "\"{subtype}\" Taste kontinuierlich gedr\u00fcckt", "remote_button_long_release": "\"{subtype}\" Taste nach langem Dr\u00fccken losgelassen", diff --git a/homeassistant/components/zha/.translations/fr.json b/homeassistant/components/zha/.translations/fr.json index 5d8bdfa82eb..99905bba836 100644 --- a/homeassistant/components/zha/.translations/fr.json +++ b/homeassistant/components/zha/.translations/fr.json @@ -54,7 +54,7 @@ "device_shaken": "Appareil secou\u00e9", "device_slid": "Appareil gliss\u00e9 \"{subtype}\"", "device_tilted": "Dispositif inclin\u00e9", - "remote_button_double_press": "Bouton \"{subtype}\" double cliqu\u00e9", + "remote_button_double_press": "Double clic sur le bouton \" {subtype} \"", "remote_button_long_press": "Bouton \"{subtype}\" appuy\u00e9 continuellement", "remote_button_long_release": "Bouton \" {subtype} \" rel\u00e2ch\u00e9 apr\u00e8s un appui long", "remote_button_quadruple_press": "bouton \" {subtype} \" quadruple clics", diff --git a/homeassistant/components/zha/.translations/it.json b/homeassistant/components/zha/.translations/it.json index bb05977fd09..5048ce52599 100644 --- a/homeassistant/components/zha/.translations/it.json +++ b/homeassistant/components/zha/.translations/it.json @@ -54,6 +54,14 @@ "device_shaken": "Dispositivo in vibrazione", "device_slid": "Dispositivo scivolato \"{sottotipo}\"", "device_tilted": "Dispositivo inclinato", + "remote_button_alt_double_press": "Pulsante \"{subtype}\" cliccato due volte (modalit\u00e0 Alternata)", + "remote_button_alt_long_press": "Pulsante \"{subtype}\" premuto continuamente (modalit\u00e0 Alternata)", + "remote_button_alt_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione (modalit\u00e0 Alternata)", + "remote_button_alt_quadruple_press": "Pulsante \"{subtype}\" cliccato quattro volte (modalit\u00e0 Alternata)", + "remote_button_alt_quintuple_press": "Pulsante \"{subtype}\" cliccato cinque volte (modalit\u00e0 Alternata)", + "remote_button_alt_short_press": "Pulsante \"{subtype}\" premuto (modalit\u00e0 Alternata)", + "remote_button_alt_short_release": "Pulsante \"{subtype}\" rilasciato (modalit\u00e0 Alternata)", + "remote_button_alt_triple_press": "Pulsante \"{subtype}\" cliccato tre volte (modalit\u00e0 Alternata)", "remote_button_double_press": "Pulsante \"{subtype}\" cliccato due volte", "remote_button_long_press": "Pulsante \"{subtype}\" premuto continuamente", "remote_button_long_release": "Pulsante \"{subtype}\" rilasciato dopo una lunga pressione", diff --git a/homeassistant/components/zha/.translations/ko.json b/homeassistant/components/zha/.translations/ko.json index 69b8f9ad9a4..76a10d2c976 100644 --- a/homeassistant/components/zha/.translations/ko.json +++ b/homeassistant/components/zha/.translations/ko.json @@ -54,6 +54,14 @@ "device_shaken": "\uae30\uae30\uac00 \ud754\ub4e4\ub9b4 \ub54c", "device_slid": "\"{subtype}\" \uae30\uae30\uac00 \ubbf8\ub044\ub7ec\uc9c8 \ub54c", "device_tilted": "\uae30\uae30\uac00 \uae30\uc6b8\uc5b4\uc9c8 \ub54c", + "remote_button_alt_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c (\ub300\uccb4\ubaa8\ub4dc)", + "remote_button_alt_quadruple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub124 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_quintuple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub2e4\uc12f \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", + "remote_button_alt_triple_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uc138 \ubc88 \ub20c\ub9b4 \ub54c (\ub300\uccb4 \ubaa8\ub4dc)", "remote_button_double_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub450 \ubc88 \ub20c\ub9b4 \ub54c", "remote_button_long_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \uacc4\uc18d \ub20c\ub824\uc9c8 \ub54c", "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", diff --git a/homeassistant/components/zha/.translations/pl.json b/homeassistant/components/zha/.translations/pl.json index 4698a0a37ef..bf651fb16ed 100644 --- a/homeassistant/components/zha/.translations/pl.json +++ b/homeassistant/components/zha/.translations/pl.json @@ -54,6 +54,14 @@ "device_shaken": "nast\u0105pi potrz\u0105\u015bni\u0119cie urz\u0105dzeniem", "device_slid": "nast\u0105pi przesuni\u0119cie urz\u0105dzenia \"{subtype}\"", "device_tilted": "nast\u0105pi przechylenie urz\u0105dzenia", + "remote_button_alt_double_press": "\"{subtype}\" dwukrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y (tryb alternatywny)", + "remote_button_alt_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu (tryb alternatywny)", + "remote_button_alt_quadruple_press": "\"{subtype}\" czterokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_quintuple_press": "\"{subtype}\" zostanie pi\u0119ciokrotnie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty (tryb alternatywny)", + "remote_button_alt_short_release": "\"{subtype}\" zostanie zwolniony (tryb alternatywny)", + "remote_button_alt_triple_press": "\"{subtype}\" zostanie trzykrotnie naci\u015bni\u0119ty (tryb alternatywny)", "remote_button_double_press": "\"{subtype}\" zostanie podw\u00f3jnie naci\u015bni\u0119ty", "remote_button_long_press": "\"{subtype}\" zostanie naci\u015bni\u0119ty w spos\u00f3b ci\u0105g\u0142y", "remote_button_long_release": "\"{subtype}\" zostanie zwolniony po d\u0142ugim naci\u015bni\u0119ciu", diff --git a/homeassistant/components/zha/.translations/sl.json b/homeassistant/components/zha/.translations/sl.json index 226bd37200e..53a45000701 100644 --- a/homeassistant/components/zha/.translations/sl.json +++ b/homeassistant/components/zha/.translations/sl.json @@ -54,6 +54,14 @@ "device_shaken": "Naprava se je pretresla", "device_slid": "Naprava zdrsnila \"{subtype}\"", "device_tilted": "Naprava je nagnjena", + "remote_button_alt_double_press": "Dvojni klik gumba \" {subtype} \" (nadomestni na\u010din)", + "remote_button_alt_long_press": "Gumb \" {subtype} \" neprekinjeno pritisnjen (nadomestni na\u010din)", + "remote_button_alt_long_release": "\"{Subtype}\" gumb spro\u0161\u010den po dolgem pritisku (nadomestni na\u010din)", + "remote_button_alt_quadruple_press": "\u0161tirikrat kliknjen gumb \" {subtype} \" (nadomestni na\u010din)", + "remote_button_alt_quintuple_press": "Petkrat kliknjen \"{podtipa}\" gumb (Nadomestni na\u010din)", + "remote_button_alt_short_press": "pritisnjen gumb \" {subtype} \" (nadomestni na\u010din)", + "remote_button_alt_short_release": "Gumb \" {subtype} \" spro\u0161\u010den (nadomestni na\u010din)", + "remote_button_alt_triple_press": "Trikrat kliknjen gumb \" {subtype} \" (nadomestni na\u010din)", "remote_button_double_press": "Dvakrat kliknete gumb \"{subtype}\"", "remote_button_long_press": "\"{subtype}\" gumb neprekinjeno pritisnjen", "remote_button_long_release": "\"{subtype}\" gumb spro\u0161\u010den po dolgem pritisku", From aec2fe86e42a07180b1035775ddd4e4b21b963ed Mon Sep 17 00:00:00 2001 From: roleo <39277388+roleoroleo@users.noreply.github.com> Date: Tue, 24 Mar 2020 17:57:14 +0100 Subject: [PATCH 234/431] Add onvif snapshot uri (#33149) * Add onvif snapshot uri If the cam supports http snapshot uri, use it instead of ffmpeg. * Code formatting * Fix to pass pre-commit --- homeassistant/components/onvif/camera.py | 82 ++++++++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index cb518d6c5ee..c9f592cdba4 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -5,7 +5,9 @@ import logging import os from typing import Optional +from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError +import async_timeout from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame import onvif @@ -166,6 +168,7 @@ class ONVIFHassCamera(Camera): self._profile_index = config.get(CONF_PROFILE) self._ptz_service = None self._input = None + self._snapshot = None self.stream_options[CONF_RTSP_TRANSPORT] = config.get(CONF_RTSP_TRANSPORT) self._mac = None @@ -198,6 +201,7 @@ class ONVIFHassCamera(Camera): await self.async_obtain_mac_address() await self.async_check_date_and_time() await self.async_obtain_input_uri() + await self.async_obtain_snapshot_uri() self.setup_ptz() except ClientConnectionError as err: _LOGGER.warning( @@ -372,6 +376,57 @@ class ONVIFHassCamera(Camera): except exceptions.ONVIFError as err: _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) + async def async_obtain_snapshot_uri(self): + """Set the snapshot uri for the camera.""" + _LOGGER.debug( + "Connecting with ONVIF Camera: %s on port %s", self._host, self._port + ) + + try: + _LOGGER.debug("Retrieving profiles") + + media_service = self._camera.create_media_service() + + profiles = await media_service.GetProfiles() + + _LOGGER.debug("Retrieved '%d' profiles", len(profiles)) + + if self._profile_index >= len(profiles): + _LOGGER.warning( + "ONVIF Camera '%s' doesn't provide profile %d." + " Using the last profile.", + self._name, + self._profile_index, + ) + self._profile_index = -1 + + _LOGGER.debug("Using profile index '%d'", self._profile_index) + + _LOGGER.debug("Retrieving snapshot uri") + + # Fix Onvif setup error on Goke GK7102 based IP camera + # where we need to recreate media_service #26781 + media_service = self._camera.create_media_service() + + req = media_service.create_type("GetSnapshotUri") + req.ProfileToken = profiles[self._profile_index].token + + snapshot_uri = await media_service.GetSnapshotUri(req) + uri_no_auth = snapshot_uri.Uri + uri_for_log = uri_no_auth.replace("http://", "http://:@", 1) + # Same authentication as rtsp + self._snapshot = uri_no_auth.replace( + "http://", f"http://{self._username}:{self._password}@", 1 + ) + + _LOGGER.debug( + "ONVIF Camera Using the following URL for %s snapshot: %s", + self._name, + uri_for_log, + ) + except exceptions.ONVIFError as err: + _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) + def setup_ptz(self): """Set up PTZ if available.""" _LOGGER.debug("Setting up the ONVIF PTZ service") @@ -457,13 +512,30 @@ class ONVIFHassCamera(Camera): _LOGGER.debug("Retrieving image from camera '%s'", self._name) - ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) + if self._snapshot is not None: + try: + websession = async_get_clientsession(self.hass) + with async_timeout.timeout(10): + response = await websession.get(self._snapshot) + image = await response.read() + except asyncio.TimeoutError: + _LOGGER.error("Timeout getting image from: %s", self._name) + image = None + except ClientError as err: + _LOGGER.error("Error getting new camera image: %s", err) + image = None - image = await asyncio.shield( - ffmpeg.get_image( - self._input, output_format=IMAGE_JPEG, extra_cmd=self._ffmpeg_arguments + if self._snapshot is None or image is None: + ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) + + image = await asyncio.shield( + ffmpeg.get_image( + self._input, + output_format=IMAGE_JPEG, + extra_cmd=self._ffmpeg_arguments, + ) ) - ) + return image async def handle_async_mjpeg_stream(self, request): From fb22f6c3011d8ab9afb71a287deac08c6a0b23b6 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 24 Mar 2020 17:59:17 +0100 Subject: [PATCH 235/431] Add Context support for async_entity removal (#33209) * Add Context for async_remove * Check context in state automation on entity removal --- homeassistant/components/configurator/__init__.py | 6 +++--- homeassistant/components/microsoft_face/__init__.py | 2 +- .../components/persistent_notification/__init__.py | 2 +- homeassistant/core.py | 4 +++- homeassistant/helpers/entity.py | 2 +- homeassistant/helpers/entity_registry.py | 2 +- tests/components/automation/test_state.py | 4 +++- 7 files changed, 13 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/configurator/__init__.py b/homeassistant/components/configurator/__init__.py index e1e6181d8ca..d03dbe1fe7b 100644 --- a/homeassistant/components/configurator/__init__.py +++ b/homeassistant/components/configurator/__init__.py @@ -14,7 +14,7 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, EVENT_TIME_CHANGED, ) -from homeassistant.core import callback as async_callback +from homeassistant.core import Event, callback as async_callback from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.loader import bind_hass from homeassistant.util.async_ import run_callback_threadsafe @@ -214,9 +214,9 @@ class Configurator: # it shortly after so that it is deleted when the client updates. self.hass.states.async_set(entity_id, STATE_CONFIGURED) - def deferred_remove(event): + def deferred_remove(event: Event): """Remove the request state.""" - self.hass.states.async_remove(entity_id) + self.hass.states.async_remove(entity_id, context=event.context) self.hass.bus.async_listen_once(EVENT_TIME_CHANGED, deferred_remove) diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 780f4d6dd48..1bc9c116cda 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -113,7 +113,7 @@ async def async_setup(hass, config): face.store.pop(g_id) entity = entities.pop(g_id) - hass.states.async_remove(entity.entity_id) + hass.states.async_remove(entity.entity_id, service.context) except HomeAssistantError as err: _LOGGER.error("Can't delete group '%s' with error: %s", g_id, err) diff --git a/homeassistant/components/persistent_notification/__init__.py b/homeassistant/components/persistent_notification/__init__.py index 0311bd4d30d..54e252a97a5 100644 --- a/homeassistant/components/persistent_notification/__init__.py +++ b/homeassistant/components/persistent_notification/__init__.py @@ -158,7 +158,7 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: if entity_id not in persistent_notifications: return - hass.states.async_remove(entity_id) + hass.states.async_remove(entity_id, call.context) del persistent_notifications[entity_id] hass.bus.async_fire(EVENT_PERSISTENT_NOTIFICATIONS_UPDATED) diff --git a/homeassistant/core.py b/homeassistant/core.py index afd1e4daa1a..fd894fd6c05 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -901,7 +901,7 @@ class StateMachine: ).result() @callback - def async_remove(self, entity_id: str) -> bool: + def async_remove(self, entity_id: str, context: Optional[Context] = None) -> bool: """Remove the state of an entity. Returns boolean to indicate if an entity was removed. @@ -917,6 +917,8 @@ class StateMachine: self._bus.async_fire( EVENT_STATE_CHANGED, {"entity_id": entity_id, "old_state": old_state, "new_state": None}, + EventOrigin.local, + context=context, ) return True diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 186aecd78f4..6a238ff84b1 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -504,7 +504,7 @@ class Entity(ABC): while self._on_remove: self._on_remove.pop()() - self.hass.states.async_remove(self.entity_id) + self.hass.states.async_remove(self.entity_id, context=self._context) async def async_added_to_hass(self) -> None: """Run when entity about to be added to hass. diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 87383d45635..b8e54155922 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -518,7 +518,7 @@ def async_setup_entity_restore( if state is None or not state.attributes.get(ATTR_RESTORED): return - hass.states.async_remove(event.data["entity_id"]) + hass.states.async_remove(event.data["entity_id"], context=event.context) hass.bus.async_listen(EVENT_ENTITY_REGISTRY_UPDATED, cleanup_restored_states) diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 173af8158a4..949851f5470 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -521,6 +521,7 @@ async def test_if_fires_on_entity_change_with_for(hass, calls): async def test_if_fires_on_entity_removal(hass, calls): """Test for firing on entity removal, when new_state is None.""" + context = Context() hass.states.async_set("test.entity", "hello") await hass.async_block_till_done() @@ -536,9 +537,10 @@ async def test_if_fires_on_entity_removal(hass, calls): ) await hass.async_block_till_done() - assert hass.states.async_remove("test.entity") + assert hass.states.async_remove("test.entity", context=context) await hass.async_block_till_done() assert 1 == len(calls) + assert calls[0].context.parent_id == context.id async def test_if_fires_on_for_condition(hass, calls): From 109f083c5d9c63997359dac28510fde0043453ff Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Tue, 24 Mar 2020 18:29:40 +0100 Subject: [PATCH 236/431] Add Body Composition data to Garmin Connect (#32841) * Added support for Garmin index smart scale * Added body composition data * Added keyerror exception * Code optimization * Update library, changed logger errors to exception * Fixed manifest --- .../components/garmin_connect/__init__.py | 22 +++++++++++-------- .../components/garmin_connect/const.py | 9 ++++++++ .../components/garmin_connect/manifest.json | 2 +- .../components/garmin_connect/sensor.py | 8 ++++++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 6 files changed, 32 insertions(+), 13 deletions(-) diff --git a/homeassistant/components/garmin_connect/__init__.py b/homeassistant/components/garmin_connect/__init__.py index d63d82d1284..1536a875698 100644 --- a/homeassistant/components/garmin_connect/__init__.py +++ b/homeassistant/components/garmin_connect/__init__.py @@ -43,13 +43,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, ) as err: - _LOGGER.error("Error occurred during Garmin Connect login: %s", err) + _LOGGER.error("Error occurred during Garmin Connect login request: %s", err) return False except (GarminConnectConnectionError) as err: - _LOGGER.error("Error occurred during Garmin Connect login: %s", err) + _LOGGER.error( + "Connection error occurred during Garmin Connect login request: %s", err + ) raise ConfigEntryNotReady except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occurred during Garmin Connect login") + _LOGGER.exception("Unknown error occurred during Garmin Connect login request") return False garmin_data = GarminConnectData(hass, garmin_client) @@ -93,16 +95,18 @@ class GarminConnectData: today = date.today() try: - self.data = self.client.get_stats(today.isoformat()) + self.data = self.client.get_stats_and_body(today.isoformat()) except ( GarminConnectAuthenticationError, GarminConnectTooManyRequestsError, + GarminConnectConnectionError, ) as err: - _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err) - return - except (GarminConnectConnectionError) as err: - _LOGGER.error("Error occurred during Garmin Connect get stats: %s", err) + _LOGGER.error( + "Error occurred during Garmin Connect get activity request: %s", err + ) return except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occurred during Garmin Connect get stats") + _LOGGER.exception( + "Unknown error occurred during Garmin Connect get activity request" + ) return diff --git a/homeassistant/components/garmin_connect/const.py b/homeassistant/components/garmin_connect/const.py index 38245ff5eb8..ebf56b27a7f 100644 --- a/homeassistant/components/garmin_connect/const.py +++ b/homeassistant/components/garmin_connect/const.py @@ -309,4 +309,13 @@ GARMIN_ENTITY_LIST = { DEVICE_CLASS_TIMESTAMP, False, ], + "weight": ["Weight", "kg", "mdi:weight-kilogram", None, False], + "bmi": ["BMI", "", "mdi:food", None, False], + "bodyFat": ["Body Fat", "%", "mdi:food", None, False], + "bodyWater": ["Body Water", "%", "mdi:water-percent", None, False], + "bodyMass": ["Body Mass", "kg", "mdi:food", None, False], + "muscleMass": ["Muscle Mass", "kg", "mdi:dumbbell", None, False], + "physiqueRating": ["Physique Rating", "", "mdi:numeric", None, False], + "visceralFat": ["Visceral Fat", "", "mdi:food", None, False], + "metabolicAge": ["Metabolic Age", "", "mdi:calendar-heart", None, False], } diff --git a/homeassistant/components/garmin_connect/manifest.json b/homeassistant/components/garmin_connect/manifest.json index b2282831572..ee534354cb3 100644 --- a/homeassistant/components/garmin_connect/manifest.json +++ b/homeassistant/components/garmin_connect/manifest.json @@ -3,7 +3,7 @@ "name": "Garmin Connect", "documentation": "https://www.home-assistant.io/integrations/garmin_connect", "dependencies": [], - "requirements": ["garminconnect==0.1.8"], + "requirements": ["garminconnect==0.1.10"], "codeowners": ["@cyberjunky"], "config_flow": true } diff --git a/homeassistant/components/garmin_connect/sensor.py b/homeassistant/components/garmin_connect/sensor.py index 5edf54d95dc..78bf248c51b 100644 --- a/homeassistant/components/garmin_connect/sensor.py +++ b/homeassistant/components/garmin_connect/sensor.py @@ -34,7 +34,7 @@ async def async_setup_entry( ) as err: _LOGGER.error("Error occurred during Garmin Connect Client update: %s", err) except Exception: # pylint: disable=broad-except - _LOGGER.error("Unknown error occurred during Garmin Connect Client update.") + _LOGGER.exception("Unknown error occurred during Garmin Connect Client update.") entities = [] for ( @@ -172,6 +172,12 @@ class GarminConnectSensor(Entity): if "Duration" in self._type or "Seconds" in self._type: self._state = data[self._type] // 60 + elif "Mass" in self._type or self._type == "weight": + self._state = round((data[self._type] / 1000), 2) + elif ( + self._type == "bodyFat" or self._type == "bodyWater" or self._type == "bmi" + ): + self._state = round(data[self._type], 2) else: self._state = data[self._type] diff --git a/requirements_all.txt b/requirements_all.txt index d40e646ef54..5e0c9f762aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -576,7 +576,7 @@ fritzconnection==1.2.0 gTTS-token==1.1.3 # homeassistant.components.garmin_connect -garminconnect==0.1.8 +garminconnect==0.1.10 # homeassistant.components.gearbest gearbest_parser==1.0.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c69f2799c26..48441639880 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -214,7 +214,7 @@ foobot_async==0.3.1 gTTS-token==1.1.3 # homeassistant.components.garmin_connect -garminconnect==0.1.8 +garminconnect==0.1.10 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed From 0e2fa7700d4abd7d12601fbaa20780c1112abdf6 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Tue, 24 Mar 2020 12:39:38 -0600 Subject: [PATCH 237/431] =?UTF-8?q?Allow=20more=20than=20one=20AirVisual?= =?UTF-8?q?=20config=20entry=20with=20the=20same=20API=20k=E2=80=A6=20(#33?= =?UTF-8?q?072)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Allow more than one AirVisual config entry with the same API key * Add tests * Correctly pop geography * Code review * Code review --- .../airvisual/.translations/en.json | 2 +- .../components/airvisual/__init__.py | 111 +++++++++++------- .../components/airvisual/config_flow.py | 53 ++++----- homeassistant/components/airvisual/const.py | 1 - homeassistant/components/airvisual/sensor.py | 15 ++- .../components/airvisual/strings.json | 2 +- .../components/airvisual/test_config_flow.py | 71 +++++++++-- 7 files changed, 169 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/airvisual/.translations/en.json b/homeassistant/components/airvisual/.translations/en.json index 604baf1feb6..982ed8e13e7 100644 --- a/homeassistant/components/airvisual/.translations/en.json +++ b/homeassistant/components/airvisual/.translations/en.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "This API key is already in use." + "already_configured": "These coordinates have already been registered." }, "error": { "invalid_api_key": "Invalid API key" diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index a48acf7bb34..e234c2b1c67 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -1,5 +1,4 @@ """The airvisual component.""" -import asyncio import logging from pyairvisual import Client @@ -23,7 +22,6 @@ from homeassistant.helpers.event import async_track_time_interval from .const import ( CONF_CITY, CONF_COUNTRY, - CONF_GEOGRAPHIES, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, @@ -36,7 +34,7 @@ DATA_LISTENER = "listener" DEFAULT_OPTIONS = {CONF_SHOW_ON_MAP: True} -CONF_NODE_ID = "node_id" +CONF_GEOGRAPHIES = "geographies" GEOGRAPHY_COORDINATES_SCHEMA = vol.Schema( { @@ -70,34 +68,38 @@ CONFIG_SCHEMA = vol.Schema({DOMAIN: CLOUD_API_SCHEMA}, extra=vol.ALLOW_EXTRA) def async_get_geography_id(geography_dict): """Generate a unique ID from a geography dict.""" if CONF_CITY in geography_dict: - return ",".join( + return ", ".join( ( geography_dict[CONF_CITY], geography_dict[CONF_STATE], geography_dict[CONF_COUNTRY], ) ) - return ",".join( + return ", ".join( (str(geography_dict[CONF_LATITUDE]), str(geography_dict[CONF_LONGITUDE])) ) async def async_setup(hass, config): """Set up the AirVisual component.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} - hass.data[DOMAIN][DATA_LISTENER] = {} + hass.data[DOMAIN] = {DATA_CLIENT: {}, DATA_LISTENER: {}} if DOMAIN not in config: return True conf = config[DOMAIN] - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=conf + for geography in conf.get( + CONF_GEOGRAPHIES, + [{CONF_LATITUDE: hass.config.latitude, CONF_LONGITUDE: hass.config.longitude}], + ): + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_API_KEY: conf[CONF_API_KEY], **geography}, + ) ) - ) return True @@ -144,6 +146,45 @@ async def async_setup_entry(hass, config_entry): return True +async def async_migrate_entry(hass, config_entry): + """Migrate an old config entry.""" + version = config_entry.version + + _LOGGER.debug("Migrating from version %s", version) + + # 1 -> 2: One geography per config entry + if version == 1: + version = config_entry.version = 2 + + # Update the config entry to only include the first geography (there is always + # guaranteed to be at least one): + data = {**config_entry.data} + geographies = data.pop(CONF_GEOGRAPHIES) + first_geography = geographies.pop(0) + first_id = async_get_geography_id(first_geography) + + hass.config_entries.async_update_entry( + config_entry, + unique_id=first_id, + title=f"Cloud API ({first_id})", + data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **first_geography}, + ) + + # For any geographies that remain, create a new config entry for each one: + for geography in geographies: + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_API_KEY: config_entry.data[CONF_API_KEY], **geography}, + ) + ) + + _LOGGER.info("Migration to version %s successful", version) + + return True + + async def async_unload_entry(hass, config_entry): """Unload an AirVisual config entry.""" hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) @@ -170,40 +211,28 @@ class AirVisualData: self._client = client self._hass = hass self.data = {} + self.geography_data = config_entry.data + self.geography_id = config_entry.unique_id self.options = config_entry.options - self.geographies = { - async_get_geography_id(geography): geography - for geography in config_entry.data[CONF_GEOGRAPHIES] - } - async def async_update(self): """Get new data for all locations from the AirVisual cloud API.""" - tasks = [] + if CONF_CITY in self.geography_data: + api_coro = self._client.api.city( + self.geography_data[CONF_CITY], + self.geography_data[CONF_STATE], + self.geography_data[CONF_COUNTRY], + ) + else: + api_coro = self._client.api.nearest_city( + self.geography_data[CONF_LATITUDE], self.geography_data[CONF_LONGITUDE], + ) - for geography in self.geographies.values(): - if CONF_CITY in geography: - tasks.append( - self._client.api.city( - geography[CONF_CITY], - geography[CONF_STATE], - geography[CONF_COUNTRY], - ) - ) - else: - tasks.append( - self._client.api.nearest_city( - geography[CONF_LATITUDE], geography[CONF_LONGITUDE], - ) - ) - - results = await asyncio.gather(*tasks, return_exceptions=True) - for geography_id, result in zip(self.geographies, results): - if isinstance(result, AirVisualError): - _LOGGER.error("Error while retrieving data: %s", result) - self.data[geography_id] = {} - continue - self.data[geography_id] = result + try: + self.data[self.geography_id] = await api_coro + except AirVisualError as err: + _LOGGER.error("Error while retrieving data: %s", err) + self.data[self.geography_id] = {} _LOGGER.debug("Received new data") async_dispatcher_send(self._hass, TOPIC_UPDATE) diff --git a/homeassistant/components/airvisual/config_flow.py b/homeassistant/components/airvisual/config_flow.py index 2f961ccfb49..047f585a4ff 100644 --- a/homeassistant/components/airvisual/config_flow.py +++ b/homeassistant/components/airvisual/config_flow.py @@ -1,5 +1,5 @@ """Define a config flow manager for AirVisual.""" -import logging +import asyncio from pyairvisual import Client from pyairvisual.errors import InvalidKeyError @@ -15,15 +15,14 @@ from homeassistant.const import ( from homeassistant.core import callback from homeassistant.helpers import aiohttp_client, config_validation as cv -from .const import CONF_GEOGRAPHIES, DOMAIN # pylint: disable=unused-import - -_LOGGER = logging.getLogger("homeassistant.components.airvisual") +from . import async_get_geography_id +from .const import DOMAIN # pylint: disable=unused-import class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle an AirVisual config flow.""" - VERSION = 1 + VERSION = 2 CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @property @@ -68,35 +67,33 @@ class AirVisualFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if not user_input: return await self._show_form() - await self._async_set_unique_id(user_input[CONF_API_KEY]) + geo_id = async_get_geography_id(user_input) + await self._async_set_unique_id(geo_id) websession = aiohttp_client.async_get_clientsession(self.hass) client = Client(websession, api_key=user_input[CONF_API_KEY]) - try: - await client.api.nearest_city() - except InvalidKeyError: - return await self._show_form(errors={CONF_API_KEY: "invalid_api_key"}) - - data = {CONF_API_KEY: user_input[CONF_API_KEY]} - if user_input.get(CONF_GEOGRAPHIES): - data[CONF_GEOGRAPHIES] = user_input[CONF_GEOGRAPHIES] - else: - data[CONF_GEOGRAPHIES] = [ - { - CONF_LATITUDE: user_input.get( - CONF_LATITUDE, self.hass.config.latitude - ), - CONF_LONGITUDE: user_input.get( - CONF_LONGITUDE, self.hass.config.longitude - ), - } - ] - - return self.async_create_entry( - title=f"Cloud API (API key: {user_input[CONF_API_KEY][:4]}...)", data=data + # If this is the first (and only the first) time we've seen this API key, check + # that it's valid: + checked_keys = self.hass.data.setdefault("airvisual_checked_api_keys", set()) + check_keys_lock = self.hass.data.setdefault( + "airvisual_checked_api_keys_lock", asyncio.Lock() ) + async with check_keys_lock: + if user_input[CONF_API_KEY] not in checked_keys: + try: + await client.api.nearest_city() + except InvalidKeyError: + return await self._show_form( + errors={CONF_API_KEY: "invalid_api_key"} + ) + + checked_keys.add(user_input[CONF_API_KEY]) + return self.async_create_entry( + title=f"Cloud API ({geo_id})", data=user_input + ) + class AirVisualOptionsFlowHandler(config_entries.OptionsFlow): """Handle an AirVisual options flow.""" diff --git a/homeassistant/components/airvisual/const.py b/homeassistant/components/airvisual/const.py index ab54e191116..3bfc224a735 100644 --- a/homeassistant/components/airvisual/const.py +++ b/homeassistant/components/airvisual/const.py @@ -5,7 +5,6 @@ DOMAIN = "airvisual" CONF_CITY = "city" CONF_COUNTRY = "country" -CONF_GEOGRAPHIES = "geographies" DATA_CLIENT = "client" diff --git a/homeassistant/components/airvisual/sensor.py b/homeassistant/components/airvisual/sensor.py index 28d2b3f5f86..49a5f53361f 100644 --- a/homeassistant/components/airvisual/sensor.py +++ b/homeassistant/components/airvisual/sensor.py @@ -191,16 +191,19 @@ class AirVisualSensor(Entity): } ) - geography = self._airvisual.geographies[self._geography_id] - if CONF_LATITUDE in geography: + if CONF_LATITUDE in self._airvisual.geography_data: if self._airvisual.options[CONF_SHOW_ON_MAP]: - self._attrs[ATTR_LATITUDE] = geography[CONF_LATITUDE] - self._attrs[ATTR_LONGITUDE] = geography[CONF_LONGITUDE] + self._attrs[ATTR_LATITUDE] = self._airvisual.geography_data[ + CONF_LATITUDE + ] + self._attrs[ATTR_LONGITUDE] = self._airvisual.geography_data[ + CONF_LONGITUDE + ] self._attrs.pop("lati", None) self._attrs.pop("long", None) else: - self._attrs["lati"] = geography[CONF_LATITUDE] - self._attrs["long"] = geography[CONF_LONGITUDE] + self._attrs["lati"] = self._airvisual.geography_data[CONF_LATITUDE] + self._attrs["long"] = self._airvisual.geography_data[CONF_LONGITUDE] self._attrs.pop(ATTR_LATITUDE, None) self._attrs.pop(ATTR_LONGITUDE, None) diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index 6e94c393da6..8791e6d864d 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -16,7 +16,7 @@ "invalid_api_key": "Invalid API key" }, "abort": { - "already_configured": "This API key is already in use." + "already_configured": "These coordinates have already been registered." } }, "options": { diff --git a/tests/components/airvisual/test_config_flow.py b/tests/components/airvisual/test_config_flow.py index fb32a86a01a..d21aec14fa0 100644 --- a/tests/components/airvisual/test_config_flow.py +++ b/tests/components/airvisual/test_config_flow.py @@ -11,15 +11,22 @@ from homeassistant.const import ( CONF_LONGITUDE, CONF_SHOW_ON_MAP, ) +from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" - conf = {CONF_API_KEY: "abcde12345"} + conf = { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + } - MockConfigEntry(domain=DOMAIN, unique_id="abcde12345", data=conf).add_to_hass(hass) + MockConfigEntry( + domain=DOMAIN, unique_id="51.528308, -0.3817765", data=conf + ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -31,7 +38,11 @@ async def test_duplicate_error(hass): async def test_invalid_api_key(hass): """Test that invalid credentials throws an error.""" - conf = {CONF_API_KEY: "abcde12345"} + conf = { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + } with patch( "pyairvisual.api.API.nearest_city", side_effect=InvalidKeyError, @@ -42,6 +53,47 @@ async def test_invalid_api_key(hass): assert result["errors"] == {CONF_API_KEY: "invalid_api_key"} +async def test_migration_1_2(hass): + """Test migrating from version 1 to version 2.""" + conf = { + CONF_API_KEY: "abcde12345", + CONF_GEOGRAPHIES: [ + {CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}, + {CONF_LATITUDE: 35.48847, CONF_LONGITUDE: 137.5263065}, + ], + } + + config_entry = MockConfigEntry( + domain=DOMAIN, version=1, unique_id="abcde12345", data=conf + ) + config_entry.add_to_hass(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + + with patch("pyairvisual.api.API.nearest_city"): + assert await async_setup_component(hass, DOMAIN, {DOMAIN: conf}) + + config_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(config_entries) == 2 + + assert config_entries[0].unique_id == "51.528308, -0.3817765" + assert config_entries[0].title == "Cloud API (51.528308, -0.3817765)" + assert config_entries[0].data == { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, + } + + assert config_entries[1].unique_id == "35.48847, 137.5263065" + assert config_entries[1].title == "Cloud API (35.48847, 137.5263065)" + assert config_entries[1].data == { + CONF_API_KEY: "abcde12345", + CONF_LATITUDE: 35.48847, + CONF_LONGITUDE: 137.5263065, + } + + async def test_options_flow(hass): """Test config flow options.""" conf = {CONF_API_KEY: "abcde12345"} @@ -84,7 +136,8 @@ async def test_step_import(hass): """Test that the import step works.""" conf = { CONF_API_KEY: "abcde12345", - CONF_GEOGRAPHIES: [{CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}], + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, } with patch( @@ -95,10 +148,11 @@ async def test_step_import(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Cloud API (API key: abcd...)" + assert result["title"] == "Cloud API (51.528308, -0.3817765)" assert result["data"] == { CONF_API_KEY: "abcde12345", - CONF_GEOGRAPHIES: [{CONF_LATITUDE: 51.528308, CONF_LONGITUDE: -0.3817765}], + CONF_LATITUDE: 51.528308, + CONF_LONGITUDE: -0.3817765, } @@ -117,8 +171,9 @@ async def test_step_user(hass): DOMAIN, context={"source": SOURCE_USER}, data=conf ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "Cloud API (API key: abcd...)" + assert result["title"] == "Cloud API (32.87336, -117.22743)" assert result["data"] == { CONF_API_KEY: "abcde12345", - CONF_GEOGRAPHIES: [{CONF_LATITUDE: 32.87336, CONF_LONGITUDE: -117.22743}], + CONF_LATITUDE: 32.87336, + CONF_LONGITUDE: -117.22743, } From ad9f4db983b54e327758ccd6d8b7db8f2981fee4 Mon Sep 17 00:00:00 2001 From: Patryk <36936369+C6H6@users.noreply.github.com> Date: Tue, 24 Mar 2020 21:49:01 +0100 Subject: [PATCH 238/431] Add leak sensor support to Homekit integration (#33171) * Add leak sensor support to Homekit integration * Add leak entries to binary_sensor/strings.json * Revert changes from binary_sensor * Fix * Review fix --- .../homekit_controller/binary_sensor.py | 20 +++++++++++++++ .../components/homekit_controller/const.py | 1 + .../homekit_controller/test_binary_sensor.py | 25 +++++++++++++++++++ 3 files changed, 46 insertions(+) diff --git a/homeassistant/components/homekit_controller/binary_sensor.py b/homeassistant/components/homekit_controller/binary_sensor.py index 39d0e19ba40..b96e5f651e3 100644 --- a/homeassistant/components/homekit_controller/binary_sensor.py +++ b/homeassistant/components/homekit_controller/binary_sensor.py @@ -4,6 +4,7 @@ import logging from aiohomekit.model.characteristics import CharacteristicsTypes from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, @@ -89,11 +90,30 @@ class HomeKitOccupancySensor(HomeKitEntity, BinarySensorDevice): return self.service.value(CharacteristicsTypes.OCCUPANCY_DETECTED) == 1 +class HomeKitLeakSensor(HomeKitEntity, BinarySensorDevice): + """Representation of a Homekit leak sensor.""" + + def get_characteristic_types(self): + """Define the homekit characteristics the entity is tracking.""" + return [CharacteristicsTypes.LEAK_DETECTED] + + @property + def device_class(self): + """Define this binary_sensor as a leak sensor.""" + return DEVICE_CLASS_MOISTURE + + @property + def is_on(self): + """Return true if a leak is detected from the binary sensor.""" + return self.service.value(CharacteristicsTypes.LEAK_DETECTED) == 1 + + ENTITY_TYPES = { "motion": HomeKitMotionSensor, "contact": HomeKitContactSensor, "smoke": HomeKitSmokeSensor, "occupancy": HomeKitOccupancySensor, + "leak": HomeKitLeakSensor, } diff --git a/homeassistant/components/homekit_controller/const.py b/homeassistant/components/homekit_controller/const.py index a13ad22df51..7b40863141c 100644 --- a/homeassistant/components/homekit_controller/const.py +++ b/homeassistant/components/homekit_controller/const.py @@ -27,6 +27,7 @@ HOMEKIT_ACCESSORY_DISPATCH = { "temperature": "sensor", "battery": "sensor", "smoke": "binary_sensor", + "leak": "binary_sensor", "fan": "fan", "fanv2": "fan", "air-quality": "air_quality", diff --git a/tests/components/homekit_controller/test_binary_sensor.py b/tests/components/homekit_controller/test_binary_sensor.py index 8817ed5c22d..460d14d0d48 100644 --- a/tests/components/homekit_controller/test_binary_sensor.py +++ b/tests/components/homekit_controller/test_binary_sensor.py @@ -3,6 +3,7 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_MOISTURE, DEVICE_CLASS_MOTION, DEVICE_CLASS_OCCUPANCY, DEVICE_CLASS_OPENING, @@ -15,6 +16,7 @@ MOTION_DETECTED = ("motion", "motion-detected") CONTACT_STATE = ("contact", "contact-state") SMOKE_DETECTED = ("smoke", "smoke-detected") OCCUPANCY_DETECTED = ("occupancy", "occupancy-detected") +LEAK_DETECTED = ("leak", "leak-detected") def create_motion_sensor_service(accessory): @@ -107,3 +109,26 @@ async def test_occupancy_sensor_read_state(hass, utcnow): assert state.state == "on" assert state.attributes["device_class"] == DEVICE_CLASS_OCCUPANCY + + +def create_leak_sensor_service(accessory): + """Define leak characteristics.""" + service = accessory.add_service(ServicesTypes.LEAK_SENSOR) + + cur_state = service.add_char(CharacteristicsTypes.LEAK_DETECTED) + cur_state.value = 0 + + +async def test_leak_sensor_read_state(hass, utcnow): + """Test that we can read the state of a HomeKit leak sensor accessory.""" + helper = await setup_test_component(hass, create_leak_sensor_service) + + helper.characteristics[LEAK_DETECTED].value = 0 + state = await helper.poll_and_get_state() + assert state.state == "off" + + helper.characteristics[LEAK_DETECTED].value = 1 + state = await helper.poll_and_get_state() + assert state.state == "on" + + assert state.attributes["device_class"] == DEVICE_CLASS_MOISTURE From 3f4a7ec396c21758579ec0631c8a4244dcc1ee6f Mon Sep 17 00:00:00 2001 From: Balazs Keresztury Date: Tue, 24 Mar 2020 21:52:49 +0100 Subject: [PATCH 239/431] Fix minor bmp280 issues mentioned in late review (#33211) * Fix minor issues mentioned in #30837 after it was closed * Update homeassistant/components/bmp280/sensor.py Co-Authored-By: Martin Hjelmare Co-authored-by: Martin Hjelmare --- homeassistant/components/bmp280/sensor.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bmp280/sensor.py b/homeassistant/components/bmp280/sensor.py index 613902d1cd7..70efbce7d85 100644 --- a/homeassistant/components/bmp280/sensor.py +++ b/homeassistant/components/bmp280/sensor.py @@ -21,7 +21,7 @@ from homeassistant.util import Throttle _LOGGER = logging.getLogger(__name__) DEFAULT_NAME = "BMP280" -DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) +SCAN_INTERVAL = timedelta(seconds=15) MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=3) @@ -51,13 +51,14 @@ def setup_platform(hass, config, add_entities, discovery_info=None): # this usually happens when the board is I2C capable, but the device can't be found at the configured address if str(error.args[0]).startswith("No I2C device at address"): _LOGGER.error( - "%s. Hint: Check wiring and make sure that the SDO pin is tied to either ground (0x76) or VCC (0x77)!", + "%s. Hint: Check wiring and make sure that the SDO pin is tied to either ground (0x76) or VCC (0x77)", error.args[0], ) raise PlatformNotReady() - raise error + _LOGGER.error(error) + return # use custom name if there's any - name = config.get(CONF_NAME) + name = config[CONF_NAME] # BMP280 has both temperature and pressure sensing capability add_entities( [Bmp280TemperatureSensor(bmp280, name), Bmp280PressureSensor(bmp280, name)] From 46985bba0dda8a5eb0034aa3c1f4853320eaba76 Mon Sep 17 00:00:00 2001 From: brefra Date: Tue, 24 Mar 2020 21:58:50 +0100 Subject: [PATCH 240/431] Fix velbus dimming control (#33139) * Fix light control * Optimize conversion logic --- homeassistant/components/velbus/light.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/velbus/light.py b/homeassistant/components/velbus/light.py index d428b766edc..d7654feab2d 100644 --- a/homeassistant/components/velbus/light.py +++ b/homeassistant/components/velbus/light.py @@ -64,7 +64,7 @@ class VelbusLight(VelbusEntity, Light): @property def brightness(self): """Return the brightness of the light.""" - return self._module.get_dimmer_state(self._channel) + return int((self._module.get_dimmer_state(self._channel) * 255) / 100) def turn_on(self, **kwargs): """Instruct the Velbus light to turn on.""" @@ -80,10 +80,15 @@ class VelbusLight(VelbusEntity, Light): attr, *args = "set_led_state", self._channel, "on" 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", self._channel, - kwargs[ATTR_BRIGHTNESS], + brightness, kwargs.get(ATTR_TRANSITION, 0), ) else: From 2a36adae460c6a0c4680e4d3300cdb96f77915be Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 24 Mar 2020 19:09:24 -0500 Subject: [PATCH 241/431] Abort myq homekit config when one is already setup. (#33218) We can see myq on the network to tell them to configure it, but since the device will not give up the account it is bound to and there can be multiple myq gateways on a single account, we avoid showing the device as discovered once they already have one configured as they can always add a new one via "+" --- homeassistant/components/myq/config_flow.py | 8 +++++++ homeassistant/components/myq/strings.json | 2 +- tests/components/myq/test_config_flow.py | 24 +++++++++++++++++++++ 3 files changed, 33 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/myq/config_flow.py b/homeassistant/components/myq/config_flow.py index baa7aad4cff..07d57921e35 100644 --- a/homeassistant/components/myq/config_flow.py +++ b/homeassistant/components/myq/config_flow.py @@ -67,6 +67,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" + if self._async_current_entries(): + # We can see myq on the network to tell them to configure + # it, but since the device will not give up the account it is + # bound to and there can be multiple myq gateways on a single + # account, we avoid showing the device as discovered once + # they already have one configured as they can always + # add a new one via "+" + return self.async_abort(reason="already_configured") return await self.async_step_user() async def async_step_import(self, user_input): diff --git a/homeassistant/components/myq/strings.json b/homeassistant/components/myq/strings.json index c31162b2894..2aa0eab328e 100644 --- a/homeassistant/components/myq/strings.json +++ b/homeassistant/components/myq/strings.json @@ -19,4 +19,4 @@ "already_configured": "MyQ is already configured" } } -} \ No newline at end of file +} diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index c0bae8c5225..9fb3b34ca63 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -4,6 +4,9 @@ from pymyq.errors import InvalidCredentialsError, MyQError from homeassistant import config_entries, setup from homeassistant.components.myq.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry async def test_form_user(hass): @@ -101,3 +104,24 @@ async def test_form_cannot_connect(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_homekit(hass): + """Test that we abort from homekit if myq is already setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "homekit"} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "homekit"} + ) + assert result["type"] == "abort" From 28c2f9caa982db5d61b12c8f734badc48ad6ce9c Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 24 Mar 2020 20:32:46 -0400 Subject: [PATCH 242/431] Refactor ZHA platform setup (#33226) Setup ZHA platforms only after successful gateway startup. --- homeassistant/components/zha/__init__.py | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 63659a47e0d..5fef586d5cf 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -93,14 +93,10 @@ async def async_setup_entry(hass, config_entry): """ zha_data = hass.data.setdefault(DATA_ZHA, {}) - zha_data[DATA_ZHA_PLATFORM_LOADED] = {} config = zha_data.get(DATA_ZHA_CONFIG, {}) - zha_data[DATA_ZHA_DISPATCHERS] = [] for component in COMPONENTS: - zha_data[component] = [] - coro = hass.config_entries.async_forward_entry_setup(config_entry, component) - zha_data[DATA_ZHA_PLATFORM_LOADED][component] = hass.async_create_task(coro) + zha_data.setdefault(component, []) if config.get(CONF_ENABLE_QUIRKS, True): # needs to be done here so that the ZHA module is finished loading @@ -110,6 +106,12 @@ async def async_setup_entry(hass, config_entry): zha_gateway = ZHAGateway(hass, config, config_entry) await zha_gateway.async_initialize() + zha_data[DATA_ZHA_DISPATCHERS] = [] + zha_data[DATA_ZHA_PLATFORM_LOADED] = [] + for component in COMPONENTS: + coro = hass.config_entries.async_forward_entry_setup(config_entry, component) + zha_data[DATA_ZHA_PLATFORM_LOADED].append(hass.async_create_task(coro)) + device_registry = await hass.helpers.device_registry.async_get_registry() device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -128,7 +130,7 @@ async def async_setup_entry(hass, config_entry): await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown) - hass.async_create_task(async_load_entities(hass, config_entry)) + asyncio.create_task(async_load_entities(hass, config_entry)) return True @@ -153,11 +155,7 @@ async def async_load_entities( ) -> None: """Load entities after integration was setup.""" await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_prepare_entities() - to_setup = [ - hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED][comp] - for comp in COMPONENTS - if hass.data[DATA_ZHA][comp] - ] + to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] results = await asyncio.gather(*to_setup, return_exceptions=True) for res in results: if isinstance(res, Exception): From 44425a184e2f26519d780d7f952969671a582603 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Wed, 25 Mar 2020 08:47:01 +0100 Subject: [PATCH 243/431] Fix pre-commit hooks env for gen_requirements_all & hassfest (#33187) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Run-in-env script.gen_requirements_all and hassfest in pre-commit * Apply suggestions from code review and change `language` to script Co-authored-by: Ville Skyttä --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 017d0145da1..4211438379c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -61,15 +61,15 @@ repos: files: ^homeassistant/.+\.py$ - id: gen_requirements_all name: gen_requirements_all - entry: python3 -m script.gen_requirements_all + entry: script/run-in-env.sh python3 -m script.gen_requirements_all pass_filenames: false - language: system + language: script types: [json] files: ^homeassistant/.+/manifest\.json$ - id: hassfest name: hassfest - entry: python3 -m script.hassfest + entry: script/run-in-env.sh python3 -m script.hassfest pass_filenames: false - language: system + language: script types: [json] files: ^homeassistant/.+/manifest\.json$ From 3ee05ad4bbb3171fe45a2bdf586d64893b3cd4bd Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 25 Mar 2020 10:14:15 +0100 Subject: [PATCH 244/431] Enable Jemalloc for docker Core (#33237) --- build.json | 10 +++++----- rootfs/etc/services.d/home-assistant/finish | 2 ++ rootfs/etc/services.d/home-assistant/run | 3 +++ 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/build.json b/build.json index c61a693af1c..331999b5470 100644 --- a/build.json +++ b/build.json @@ -1,11 +1,11 @@ { "image": "homeassistant/{arch}-homeassistant", "build_from": { - "aarch64": "homeassistant/aarch64-homeassistant-base:7.0.1", - "armhf": "homeassistant/armhf-homeassistant-base:7.0.1", - "armv7": "homeassistant/armv7-homeassistant-base:7.0.1", - "amd64": "homeassistant/amd64-homeassistant-base:7.0.1", - "i386": "homeassistant/i386-homeassistant-base:7.0.1" + "aarch64": "homeassistant/aarch64-homeassistant-base:7.1.0", + "armhf": "homeassistant/armhf-homeassistant-base:7.1.0", + "armv7": "homeassistant/armv7-homeassistant-base:7.1.0", + "amd64": "homeassistant/amd64-homeassistant-base:7.1.0", + "i386": "homeassistant/i386-homeassistant-base:7.1.0" }, "labels": { "io.hass.type": "core" diff --git a/rootfs/etc/services.d/home-assistant/finish b/rootfs/etc/services.d/home-assistant/finish index 84b7abcab8b..3afed0ca8d8 100644 --- a/rootfs/etc/services.d/home-assistant/finish +++ b/rootfs/etc/services.d/home-assistant/finish @@ -2,4 +2,6 @@ # ============================================================================== # Take down the S6 supervision tree when Home Assistant fails # ============================================================================== +if { s6-test ${1} -ne 100 } + s6-svscanctl -t /var/run/s6/services \ No newline at end of file diff --git a/rootfs/etc/services.d/home-assistant/run b/rootfs/etc/services.d/home-assistant/run index a153db56b61..750d00a91ec 100644 --- a/rootfs/etc/services.d/home-assistant/run +++ b/rootfs/etc/services.d/home-assistant/run @@ -4,4 +4,7 @@ # ============================================================================== cd /config || bashio::exit.nok "Can't find config folder!" +# Enable Jemalloc for Home Assistant Core +export LD_PRELOAD="/usr/local/lib/libjemalloc.so.2" + exec python3 -m homeassistant --config /config From 2a3c94bad04386c5b59a45cab5232fbc647897e2 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Mar 2020 07:23:54 -0400 Subject: [PATCH 245/431] Add group entity support to ZHA (#33196) * split entity into base and entity * add initial light group support * add dispatching of groups to light * added zha group object * add group event listener * add and remove group members * get group by name * fix rebase * fix rebase * use group_id for unique_id * get entities from registry * use group name * update entity domain * update zha storage to handle groups * dispatch group entities * update light group * fix group remove and dispatch light group entities * allow picking the domain for group entities * beginning - auto determine entity domain * move methods to helpers so they can be shared * fix rebase * remove double init groups... again * cleanup startup * use asyncio create task * group entity discovery * add logging and fix group name * add logging and update group after probe if needed * test add group via gateway * add method to get group entity ids * update storage * test get group by name * update storage on remove * test group with single member * add light group tests * test some light group logic * type hints * fix tests and cleanup * revert init changes except for create task * remove group entity domain changing for now * add missing import * tricky code saving * review comments * clean up class defs * cleanup * fix rebase because I cant read * make pylint happy --- homeassistant/components/zha/__init__.py | 8 +- homeassistant/components/zha/core/const.py | 2 + homeassistant/components/zha/core/device.py | 2 +- .../components/zha/core/discovery.py | 100 ++++++ homeassistant/components/zha/core/gateway.py | 123 ++++--- homeassistant/components/zha/core/group.py | 80 ++++- homeassistant/components/zha/core/helpers.py | 42 ++- .../components/zha/core/registries.py | 20 ++ homeassistant/components/zha/core/store.py | 117 +++++- homeassistant/components/zha/core/typing.py | 4 + homeassistant/components/zha/entity.py | 153 ++++---- homeassistant/components/zha/light.py | 339 +++++++++++++----- tests/components/zha/common.py | 39 +- tests/components/zha/conftest.py | 3 +- tests/components/zha/test_gateway.py | 140 +++++++- tests/components/zha/test_light.py | 169 ++++++++- 16 files changed, 1084 insertions(+), 257 deletions(-) diff --git a/homeassistant/components/zha/__init__.py b/homeassistant/components/zha/__init__.py index 5fef586d5cf..2af35e8fb92 100644 --- a/homeassistant/components/zha/__init__.py +++ b/homeassistant/components/zha/__init__.py @@ -130,7 +130,7 @@ async def async_setup_entry(hass, config_entry): await zha_data[DATA_ZHA_GATEWAY].async_update_device_storage() hass.bus.async_listen_once(ha_const.EVENT_HOMEASSISTANT_STOP, async_zha_shutdown) - asyncio.create_task(async_load_entities(hass, config_entry)) + asyncio.create_task(async_load_entities(hass)) return True @@ -150,11 +150,9 @@ async def async_unload_entry(hass, config_entry): return True -async def async_load_entities( - hass: HomeAssistantType, config_entry: config_entries.ConfigEntry -) -> None: +async def async_load_entities(hass: HomeAssistantType) -> None: """Load entities after integration was setup.""" - await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_prepare_entities() + await hass.data[DATA_ZHA][DATA_ZHA_GATEWAY].async_initialize_devices_and_entities() to_setup = hass.data[DATA_ZHA][DATA_ZHA_PLATFORM_LOADED] results = await asyncio.gather(*to_setup, return_exceptions=True) for res in results: diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c2813c464e5..2eb567ab4c4 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -23,6 +23,7 @@ ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE_IEEE = "device_ieee" ATTR_DEVICE_TYPE = "device_type" ATTR_ENDPOINT_ID = "endpoint_id" +ATTR_ENTITY_DOMAIN = "entity_domain" ATTR_IEEE = "ieee" ATTR_LAST_SEEN = "last_seen" ATTR_LEVEL = "level" @@ -207,6 +208,7 @@ SIGNAL_REMOVE = "remove" SIGNAL_SET_LEVEL = "set_level" SIGNAL_STATE_ATTR = "update_state_attribute" SIGNAL_UPDATE_DEVICE = "{}_zha_update_device" +SIGNAL_REMOVE_GROUP = "remove_group" UNKNOWN = "unknown" UNKNOWN_MANUFACTURER = "unk_manufacturer" diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 47b564f1767..0a7278cb5d5 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -373,7 +373,7 @@ class ZHADevice(LogMixin): self.debug("started configuration") await self._channels.async_configure() self.debug("completed configuration") - entry = self.gateway.zha_storage.async_create_or_update(self) + entry = self.gateway.zha_storage.async_create_or_update_device(self) self.debug("stored in registry: %s", entry) if self._channels.identify_ch is not None: diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 5f8f6b593f8..7202fd869fa 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -1,10 +1,12 @@ """Device discovery functions for Zigbee Home Automation.""" +from collections import Counter import logging from typing import Callable, List, Tuple from homeassistant import const as ha_const from homeassistant.core import callback +from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import HomeAssistantType from . import const as zha_const, registries as zha_regs, typing as zha_typing @@ -157,4 +159,102 @@ class ProbeEndpoint: self._device_configs.update(overrides) +class GroupProbe: + """Determine the appropriate component for a group.""" + + def __init__(self): + """Initialize instance.""" + self._hass = None + + def initialize(self, hass: HomeAssistantType) -> None: + """Initialize the group probe.""" + self._hass = hass + + @callback + def discover_group_entities(self, group: zha_typing.ZhaGroupType) -> None: + """Process a group and create any entities that are needed.""" + # only create a group entity if there are 2 or more members in a group + if len(group.members) < 2: + _LOGGER.debug( + "Group: %s:0x%04x has less than 2 members - skipping entity discovery", + group.name, + group.group_id, + ) + return + + if group.entity_domain is None: + _LOGGER.debug( + "Group: %s:0x%04x has no user set entity domain - attempting entity domain discovery", + group.name, + group.group_id, + ) + group.entity_domain = GroupProbe.determine_default_entity_domain( + self._hass, group + ) + + if group.entity_domain is None: + return + + _LOGGER.debug( + "Group: %s:0x%04x has an entity domain of: %s after discovery", + group.name, + group.group_id, + group.entity_domain, + ) + + zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + entity_class = zha_regs.ZHA_ENTITIES.get_group_entity(group.entity_domain) + if entity_class is None: + return + + self._hass.data[zha_const.DATA_ZHA][group.entity_domain].append( + ( + entity_class, + ( + group.domain_entity_ids, + f"{group.entity_domain}_group_{group.group_id}", + group.group_id, + zha_gateway.coordinator_zha_device, + ), + ) + ) + + @staticmethod + def determine_default_entity_domain( + hass: HomeAssistantType, group: zha_typing.ZhaGroupType + ): + """Determine the default entity domain for this group.""" + if len(group.members) < 2: + _LOGGER.debug( + "Group: %s:0x%04x has less than 2 members so cannot default an entity domain", + group.name, + group.group_id, + ) + return None + + zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] + all_domain_occurrences = [] + for device in group.members: + entities = async_entries_for_device( + zha_gateway.ha_entity_registry, device.device_id + ) + all_domain_occurrences.extend( + [ + entity.domain + for entity in entities + if entity.domain in zha_regs.GROUP_ENTITY_DOMAINS + ] + ) + counts = Counter(all_domain_occurrences) + domain = counts.most_common(1)[0][0] + _LOGGER.debug( + "The default entity domain is: %s for group: %s:0x%04x", + domain, + group.name, + group.group_id, + ) + return domain + + PROBE = ProbeEndpoint() +GROUP_PROBE = GroupProbe() diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 78b5f939cae..9d5bf609ed2 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -6,6 +6,7 @@ import itertools import logging import os import traceback +from typing import List, Optional from serial import SerialException import zigpy.device as zigpy_dev @@ -52,6 +53,7 @@ from .const import ( DOMAIN, SIGNAL_ADD_ENTITIES, SIGNAL_REMOVE, + SIGNAL_REMOVE_GROUP, UNKNOWN_MANUFACTURER, UNKNOWN_MODEL, ZHA_GW_MSG, @@ -75,6 +77,7 @@ from .group import ZHAGroup from .patches import apply_application_controller_patch from .registries import RADIO_TYPES from .store import async_get_registry +from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType _LOGGER = logging.getLogger(__name__) @@ -93,6 +96,7 @@ class ZHAGateway: self._config = config self._devices = {} self._groups = {} + self.coordinator_zha_device = None self._device_registry = collections.defaultdict(list) self.zha_storage = None self.ha_device_registry = None @@ -110,6 +114,7 @@ class ZHAGateway: async def async_initialize(self): """Initialize controller and connect radio.""" discovery.PROBE.initialize(self._hass) + discovery.GROUP_PROBE.initialize(self._hass) self.zha_storage = await async_get_registry(self._hass) self.ha_device_registry = await get_dev_reg(self._hass) @@ -156,17 +161,29 @@ class ZHAGateway: self._hass.data[DATA_ZHA][DATA_ZHA_BRIDGE_ID] = str( self.application_controller.ieee ) - await self.async_load_devices() - self._initialize_groups() + self.async_load_devices() + self.async_load_groups() - async def async_load_devices(self) -> None: + @callback + def async_load_devices(self) -> None: """Restore ZHA devices from zigpy application state.""" zigpy_devices = self.application_controller.devices.values() for zigpy_device in zigpy_devices: - self._async_get_or_create_device(zigpy_device, restored=True) + zha_device = self._async_get_or_create_device(zigpy_device, restored=True) + if zha_device.nwk == 0x0000: + self.coordinator_zha_device = zha_device - async def async_prepare_entities(self) -> None: - """Prepare entities by initializing device channels.""" + @callback + def async_load_groups(self) -> None: + """Initialize ZHA groups.""" + for group_id in self.application_controller.groups: + group = self.application_controller.groups[group_id] + zha_group = self._async_get_or_create_group(group) + # we can do this here because the entities are in the entity registry tied to the devices + discovery.GROUP_PROBE.discover_group_entities(zha_group) + + async def async_initialize_devices_and_entities(self) -> None: + """Initialize devices and load entities.""" semaphore = asyncio.Semaphore(2) async def _throttle(zha_device: zha_typing.ZhaDeviceType, cached: bool): @@ -231,35 +248,44 @@ class ZHAGateway: """Handle device leaving the network.""" self.async_update_device(device, False) - def group_member_removed(self, zigpy_group, endpoint): + def group_member_removed( + self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType + ) -> None: """Handle zigpy group member removed event.""" # need to handle endpoint correctly on groups zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_member_removed - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) - def group_member_added(self, zigpy_group, endpoint): + def group_member_added( + self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType + ) -> None: """Handle zigpy group member added event.""" # need to handle endpoint correctly on groups zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_member_added - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) - def group_added(self, zigpy_group): + def group_added(self, zigpy_group: ZigpyGroupType) -> None: """Handle zigpy group added event.""" zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_added") # need to dispatch for entity creation here self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_ADDED) - def group_removed(self, zigpy_group): + def group_removed(self, zigpy_group: ZigpyGroupType) -> None: """Handle zigpy group added event.""" self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_REMOVED) zha_group = self._groups.pop(zigpy_group.group_id, None) zha_group.info("group_removed") + async_dispatcher_send( + self._hass, f"{SIGNAL_REMOVE_GROUP}_{zigpy_group.group_id}" + ) - def _send_group_gateway_message(self, zigpy_group, gateway_message_type): - """Send the gareway event for a zigpy group event.""" + def _send_group_gateway_message( + self, zigpy_group: ZigpyGroupType, gateway_message_type: str + ) -> None: + """Send the gateway event for a zigpy group event.""" zha_group = self._groups.get(zigpy_group.group_id) if zha_group is not None: async_dispatcher_send( @@ -306,12 +332,12 @@ class ZHAGateway: """Return ZHADevice for given ieee.""" return self._devices.get(ieee) - def get_group(self, group_id): + def get_group(self, group_id: str) -> Optional[ZhaGroupType]: """Return Group for given group id.""" return self.groups.get(group_id) @callback - def async_get_group_by_name(self, group_name): + def async_get_group_by_name(self, group_name: str) -> Optional[ZhaGroupType]: """Get ZHA group by name.""" for group in self.groups.values(): if group.name == group_name: @@ -390,12 +416,6 @@ class ZHAGateway: logging.getLogger(logger_name).removeHandler(self._log_relay_handler) self.debug_enabled = False - def _initialize_groups(self): - """Initialize ZHA groups.""" - for group_id in self.application_controller.groups: - group = self.application_controller.groups[group_id] - self._async_get_or_create_group(group) - @callback def _async_get_or_create_device( self, zigpy_device: zha_typing.ZigpyDeviceType, restored: bool = False @@ -414,17 +434,19 @@ class ZHAGateway: model=zha_device.model, ) zha_device.set_device_id(device_registry_device.id) - entry = self.zha_storage.async_get_or_create(zha_device) + entry = self.zha_storage.async_get_or_create_device(zha_device) zha_device.async_update_last_seen(entry.last_seen) return zha_device @callback - def _async_get_or_create_group(self, zigpy_group): + def _async_get_or_create_group(self, zigpy_group: ZigpyGroupType) -> ZhaGroupType: """Get or create a ZHA group.""" zha_group = self._groups.get(zigpy_group.group_id) if zha_group is None: zha_group = ZHAGroup(self._hass, self, zigpy_group) self._groups[zigpy_group.group_id] = zha_group + group_entry = self.zha_storage.async_get_or_create_group(zha_group) + zha_group.entity_domain = group_entry.entity_domain return zha_group @callback @@ -446,7 +468,9 @@ class ZHAGateway: async def async_update_device_storage(self): """Update the devices in the store.""" for device in self.devices.values(): - self.zha_storage.async_update(device) + self.zha_storage.async_update_device(device) + for group in self.groups.values(): + self.zha_storage.async_update_group(group) await self.zha_storage.async_save() async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): @@ -494,25 +518,6 @@ class ZHAGateway: zha_device.update_available(True) async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) - # only public for testing - async def async_device_restored(self, device: zha_typing.ZigpyDeviceType): - """Add an existing device to the ZHA zigbee network when ZHA first starts.""" - zha_device = self._async_get_or_create_device(device, restored=True) - - if zha_device.is_mains_powered: - # the device isn't a battery powered device so we should be able - # to update it now - _LOGGER.debug( - "attempting to request fresh state for device - %s:%s %s with power source %s", - zha_device.nwk, - zha_device.ieee, - zha_device.name, - zha_device.power_source, - ) - await zha_device.async_initialize(from_cache=False) - else: - await zha_device.async_initialize(from_cache=True) - async def _async_device_rejoined(self, zha_device): _LOGGER.debug( "skipping discovery for previously discovered device - %s:%s", @@ -524,7 +529,9 @@ class ZHAGateway: # will cause async_init to fire so don't explicitly call it zha_device.update_available(True) - async def async_create_zigpy_group(self, name, members): + async def async_create_zigpy_group( + self, name: str, members: List[ZhaDeviceType] + ) -> ZhaGroupType: """Create a new Zigpy Zigbee group.""" # we start with one to fill any gaps from a user removing existing groups group_id = 1 @@ -537,24 +544,40 @@ class ZHAGateway: if members is not None: tasks = [] for ieee in members: + _LOGGER.debug( + "Adding member with IEEE: %s to group: %s:0x%04x", + ieee, + name, + group_id, + ) tasks.append(self.devices[ieee].async_add_to_group(group_id)) await asyncio.gather(*tasks) - return self.groups.get(group_id) + zha_group = self.groups.get(group_id) + _LOGGER.debug( + "Probing group: %s:0x%04x for entity discovery", + zha_group.name, + zha_group.group_id, + ) + discovery.GROUP_PROBE.discover_group_entities(zha_group) + if zha_group.entity_domain is not None: + self.zha_storage.async_update_group(zha_group) + async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) + return zha_group - async def async_remove_zigpy_group(self, group_id): + async def async_remove_zigpy_group(self, group_id: int) -> None: """Remove a Zigbee group from Zigpy.""" group = self.groups.get(group_id) + if not group: + _LOGGER.debug("Group: %s:0x%04x could not be found", group.name, group_id) + return if group and group.members: tasks = [] for member in group.members: tasks.append(member.async_remove_from_group(group_id)) if tasks: await asyncio.gather(*tasks) - else: - # we have members but none are tracked by ZHA for whatever reason - self.application_controller.groups.pop(group_id) - else: - self.application_controller.groups.pop(group_id) + self.application_controller.groups.pop(group_id) + self.zha_storage.async_delete_group(group) async def shutdown(self): """Stop ZHA Controller Application.""" diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index ca2cc0ff1d3..e6b2dee0625 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -1,10 +1,16 @@ """Group for Zigbee Home Automation.""" import asyncio import logging +from typing import Any, Dict, List, Optional + +from zigpy.types.named import EUI64 from homeassistant.core import callback +from homeassistant.helpers.entity_registry import async_entries_for_device +from homeassistant.helpers.typing import HomeAssistantType from .helpers import LogMixin +from .typing import ZhaDeviceType, ZhaGatewayType, ZigpyEndpointType, ZigpyGroupType _LOGGER = logging.getLogger(__name__) @@ -12,29 +18,45 @@ _LOGGER = logging.getLogger(__name__) class ZHAGroup(LogMixin): """ZHA Zigbee group object.""" - def __init__(self, hass, zha_gateway, zigpy_group): + def __init__( + self, + hass: HomeAssistantType, + zha_gateway: ZhaGatewayType, + zigpy_group: ZigpyGroupType, + ): """Initialize the group.""" - self.hass = hass - self._zigpy_group = zigpy_group - self._zha_gateway = zha_gateway + self.hass: HomeAssistantType = hass + self._zigpy_group: ZigpyGroupType = zigpy_group + self._zha_gateway: ZhaGatewayType = zha_gateway + self._entity_domain: str = None @property - def name(self): + def name(self) -> str: """Return group name.""" return self._zigpy_group.name @property - def group_id(self): + def group_id(self) -> int: """Return group name.""" return self._zigpy_group.group_id @property - def endpoint(self): + def endpoint(self) -> ZigpyEndpointType: """Return the endpoint for this group.""" return self._zigpy_group.endpoint @property - def members(self): + def entity_domain(self) -> Optional[str]: + """Return the domain that will be used for the entity representing this group.""" + return self._entity_domain + + @entity_domain.setter + def entity_domain(self, domain: Optional[str]) -> None: + """Set the domain that will be used for the entity representing this group.""" + self._entity_domain = domain + + @property + def members(self) -> List[ZhaDeviceType]: """Return the ZHA devices that are members of this group.""" return [ self._zha_gateway.devices.get(member_ieee[0]) @@ -42,7 +64,7 @@ class ZHAGroup(LogMixin): if member_ieee[0] in self._zha_gateway.devices ] - async def async_add_members(self, member_ieee_addresses): + async def async_add_members(self, member_ieee_addresses: List[EUI64]) -> None: """Add members to this group.""" if len(member_ieee_addresses) > 1: tasks = [] @@ -56,7 +78,7 @@ class ZHAGroup(LogMixin): member_ieee_addresses[0] ].async_add_to_group(self.group_id) - async def async_remove_members(self, member_ieee_addresses): + async def async_remove_members(self, member_ieee_addresses: List[EUI64]) -> None: """Remove members from this group.""" if len(member_ieee_addresses) > 1: tasks = [] @@ -72,18 +94,50 @@ class ZHAGroup(LogMixin): member_ieee_addresses[0] ].async_remove_from_group(self.group_id) + @property + def member_entity_ids(self) -> List[str]: + """Return the ZHA entity ids for all entities for the members of this group.""" + all_entity_ids: List[str] = [] + for device in self.members: + entities = async_entries_for_device( + self._zha_gateway.ha_entity_registry, device.device_id + ) + for entity in entities: + all_entity_ids.append(entity.entity_id) + return all_entity_ids + + @property + def domain_entity_ids(self) -> List[str]: + """Return entity ids from the entity domain for this group.""" + if self.entity_domain is None: + return + domain_entity_ids: List[str] = [] + for device in self.members: + entities = async_entries_for_device( + self._zha_gateway.ha_entity_registry, device.device_id + ) + domain_entity_ids.extend( + [ + entity.entity_id + for entity in entities + if entity.domain == self.entity_domain + ] + ) + return domain_entity_ids + @callback - def async_get_info(self): + def async_get_info(self) -> Dict[str, Any]: """Get ZHA group info.""" - group_info = {} + group_info: Dict[str, Any] = {} group_info["group_id"] = self.group_id + group_info["entity_domain"] = self.entity_domain group_info["name"] = self.name group_info["members"] = [ zha_device.async_get_info() for zha_device in self.members ] return group_info - def log(self, level, msg, *args): + def log(self, level: int, msg: str, *args): """Log a message.""" msg = f"[%s](%s): {msg}" args = (self.name, self.group_id) + args diff --git a/homeassistant/components/zha/core/helpers.py b/homeassistant/components/zha/core/helpers.py index ab4c7ae540c..4441ac90717 100644 --- a/homeassistant/components/zha/core/helpers.py +++ b/homeassistant/components/zha/core/helpers.py @@ -1,10 +1,11 @@ """Helpers for Zigbee Home Automation.""" import collections import logging +from typing import Any, Callable, Iterator, List, Optional import zigpy.types -from homeassistant.core import callback +from homeassistant.core import State, callback from .const import CLUSTER_TYPE_IN, CLUSTER_TYPE_OUT, DATA_ZHA, DATA_ZHA_GATEWAY from .registries import BINDABLE_CLUSTERS @@ -85,6 +86,45 @@ async def async_get_zha_device(hass, device_id): return zha_gateway.devices[ieee] +def find_state_attributes(states: List[State], key: str) -> Iterator[Any]: + """Find attributes with matching key from states.""" + for state in states: + value = state.attributes.get(key) + if value is not None: + yield value + + +def mean_int(*args): + """Return the mean of the supplied values.""" + return int(sum(args) / len(args)) + + +def mean_tuple(*args): + """Return the mean values along the columns of the supplied values.""" + return tuple(sum(l) / len(l) for l in zip(*args)) + + +def reduce_attribute( + states: List[State], + key: str, + default: Optional[Any] = None, + reduce: Callable[..., Any] = mean_int, +) -> Any: + """Find the first attribute matching key from states. + + If none are found, return default. + """ + attrs = list(find_state_attributes(states, key)) + + if not attrs: + return default + + if len(attrs) == 1: + return attrs[0] + + return reduce(*attrs) + + class LogMixin: """Log helper.""" diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 0a1a81df5ff..34ae32c01c8 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -32,6 +32,8 @@ from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType +GROUP_ENTITY_DOMAINS = [LIGHT] + SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 SMARTTHINGS_HUMIDITY_CLUSTER = 0xFC45 @@ -275,6 +277,9 @@ RegistryDictType = Dict[ ] # pylint: disable=invalid-name +GroupRegistryDictType = Dict[str, CALLABLE_T] # pylint: disable=invalid-name + + class ZHAEntityRegistry: """Channel to ZHA Entity mapping.""" @@ -282,6 +287,7 @@ class ZHAEntityRegistry: """Initialize Registry instance.""" self._strict_registry: RegistryDictType = collections.defaultdict(dict) self._loose_registry: RegistryDictType = collections.defaultdict(dict) + self._group_registry: GroupRegistryDictType = {} def get_entity( self, @@ -300,6 +306,10 @@ class ZHAEntityRegistry: return default, [] + def get_group_entity(self, component: str) -> CALLABLE_T: + """Match a ZHA group to a ZHA Entity class.""" + return self._group_registry.get(component) + def strict_match( self, component: str, @@ -350,5 +360,15 @@ class ZHAEntityRegistry: return decorator + def group_match(self, component: str) -> Callable[[CALLABLE_T], CALLABLE_T]: + """Decorate a group match rule.""" + + def decorator(zha_ent: CALLABLE_T) -> CALLABLE_T: + """Register a group match rule.""" + self._group_registry[component] = zha_ent + return zha_ent + + return decorator + ZHA_ENTITIES = ZHAEntityRegistry() diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 46fef76b656..0cd9e045cb6 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -10,6 +10,8 @@ from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass +from .typing import ZhaDeviceType, ZhaGroupType + _LOGGER = logging.getLogger(__name__) DATA_REGISTRY = "zha_storage" @@ -28,52 +30,96 @@ class ZhaDeviceEntry: last_seen = attr.ib(type=float, default=None) -class ZhaDeviceStorage: +@attr.s(slots=True, frozen=True) +class ZhaGroupEntry: + """Zha Group storage Entry.""" + + name = attr.ib(type=str, default=None) + group_id = attr.ib(type=int, default=None) + entity_domain = attr.ib(type=float, default=None) + + +class ZhaStorage: """Class to hold a registry of zha devices.""" def __init__(self, hass: HomeAssistantType) -> None: """Initialize the zha device storage.""" - self.hass = hass + self.hass: HomeAssistantType = hass self.devices: MutableMapping[str, ZhaDeviceEntry] = {} + self.groups: MutableMapping[str, ZhaGroupEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback - def async_create(self, device) -> ZhaDeviceEntry: + def async_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Create a new ZhaDeviceEntry.""" - device_entry = ZhaDeviceEntry( + device_entry: ZhaDeviceEntry = ZhaDeviceEntry( name=device.name, ieee=str(device.ieee), last_seen=device.last_seen ) self.devices[device_entry.ieee] = device_entry - return self.async_update(device) + return self.async_update_device(device) @callback - def async_get_or_create(self, device) -> ZhaDeviceEntry: + def async_create_group(self, group: ZhaGroupType) -> ZhaGroupEntry: + """Create a new ZhaGroupEntry.""" + group_entry: ZhaGroupEntry = ZhaGroupEntry( + name=group.name, + group_id=str(group.group_id), + entity_domain=group.entity_domain, + ) + self.groups[str(group.group_id)] = group_entry + return self.async_update_group(group) + + @callback + def async_get_or_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Create a new ZhaDeviceEntry.""" - ieee_str = str(device.ieee) + ieee_str: str = str(device.ieee) if ieee_str in self.devices: return self.devices[ieee_str] - return self.async_create(device) + return self.async_create_device(device) @callback - def async_create_or_update(self, device) -> ZhaDeviceEntry: + def async_get_or_create_group(self, group: ZhaGroupType) -> ZhaGroupEntry: + """Create a new ZhaGroupEntry.""" + group_id: str = str(group.group_id) + if group_id in self.groups: + return self.groups[group_id] + return self.async_create_group(group) + + @callback + def async_create_or_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Create or update a ZhaDeviceEntry.""" if str(device.ieee) in self.devices: - return self.async_update(device) - return self.async_create(device) + return self.async_update_device(device) + return self.async_create_device(device) @callback - def async_delete(self, device) -> None: + def async_create_or_update_group(self, group: ZhaGroupType) -> ZhaGroupEntry: + """Create or update a ZhaGroupEntry.""" + if str(group.group_id) in self.groups: + return self.async_update_group(group) + return self.async_create_group(group) + + @callback + def async_delete_device(self, device: ZhaDeviceType) -> None: """Delete ZhaDeviceEntry.""" - ieee_str = str(device.ieee) + ieee_str: str = str(device.ieee) if ieee_str in self.devices: del self.devices[ieee_str] self.async_schedule_save() @callback - def async_update(self, device) -> ZhaDeviceEntry: + def async_delete_group(self, group: ZhaGroupType) -> None: + """Delete ZhaGroupEntry.""" + group_id: str = str(group.group_id) + if group_id in self.groups: + del self.groups[group_id] + self.async_schedule_save() + + @callback + def async_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Update name of ZhaDeviceEntry.""" - ieee_str = str(device.ieee) + ieee_str: str = str(device.ieee) old = self.devices[ieee_str] changes = {} @@ -83,11 +129,25 @@ class ZhaDeviceStorage: self.async_schedule_save() return new + @callback + def async_update_group(self, group: ZhaGroupType) -> ZhaGroupEntry: + """Update name of ZhaGroupEntry.""" + group_id: str = str(group.group_id) + old = self.groups[group_id] + + changes = {} + changes["entity_domain"] = group.entity_domain + + new = self.groups[group_id] = attr.evolve(old, **changes) + self.async_schedule_save() + return new + async def async_load(self) -> None: """Load the registry of zha device entries.""" data = await self._store.async_load() devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict() + groups: "OrderedDict[str, ZhaGroupEntry]" = OrderedDict() if data is not None: for device in data["devices"]: @@ -97,7 +157,18 @@ class ZhaDeviceStorage: last_seen=device["last_seen"] if "last_seen" in device else None, ) + if "groups" in data: + for group in data["groups"]: + groups[group["group_id"]] = ZhaGroupEntry( + name=group["name"], + group_id=group["group_id"], + entity_domain=group["entity_domain"] + if "entity_domain" in group + else None, + ) + self.devices = devices + self.groups = groups @callback def async_schedule_save(self) -> None: @@ -118,21 +189,29 @@ class ZhaDeviceStorage: for entry in self.devices.values() ] + data["groups"] = [ + { + "name": entry.name, + "group_id": entry.group_id, + "entity_domain": entry.entity_domain, + } + for entry in self.groups.values() + ] return data @bind_hass -async def async_get_registry(hass: HomeAssistantType) -> ZhaDeviceStorage: +async def async_get_registry(hass: HomeAssistantType) -> ZhaStorage: """Return zha device storage instance.""" task = hass.data.get(DATA_REGISTRY) if task is None: - async def _load_reg() -> ZhaDeviceStorage: - registry = ZhaDeviceStorage(hass) + async def _load_reg() -> ZhaStorage: + registry = ZhaStorage(hass) await registry.async_load() return registry task = hass.data[DATA_REGISTRY] = hass.async_create_task(_load_reg()) - return cast(ZhaDeviceStorage, await task) + return cast(ZhaStorage, await task) diff --git a/homeassistant/components/zha/core/typing.py b/homeassistant/components/zha/core/typing.py index a1cbc9f0fef..a4619d0596e 100644 --- a/homeassistant/components/zha/core/typing.py +++ b/homeassistant/components/zha/core/typing.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Callable, TypeVar import zigpy.device import zigpy.endpoint +import zigpy.group import zigpy.zcl import zigpy.zdo @@ -17,9 +18,11 @@ ZDOChannelType = "ZDOChannel" ZhaDeviceType = "ZHADevice" ZhaEntityType = "ZHAEntity" ZhaGatewayType = "ZHAGateway" +ZhaGroupType = "ZHAGroupType" ZigpyClusterType = zigpy.zcl.Cluster ZigpyDeviceType = zigpy.device.Device ZigpyEndpointType = zigpy.endpoint.Endpoint +ZigpyGroupType = zigpy.group.Group ZigpyZdoType = zigpy.zdo.ZDO if TYPE_CHECKING: @@ -38,3 +41,4 @@ if TYPE_CHECKING: ZhaDeviceType = homeassistant.components.zha.core.device.ZHADevice ZhaEntityType = homeassistant.components.zha.entity.ZhaEntity ZhaGatewayType = homeassistant.components.zha.core.gateway.ZHAGateway + ZhaGroupType = homeassistant.components.zha.core.group.ZHAGroup diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 4dd3fea016d..63ed3a6edc7 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -3,6 +3,7 @@ import asyncio import logging import time +from typing import Any, Awaitable, Dict, List from homeassistant.core import callback from homeassistant.helpers import entity @@ -20,6 +21,7 @@ from .core.const import ( SIGNAL_REMOVE, ) from .core.helpers import LogMixin +from .core.typing import CALLABLE_T, ChannelsType, ChannelType, ZhaDeviceType _LOGGER = logging.getLogger(__name__) @@ -27,30 +29,24 @@ ENTITY_SUFFIX = "entity_suffix" RESTART_GRACE_PERIOD = 7200 # 2 hours -class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): +class BaseZhaEntity(RestoreEntity, LogMixin, entity.Entity): """A base class for ZHA entities.""" - def __init__(self, unique_id, zha_device, channels, skip_entity_id=False, **kwargs): + def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs): """Init ZHA entity.""" - self._force_update = False - self._should_poll = False - self._unique_id = unique_id - ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) - ch_names = [ch.cluster.ep_attribute for ch in channels] - ch_names = ", ".join(sorted(ch_names)) - self._name = f"{zha_device.name} {ieeetail} {ch_names}" - self._state = None - self._device_state_attributes = {} - self._zha_device = zha_device - self.cluster_channels = {} - self._available = False - self._unsubs = [] - self.remove_future = None - for channel in channels: - self.cluster_channels[channel.name] = channel + self._name: str = "" + self._force_update: bool = False + self._should_poll: bool = False + self._unique_id: str = unique_id + self._state: Any = None + self._device_state_attributes: Dict[str, Any] = {} + self._zha_device: ZhaDeviceType = zha_device + self._available: bool = False + self._unsubs: List[CALLABLE_T] = [] + self.remove_future: Awaitable[None] = None @property - def name(self): + def name(self) -> str: """Return Entity's default name.""" return self._name @@ -60,12 +56,12 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): return self._unique_id @property - def zha_device(self): + def zha_device(self) -> ZhaDeviceType: """Return the zha device this entity is attached to.""" return self._zha_device @property - def device_state_attributes(self): + def device_state_attributes(self) -> Dict[str, Any]: """Return device specific state attributes.""" return self._device_state_attributes @@ -80,7 +76,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): return self._should_poll @property - def device_info(self): + def device_info(self) -> Dict[str, Any]: """Return a device description for device registry.""" zha_device_info = self._zha_device.device_info ieee = zha_device_info["ieee"] @@ -94,31 +90,94 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): } @property - def available(self): + def available(self) -> bool: """Return entity availability.""" return self._available @callback - def async_set_available(self, available): + def async_set_available(self, available: bool) -> None: """Set entity availability.""" self._available = available self.async_write_ha_state() @callback - def async_update_state_attribute(self, key, value): + def async_update_state_attribute(self, key: str, value: Any) -> None: """Update a single device state attribute.""" self._device_state_attributes.update({key: value}) self.async_write_ha_state() @callback - def async_set_state(self, attr_id, attr_name, value): + def async_set_state(self, attr_id: int, attr_name: str, value: Any) -> None: """Set the entity state.""" pass - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() self.remove_future = asyncio.Future() + await self.async_accept_signal( + None, + "{}_{}".format(SIGNAL_REMOVE, str(self.zha_device.ieee)), + self.async_remove, + signal_override=True, + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect entity object when removed.""" + for unsub in self._unsubs[:]: + unsub() + self._unsubs.remove(unsub) + self.zha_device.gateway.remove_entity_reference(self) + self.remove_future.set_result(True) + + @callback + def async_restore_last_state(self, last_state) -> None: + """Restore previous state.""" + pass + + async def async_accept_signal( + self, channel: ChannelType, signal: str, func: CALLABLE_T, signal_override=False + ): + """Accept a signal from a channel.""" + unsub = None + if signal_override: + unsub = async_dispatcher_connect(self.hass, signal, func) + else: + unsub = async_dispatcher_connect( + self.hass, f"{channel.unique_id}_{signal}", func + ) + self._unsubs.append(unsub) + + def log(self, level: int, msg: str, *args): + """Log a message.""" + msg = f"%s: {msg}" + args = (self.entity_id,) + args + _LOGGER.log(level, msg, *args) + + +class ZhaEntity(BaseZhaEntity): + """A base class for non group ZHA entities.""" + + def __init__( + self, + unique_id: str, + zha_device: ZhaDeviceType, + channels: ChannelsType, + **kwargs, + ): + """Init ZHA entity.""" + super().__init__(unique_id, zha_device, **kwargs) + ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) + ch_names = [ch.cluster.ep_attribute for ch in channels] + ch_names = ", ".join(sorted(ch_names)) + self._name: str = f"{zha_device.name} {ieeetail} {ch_names}" + self.cluster_channels: Dict[str, ChannelType] = {} + for channel in channels: + self.cluster_channels[channel.name] = channel + + async def async_added_to_hass(self) -> None: + """Run when about to be added to hass.""" + await super().async_added_to_hass() await self.async_check_recently_seen() await self.async_accept_signal( None, @@ -126,12 +185,6 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self.async_set_available, signal_override=True, ) - await self.async_accept_signal( - None, - "{}_{}".format(SIGNAL_REMOVE, str(self.zha_device.ieee)), - self.async_remove, - signal_override=True, - ) self._zha_device.gateway.register_entity_reference( self._zha_device.ieee, self.entity_id, @@ -141,7 +194,7 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self.remove_future, ) - async def async_check_recently_seen(self): + async def async_check_recently_seen(self) -> None: """Check if the device was seen within the last 2 hours.""" last_state = await self.async_get_last_state() if ( @@ -155,38 +208,8 @@ class ZhaEntity(RestoreEntity, LogMixin, entity.Entity): self.async_restore_last_state(last_state) self._zha_device.set_available(True) - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - for unsub in self._unsubs[:]: - unsub() - self._unsubs.remove(unsub) - self.zha_device.gateway.remove_entity_reference(self) - self.remove_future.set_result(True) - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - pass - - async def async_update(self): + async def async_update(self) -> None: """Retrieve latest state.""" for channel in self.cluster_channels.values(): if hasattr(channel, "async_update"): await channel.async_update() - - async def async_accept_signal(self, channel, signal, func, signal_override=False): - """Accept a signal from a channel.""" - unsub = None - if signal_override: - unsub = async_dispatcher_connect(self.hass, signal, func) - else: - unsub = async_dispatcher_connect( - self.hass, f"{channel.unique_id}_{signal}", func - ) - self._unsubs.append(unsub) - - def log(self, level, msg, *args): - """Log a message.""" - msg = f"%s: {msg}" - args = (self.entity_id,) + args - _LOGGER.log(level, msg, *args) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index bf3a457ff68..2192ec1a909 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -1,19 +1,44 @@ """Lights on Zigbee Home Automation networks.""" +from collections import Counter from datetime import timedelta import functools +import itertools import logging import random +from typing import Any, Dict, List, Optional, Tuple +from zigpy.zcl.clusters.general import Identify, LevelControl, OnOff +from zigpy.zcl.clusters.lighting import Color from zigpy.zcl.foundation import Status from homeassistant.components import light -from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.components.light import ( + ATTR_BRIGHTNESS, + ATTR_COLOR_TEMP, + ATTR_EFFECT, + ATTR_EFFECT_LIST, + ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_WHITE_VALUE, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, + SUPPORT_EFFECT, + SUPPORT_FLASH, + SUPPORT_TRANSITION, + SUPPORT_WHITE_VALUE, +) +from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_time_interval +from homeassistant.helpers.event import ( + async_track_state_change, + async_track_time_interval, +) import homeassistant.util.color as color_util -from .core import discovery +from .core import discovery, helpers from .core.const import ( CHANNEL_COLOR, CHANNEL_LEVEL, @@ -25,11 +50,12 @@ from .core.const import ( EFFECT_DEFAULT_VARIANT, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, + SIGNAL_REMOVE_GROUP, SIGNAL_SET_LEVEL, ) from .core.registries import ZHA_ENTITIES from .core.typing import ZhaDeviceType -from .entity import ZhaEntity +from .entity import BaseZhaEntity, ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -46,8 +72,19 @@ FLASH_EFFECTS = {light.FLASH_SHORT: EFFECT_BLINK, light.FLASH_LONG: EFFECT_BREAT UNSUPPORTED_ATTRIBUTE = 0x86 STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, light.DOMAIN) +GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, light.DOMAIN) PARALLEL_UPDATES = 0 +SUPPORT_GROUP_LIGHT = ( + SUPPORT_BRIGHTNESS + | SUPPORT_COLOR_TEMP + | SUPPORT_EFFECT + | SUPPORT_FLASH + | SUPPORT_COLOR + | SUPPORT_TRANSITION + | SUPPORT_WHITE_VALUE +) + async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Zigbee Home Automation light from config entry.""" @@ -63,48 +100,35 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -@STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) -class Light(ZhaEntity, light.Light): - """Representation of a ZHA or ZLL light.""" +class BaseLight(BaseZhaEntity, light.Light): + """Operations common to all light entities.""" - _REFRESH_INTERVAL = (45, 75) + def __init__(self, *args, **kwargs): + """Initialize the light.""" + super().__init__(*args, **kwargs) + self._is_on: bool = False + self._available: bool = False + self._brightness: Optional[int] = None + self._off_brightness: Optional[int] = None + self._hs_color: Optional[Tuple[float, float]] = None + self._color_temp: Optional[int] = None + self._min_mireds: Optional[int] = 154 + self._max_mireds: Optional[int] = 500 + self._white_value: Optional[int] = None + self._effect_list: Optional[List[str]] = None + self._effect: Optional[str] = None + self._supported_features: int = 0 + self._state: bool = False + self._on_off_channel = None + self._level_channel = None + self._color_channel = None + self._identify_channel = None - def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): - """Initialize the ZHA light.""" - super().__init__(unique_id, zha_device, channels, **kwargs) - self._supported_features = 0 - self._color_temp = None - self._hs_color = None - self._brightness = None - self._off_brightness = None - self._effect_list = [] - self._effect = None - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) - self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) - self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) - self._identify_channel = self.zha_device.channels.identify_ch - self._cancel_refresh_handle = None - - if self._level_channel: - self._supported_features |= light.SUPPORT_BRIGHTNESS - self._supported_features |= light.SUPPORT_TRANSITION - self._brightness = 0 - - if self._color_channel: - color_capabilities = self._color_channel.get_color_capabilities() - if color_capabilities & CAPABILITIES_COLOR_TEMP: - self._supported_features |= light.SUPPORT_COLOR_TEMP - - if color_capabilities & CAPABILITIES_COLOR_XY: - self._supported_features |= light.SUPPORT_COLOR - self._hs_color = (0, 0) - - if color_capabilities & CAPABILITIES_COLOR_LOOP: - self._supported_features |= light.SUPPORT_EFFECT - self._effect_list.append(light.EFFECT_COLORLOOP) - - if self._identify_channel: - self._supported_features |= light.SUPPORT_FLASH + @property + def device_state_attributes(self) -> Dict[str, Any]: + """Return state attributes.""" + attributes = {"off_brightness": self._off_brightness} + return attributes @property def is_on(self) -> bool: @@ -118,12 +142,6 @@ class Light(ZhaEntity, light.Light): """Return the brightness of this light.""" return self._brightness - @property - def device_state_attributes(self): - """Return state attributes.""" - attributes = {"off_brightness": self._off_brightness} - return attributes - def set_level(self, value): """Set the brightness of this light between 0..254. @@ -160,49 +178,6 @@ class Light(ZhaEntity, light.Light): """Flag supported features.""" return self._supported_features - @callback - def async_set_state(self, attr_id, attr_name, value): - """Set the state.""" - self._state = bool(value) - if value: - self._off_brightness = None - self.async_write_ha_state() - - async def async_added_to_hass(self): - """Run when about to be added to hass.""" - await super().async_added_to_hass() - await self.async_accept_signal( - self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - if self._level_channel: - await self.async_accept_signal( - self._level_channel, SIGNAL_SET_LEVEL, self.set_level - ) - refresh_interval = random.randint(*[x * 60 for x in self._REFRESH_INTERVAL]) - self._cancel_refresh_handle = async_track_time_interval( - self.hass, self._refresh, timedelta(seconds=refresh_interval) - ) - - async def async_will_remove_from_hass(self) -> None: - """Disconnect entity object when removed.""" - self._cancel_refresh_handle() - await super().async_will_remove_from_hass() - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._state = last_state.state == STATE_ON - if "brightness" in last_state.attributes: - self._brightness = last_state.attributes["brightness"] - if "off_brightness" in last_state.attributes: - self._off_brightness = last_state.attributes["off_brightness"] - if "color_temp" in last_state.attributes: - self._color_temp = last_state.attributes["color_temp"] - if "hs_color" in last_state.attributes: - self._hs_color = last_state.attributes["hs_color"] - if "effect" in last_state.attributes: - self._effect = last_state.attributes["effect"] - async def async_turn_on(self, **kwargs): """Turn the entity on.""" transition = kwargs.get(light.ATTR_TRANSITION) @@ -331,6 +306,86 @@ class Light(ZhaEntity, light.Light): self.async_write_ha_state() + +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) +class Light(ZhaEntity, BaseLight): + """Representation of a ZHA or ZLL light.""" + + _REFRESH_INTERVAL = (45, 75) + + def __init__(self, unique_id, zha_device: ZhaDeviceType, channels, **kwargs): + """Initialize the ZHA light.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._level_channel = self.cluster_channels.get(CHANNEL_LEVEL) + self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) + self._identify_channel = self.zha_device.channels.identify_ch + self._cancel_refresh_handle = None + + if self._level_channel: + self._supported_features |= light.SUPPORT_BRIGHTNESS + self._supported_features |= light.SUPPORT_TRANSITION + self._brightness = 0 + + if self._color_channel: + color_capabilities = self._color_channel.get_color_capabilities() + if color_capabilities & CAPABILITIES_COLOR_TEMP: + self._supported_features |= light.SUPPORT_COLOR_TEMP + + if color_capabilities & CAPABILITIES_COLOR_XY: + self._supported_features |= light.SUPPORT_COLOR + self._hs_color = (0, 0) + + if color_capabilities & CAPABILITIES_COLOR_LOOP: + self._supported_features |= light.SUPPORT_EFFECT + self._effect_list.append(light.EFFECT_COLORLOOP) + + if self._identify_channel: + self._supported_features |= light.SUPPORT_FLASH + + @callback + def async_set_state(self, attr_id, attr_name, value): + """Set the state.""" + self._state = bool(value) + if value: + self._off_brightness = None + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._on_off_channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + if self._level_channel: + await self.async_accept_signal( + self._level_channel, SIGNAL_SET_LEVEL, self.set_level + ) + refresh_interval = random.randint(*[x * 60 for x in self._REFRESH_INTERVAL]) + self._cancel_refresh_handle = async_track_time_interval( + self.hass, self._refresh, timedelta(seconds=refresh_interval) + ) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect entity object when removed.""" + self._cancel_refresh_handle() + await super().async_will_remove_from_hass() + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._state = last_state.state == STATE_ON + if "brightness" in last_state.attributes: + self._brightness = last_state.attributes["brightness"] + if "off_brightness" in last_state.attributes: + self._off_brightness = last_state.attributes["off_brightness"] + if "color_temp" in last_state.attributes: + self._color_temp = last_state.attributes["color_temp"] + if "hs_color" in last_state.attributes: + self._hs_color = last_state.attributes["hs_color"] + if "effect" in last_state.attributes: + self._effect = last_state.attributes["effect"] + async def async_update(self): """Attempt to retrieve on off state from the light.""" await super().async_update() @@ -410,3 +465,99 @@ class HueLight(Light): """Representation of a HUE light which does not report attributes.""" _REFRESH_INTERVAL = (3, 5) + + +@GROUP_MATCH() +class LightGroup(BaseLight): + """Representation of a light group.""" + + def __init__( + self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + ) -> None: + """Initialize a light group.""" + super().__init__(unique_id, zha_device, **kwargs) + self._name = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" + self._group_id: int = group_id + self._entity_ids: List[str] = entity_ids + group = self.zha_device.gateway.get_group(self._group_id) + self._on_off_channel = group.endpoint[OnOff.cluster_id] + self._level_channel = group.endpoint[LevelControl.cluster_id] + self._color_channel = group.endpoint[Color.cluster_id] + self._identify_channel = group.endpoint[Identify.cluster_id] + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + await self.async_accept_signal( + None, + f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", + self.async_remove, + signal_override=True, + ) + + @callback + def async_state_changed_listener( + entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener + ) + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Handle removal from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._async_unsub_state_changed is not None: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + async def async_update(self) -> None: + """Query all members and determine the light group state.""" + all_states = [self.hass.states.get(x) for x in self._entity_ids] + states: List[State] = list(filter(None, all_states)) + on_states = [state for state in states if state.state == STATE_ON] + + self._is_on = len(on_states) > 0 + self._available = any(state.state != STATE_UNAVAILABLE for state in states) + + self._brightness = helpers.reduce_attribute(on_states, ATTR_BRIGHTNESS) + + self._hs_color = helpers.reduce_attribute( + on_states, ATTR_HS_COLOR, reduce=helpers.mean_tuple + ) + + self._white_value = helpers.reduce_attribute(on_states, ATTR_WHITE_VALUE) + + self._color_temp = helpers.reduce_attribute(on_states, ATTR_COLOR_TEMP) + self._min_mireds = helpers.reduce_attribute( + states, ATTR_MIN_MIREDS, default=154, reduce=min + ) + self._max_mireds = helpers.reduce_attribute( + states, ATTR_MAX_MIREDS, default=500, reduce=max + ) + + self._effect_list = None + all_effect_lists = list(helpers.find_state_attributes(states, ATTR_EFFECT_LIST)) + if all_effect_lists: + # Merge all effects from all effect_lists with a union merge. + self._effect_list = list(set().union(*all_effect_lists)) + + self._effect = None + all_effects = list(helpers.find_state_attributes(on_states, ATTR_EFFECT)) + if all_effects: + # Report the most common effect. + effects_count = Counter(itertools.chain(all_effects)) + self._effect = effects_count.most_common(1)[0][0] + + self._supported_features = 0 + for support in helpers.find_state_attributes(states, ATTR_SUPPORTED_FEATURES): + # Merge supported features by emulating support for every feature + # we find. + self._supported_features |= support + # Bitwise-and the supported features with the GroupedLight's features + # so that we don't break in the future when a new feature is added. + self._supported_features &= SUPPORT_GROUP_LIGHT diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 3753136d59d..9c57b57419a 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -3,6 +3,8 @@ import time from unittest.mock import Mock from asynctest import CoroutineMock +from zigpy.device import Device as zigpy_dev +from zigpy.endpoint import Endpoint as zigpy_ep import zigpy.profiles.zha import zigpy.types import zigpy.zcl @@ -24,6 +26,7 @@ class FakeEndpoint: self.in_clusters = {} self.out_clusters = {} self._cluster_attr = {} + self.member_of = {} self.status = 1 self.manufacturer = manufacturer self.model = model @@ -45,6 +48,19 @@ class FakeEndpoint: patch_cluster(cluster) self.out_clusters[cluster_id] = cluster + @property + def __class__(self): + """Fake being Zigpy endpoint.""" + return zigpy_ep + + @property + def unique_id(self): + """Return the unique id for the endpoint.""" + return self.device.ieee, self.endpoint_id + + +FakeEndpoint.add_to_group = zigpy_ep.add_to_group + def patch_cluster(cluster): """Patch a cluster for testing.""" @@ -56,17 +72,19 @@ def patch_cluster(cluster): cluster.read_attributes_raw = Mock() cluster.unbind = CoroutineMock(return_value=[0]) cluster.write_attributes = CoroutineMock(return_value=[0]) + if cluster.cluster_id == 4: + cluster.add = CoroutineMock(return_value=[0]) class FakeDevice: """Fake device for mocking zigpy.""" - def __init__(self, app, ieee, manufacturer, model, node_desc=None): + def __init__(self, app, ieee, manufacturer, model, node_desc=None, nwk=0xB79C): """Init fake device.""" self._application = app self.application = app self.ieee = zigpy.types.EUI64.convert(ieee) - self.nwk = 0xB79C + self.nwk = nwk self.zdo = Mock() self.endpoints = {0: self.zdo} self.lqi = 255 @@ -78,13 +96,15 @@ class FakeDevice: self.manufacturer = manufacturer self.model = model self.node_desc = zigpy.zdo.types.NodeDescriptor() - self.add_to_group = CoroutineMock() self.remove_from_group = CoroutineMock() if node_desc is None: node_desc = b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00" self.node_desc = zigpy.zdo.types.NodeDescriptor.deserialize(node_desc)[0] +FakeDevice.add_to_group = zigpy_dev.add_to_group + + def get_zha_gateway(hass): """Return ZHA gateway from hass.data.""" try: @@ -137,6 +157,19 @@ async def find_entity_id(domain, zha_device, hass): return None +def async_find_group_entity_id(hass, domain, group): + """Find the group entity id under test.""" + entity_id = ( + f"{domain}.{group.name.lower().replace(' ','_')}_group_0x{group.group_id:04x}" + ) + + entity_ids = hass.states.async_entity_ids(domain) + + if entity_id in entity_ids: + return entity_id + return None + + async def async_enable_traffic(hass, zha_devices): """Allow traffic to flow through the gateway and the zha device.""" for zha_device in zha_devices: diff --git a/tests/components/zha/conftest.py b/tests/components/zha/conftest.py index e6056428db6..b83db53533c 100644 --- a/tests/components/zha/conftest.py +++ b/tests/components/zha/conftest.py @@ -110,10 +110,11 @@ def zigpy_device_mock(zigpy_app_controller): manufacturer="FakeManufacturer", model="FakeModel", node_descriptor=b"\x02@\x807\x10\x7fd\x00\x00*d\x00\x00", + nwk=0xB79C, ): """Make a fake device using the specified cluster classes.""" device = FakeDevice( - zigpy_app_controller, ieee, manufacturer, model, node_descriptor + zigpy_app_controller, ieee, manufacturer, model, node_descriptor, nwk=nwk ) for epid, ep in endpoints.items(): endpoint = FakeEndpoint(manufacturer, model, epid) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 74aed6f5872..3bb98522814 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,8 +1,18 @@ """Test ZHA Gateway.""" -import pytest -import zigpy.zcl.clusters.general as general +import logging -from .common import async_enable_traffic, get_zha_gateway +import pytest +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.general as general +import zigpy.zcl.clusters.lighting as lighting + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN + +from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway + +IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" +_LOGGER = logging.getLogger(__name__) @pytest.fixture @@ -15,7 +25,7 @@ def zigpy_dev_basic(zigpy_device_mock): "out_clusters": [], "device_type": 0, } - }, + } ) @@ -27,6 +37,74 @@ async def zha_dev_basic(hass, zha_device_restored, zigpy_dev_basic): return zha_device +@pytest.fixture +async def coordinator(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee="00:15:8d:00:02:32:4f:32", + nwk=0x0000, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_light_1(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + ], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_light_2(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + ], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE2, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + async def test_device_left(hass, zigpy_dev_basic, zha_dev_basic): """Device leaving the network should become unavailable.""" @@ -37,3 +115,57 @@ async def test_device_left(hass, zigpy_dev_basic, zha_dev_basic): get_zha_gateway(hass).device_left(zigpy_dev_basic) assert zha_dev_basic.available is False + + +async def test_gateway_group_methods(hass, device_light_1, device_light_2, coordinator): + """Test creating a group with 2 members.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + device_light_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group( + "Test Group", member_ieee_addresses + ) + await hass.async_block_till_done() + + assert zha_group is not None + assert zha_group.entity_domain == LIGHT_DOMAIN + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.ieee in member_ieee_addresses + + entity_id = async_find_group_entity_id(hass, LIGHT_DOMAIN, zha_group) + assert hass.states.get(entity_id) is not None + + # test get group by name + assert zha_group == zha_gateway.async_get_group_by_name(zha_group.name) + + # test removing a group + await zha_gateway.async_remove_zigpy_group(zha_group.group_id) + await hass.async_block_till_done() + + # we shouldn't have the group anymore + assert zha_gateway.async_get_group_by_name(zha_group.name) is None + + # the group entity should be cleaned up + assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + + # test creating a group with 1 member + zha_group = await zha_gateway.async_create_zigpy_group( + "Test Group", [device_light_1.ieee] + ) + await hass.async_block_till_done() + + assert zha_group is not None + assert zha_group.entity_domain is None + assert len(zha_group.members) == 1 + for member in zha_group.members: + assert member.ieee in [device_light_1.ieee] + + # the group entity should not have been cleaned up + assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f27bd329bdb..c6bafa45aea 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, call, sentinel from asynctest import CoroutineMock, patch import pytest -import zigpy.profiles.zha +import zigpy.profiles.zha as zha import zigpy.types import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.lighting as lighting @@ -17,8 +17,10 @@ import homeassistant.util.dt as dt_util from .common import ( async_enable_traffic, + async_find_group_entity_id, async_test_rejoin, find_entity_id, + get_zha_gateway, send_attributes_report, ) @@ -26,6 +28,8 @@ from tests.common import async_fire_time_changed ON = 1 OFF = 0 +IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" LIGHT_ON_OFF = { 1: { @@ -66,6 +70,76 @@ LIGHT_COLOR = { } +@pytest.fixture +async def coordinator(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee="00:15:8d:00:02:32:4f:32", + nwk=0x0000, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_light_1(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_light_2(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE2, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + @patch("zigpy.zcl.clusters.general.OnOff.read_attributes", new=MagicMock()) async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): """Test zha light platform refresh.""" @@ -337,3 +411,96 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): manufacturer=None, tsn=None, ) + + +async def async_test_zha_group_light_entity( + hass, device_light_1, device_light_2, coordinator +): + """Test the light entity for a ZHA group.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_light_1._zha_gateway = zha_gateway + device_light_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_light_1.ieee, device_light_2.ieee] + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group( + "Test Group", member_ieee_addresses + ) + await hass.async_block_till_done() + + assert zha_group is not None + assert zha_group.entity_domain == DOMAIN + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.ieee in member_ieee_addresses + + entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + assert hass.states.get(entity_id) is not None + + group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] + group_cluster_level = zha_group.endpoint[general.LevelControl.cluster_id] + group_cluster_identify = zha_group.endpoint[general.Identify.cluster_id] + + dev1_cluster_on_off = device_light_1.endpoints[1].on_off + dev2_cluster_on_off = device_light_2.endpoints[1].on_off + + # test that the lights were created and that they are 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_group.members) + + # test that the lights were created and are off + assert hass.states.get(entity_id).state == STATE_OFF + + # test turning the lights on and off from the light + await async_test_on_off_from_light(hass, group_cluster_on_off, entity_id) + + # test turning the lights on and off from the HA + await async_test_on_off_from_hass(hass, group_cluster_on_off, entity_id) + + # test short flashing the lights from the HA + await async_test_flash_from_hass( + hass, group_cluster_identify, entity_id, FLASH_SHORT + ) + + # test turning the lights on and off from the HA + await async_test_level_on_off_from_hass( + hass, group_cluster_on_off, group_cluster_level, entity_id + ) + + # test getting a brightness change from the network + await async_test_on_from_light(hass, group_cluster_on_off, entity_id) + await async_test_dimmer_from_light( + hass, group_cluster_level, entity_id, 150, STATE_ON + ) + + # test long flashing the lights from the HA + await async_test_flash_from_hass( + hass, group_cluster_identify, entity_id, FLASH_LONG + ) + + # test some of the group logic to make sure we key off states correctly + await dev1_cluster_on_off.on() + await dev2_cluster_on_off.on() + + # test that group light is on + assert hass.states.get(entity_id).state == STATE_ON + + await dev1_cluster_on_off.off() + + # test that group light is still on + assert hass.states.get(entity_id).state == STATE_ON + + await dev2_cluster_on_off.off() + + # test that group light is now off + assert hass.states.get(entity_id).state == STATE_OFF + + await dev1_cluster_on_off.on() + + # test that group light is now back on + assert hass.states.get(entity_id).state == STATE_ON From 1fa996ed686a399a299f1e06e893a560e1fcb900 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 25 Mar 2020 13:32:28 +0100 Subject: [PATCH 246/431] Fix ONVIF camera snapshot with auth (#33241) --- homeassistant/components/onvif/camera.py | 49 +++++++++++++----------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/onvif/camera.py b/homeassistant/components/onvif/camera.py index c9f592cdba4..0c6a3bffa1b 100644 --- a/homeassistant/components/onvif/camera.py +++ b/homeassistant/components/onvif/camera.py @@ -5,13 +5,13 @@ import logging import os from typing import Optional -from aiohttp import ClientError from aiohttp.client_exceptions import ClientConnectionError, ServerDisconnectedError -import async_timeout from haffmpeg.camera import CameraMjpeg from haffmpeg.tools import IMAGE_JPEG, ImageFrame import onvif from onvif import ONVIFCamera, exceptions +import requests +from requests.auth import HTTPDigestAuth import voluptuous as vol from zeep.asyncio import AsyncTransport from zeep.exceptions import Fault @@ -412,17 +412,12 @@ class ONVIFHassCamera(Camera): req.ProfileToken = profiles[self._profile_index].token snapshot_uri = await media_service.GetSnapshotUri(req) - uri_no_auth = snapshot_uri.Uri - uri_for_log = uri_no_auth.replace("http://", "http://:@", 1) - # Same authentication as rtsp - self._snapshot = uri_no_auth.replace( - "http://", f"http://{self._username}:{self._password}@", 1 - ) + self._snapshot = snapshot_uri.Uri _LOGGER.debug( "ONVIF Camera Using the following URL for %s snapshot: %s", self._name, - uri_for_log, + self._snapshot, ) except exceptions.ONVIFError as err: _LOGGER.error("Couldn't setup camera '%s'. Error: %s", self._name, err) @@ -509,25 +504,33 @@ class ONVIFHassCamera(Camera): async def async_camera_image(self): """Return a still image response from the camera.""" - _LOGGER.debug("Retrieving image from camera '%s'", self._name) + image = None if self._snapshot is not None: - try: - websession = async_get_clientsession(self.hass) - with async_timeout.timeout(10): - response = await websession.get(self._snapshot) - image = await response.read() - except asyncio.TimeoutError: - _LOGGER.error("Timeout getting image from: %s", self._name) - image = None - except ClientError as err: - _LOGGER.error("Error getting new camera image: %s", err) - image = None + auth = None + if self._username and self._password: + auth = HTTPDigestAuth(self._username, self._password) + + def fetch(): + """Read image from a URL.""" + try: + response = requests.get(self._snapshot, timeout=5, auth=auth) + return response.content + except requests.exceptions.RequestException as error: + _LOGGER.error( + "Fetch snapshot image failed from %s, falling back to FFmpeg; %s", + self._name, + error, + ) + + image = await self.hass.async_add_job(fetch) + + if image is None: + # Don't keep trying the snapshot URL + self._snapshot = None - if self._snapshot is None or image is None: ffmpeg = ImageFrame(self.hass.data[DATA_FFMPEG].binary, loop=self.hass.loop) - image = await asyncio.shield( ffmpeg.get_image( self._input, From bb3592baa085e7a9fddec33eed302d8085bf42db Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 25 Mar 2020 15:19:34 +0100 Subject: [PATCH 247/431] [skip ci] Update azure-pipelines-translation.yml for Azure Pipelines --- azure-pipelines-translation.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-translation.yml b/azure-pipelines-translation.yml index 2fd49c056f7..0791cbcaab1 100644 --- a/azure-pipelines-translation.yml +++ b/azure-pipelines-translation.yml @@ -7,7 +7,7 @@ trigger: - dev pr: none schedules: - - cron: "30 0 * * *" + - cron: "0 0 * * *" displayName: "translation update" branches: include: From bb68e7a532b7f38bb19964ab5212b0e219e98390 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Wed, 25 Mar 2020 17:11:00 +0100 Subject: [PATCH 248/431] Bump aiohue version to 2.1.0 (#33234) --- 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 5471632f9c5..a5c801f4124 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.0.0"], + "requirements": ["aiohue==2.1.0"], "ssdp": [ { "manufacturer": "Royal Philips Electronics", diff --git a/requirements_all.txt b/requirements_all.txt index 5e0c9f762aa..14eed6a8541 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -175,7 +175,7 @@ aiohomekit[IP]==0.2.35 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.0.0 +aiohue==2.1.0 # homeassistant.components.imap aioimaplib==0.7.15 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 48441639880..f82eb0f7cd4 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -79,7 +79,7 @@ aiohomekit[IP]==0.2.35 aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.0.0 +aiohue==2.1.0 # homeassistant.components.notion aionotion==1.1.0 From 6990c70123417de46859e33199113d712b45311b Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 25 Mar 2020 17:19:10 +0100 Subject: [PATCH 249/431] Fix issue with smhi-pkg (#33248) * Fix issue with smhi-pkg * Update azure-pipelines-wheels.yml --- azure-pipelines-ci.yml | 4 ++-- azure-pipelines-wheels.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 8fb014f80a7..745330c630a 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -127,7 +127,7 @@ stages: . venv/bin/activate pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt - pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt + pip install --no-binary smhi-pkg -r requirements_test_all.txt -c homeassistant/package_constraints.txt # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing @@ -171,7 +171,7 @@ stages: . venv/bin/activate pip install -U pip setuptools wheel - pip install -r requirements_all.txt -c homeassistant/package_constraints.txt + pip install --no-binary smhi-pkg -r requirements_all.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index b4ad0a556b2..77ca2a7a866 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -33,7 +33,7 @@ jobs: builderVersion: '$(versionWheels)' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy' - skipBinary: 'aiohttp' + skipBinary: 'aiohttp,smhi-pkg' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' wheelsConstraint: 'homeassistant/package_constraints.txt' From 4a0a56ebdc68571f9bf202df2f104df96af5f490 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 25 Mar 2020 17:22:34 +0100 Subject: [PATCH 250/431] Upgrade hole to 0.5.1 (#33249) --- homeassistant/components/pi_hole/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/pi_hole/manifest.json b/homeassistant/components/pi_hole/manifest.json index 2f93929d8aa..5d8f8557099 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.0"], + "requirements": ["hole==0.5.1"], "dependencies": [], "codeowners": ["@fabaff", "@johnluetke"] } diff --git a/requirements_all.txt b/requirements_all.txt index 14eed6a8541..75027277f24 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -698,7 +698,7 @@ hkavr==0.0.5 hlk-sw16==0.0.8 # homeassistant.components.pi_hole -hole==0.5.0 +hole==0.5.1 # homeassistant.components.workday holidays==0.10.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f82eb0f7cd4..a4f6a0e1add 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -273,7 +273,7 @@ hdate==0.9.5 herepy==2.0.0 # homeassistant.components.pi_hole -hole==0.5.0 +hole==0.5.1 # homeassistant.components.workday holidays==0.10.1 From d5f4dfdd6bea6e804af18fb9155bd3d1a9272ad9 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Wed, 25 Mar 2020 12:35:36 -0400 Subject: [PATCH 251/431] =?UTF-8?q?Update=20vizio=20app=5Fid=20state=20att?= =?UTF-8?q?ribute=20to=20show=20app=20config=20dict=20in=E2=80=A6=20(#3272?= =?UTF-8?q?7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * bump pyvizio and update app_id to show app config to aid in HA config generation. squashed from multiple commits to make a rebase on dev easier * bump pyvizio for bug fixes * fix pyvizio version number * only return app_id if app is unknown and explicitly create the dict that's returned * fix tests * fix docstring for app_id --- homeassistant/components/vizio/manifest.json | 2 +- .../components/vizio/media_player.py | 44 +++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/vizio/conftest.py | 6 +- tests/components/vizio/const.py | 6 ++ tests/components/vizio/test_media_player.py | 86 ++++++++++++++----- 7 files changed, 101 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index f1931f6fdb1..7608b6eae53 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -2,7 +2,7 @@ "domain": "vizio", "name": "Vizio SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", - "requirements": ["pyvizio==0.1.35"], + "requirements": ["pyvizio==0.1.44"], "dependencies": [], "codeowners": ["@raman325"], "config_flow": true, diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index d97a82ca144..d463ebca36a 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -4,8 +4,8 @@ import logging from typing import Any, Callable, Dict, List, Optional from pyvizio import VizioAsync -from pyvizio.const import INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP -from pyvizio.helpers import find_app_name +from pyvizio.api.apps import find_app_name +from pyvizio.const import APP_HOME, APPS, INPUT_APPS, NO_APP_RUNNING, UNKNOWN_APP from homeassistant.components.media_player import ( DEVICE_CLASS_SPEAKER, @@ -132,6 +132,7 @@ class VizioDevice(MediaPlayerDevice): self._is_muted = None self._current_input = None self._current_app = None + self._current_app_config = None self._available_inputs = [] self._available_apps = [] self._conf_apps = config_entry.options.get(CONF_APPS, {}) @@ -157,20 +158,6 @@ class VizioDevice(MediaPlayerDevice): return apps - async def _current_app_name(self) -> Optional[str]: - """Return name of the currently running app by parsing pyvizio output.""" - app = await self._device.get_current_app(log_api_exception=False) - if app in [None, NO_APP_RUNNING]: - return None - - if app == UNKNOWN_APP and self._additional_app_configs: - return find_app_name( - await self._device.get_current_app_config(log_api_exception=False), - self._additional_app_configs, - ) - - return app - async def async_update(self) -> None: """Retrieve latest state of the device.""" if not self._model: @@ -202,6 +189,7 @@ class VizioDevice(MediaPlayerDevice): self._current_input = None self._available_inputs = None self._current_app = None + self._current_app_config = None self._available_apps = None return @@ -237,9 +225,16 @@ class VizioDevice(MediaPlayerDevice): if not self._available_apps: self._available_apps = self._apps_list(self._device.get_apps_list()) - # Attempt to get current app name. If app name is unknown, check list - # of additional apps specified in configuration - self._current_app = await self._current_app_name() + self._current_app_config = await self._device.get_current_app_config( + log_api_exception=False + ) + + self._current_app = find_app_name( + self._current_app_config, [APP_HOME, *APPS, *self._additional_app_configs] + ) + + if self._current_app == NO_APP_RUNNING: + self._current_app = None def _get_additional_app_names(self) -> List[Dict[str, Any]]: """Return list of additional apps that were included in configuration.yaml.""" @@ -346,8 +341,15 @@ class VizioDevice(MediaPlayerDevice): @property def app_id(self) -> Optional[str]: - """Return the current app.""" - return self._current_app + """Return the ID of the current app if it is unknown by pyvizio.""" + if self._current_app_config and self.app_name == UNKNOWN_APP: + return { + "APP_ID": self._current_app_config.APP_ID, + "NAME_SPACE": self._current_app_config.NAME_SPACE, + "MESSAGE": self._current_app_config.MESSAGE, + } + + return None @property def app_name(self) -> Optional[str]: diff --git a/requirements_all.txt b/requirements_all.txt index 75027277f24..7defeea4c9c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1732,7 +1732,7 @@ pyversasense==0.0.6 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.35 +pyvizio==0.1.44 # homeassistant.components.velux pyvlx==0.2.12 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4f6a0e1add..ff686fe5e30 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -635,7 +635,7 @@ pyvera==0.3.7 pyvesync==1.1.0 # homeassistant.components.vizio -pyvizio==0.1.35 +pyvizio==0.1.44 # homeassistant.components.html5 pywebpush==1.9.2 diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index ab581bdf3c6..868bf44a11b 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -7,7 +7,7 @@ from .const import ( ACCESS_TOKEN, APP_LIST, CH_TYPE, - CURRENT_APP, + CURRENT_APP_CONFIG, CURRENT_INPUT, INPUT_LIST, INPUT_LIST_WITH_APPS, @@ -172,7 +172,7 @@ def vizio_update_with_apps_fixture(vizio_update: pytest.fixture): "homeassistant.components.vizio.media_player.VizioAsync.get_current_input", return_value="CAST", ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_app", - return_value=CURRENT_APP, + "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", + return_value=CURRENT_APP_CONFIG, ): yield diff --git a/tests/components/vizio/const.py b/tests/components/vizio/const.py index 25abd01d53b..f1ddc4abba6 100644 --- a/tests/components/vizio/const.py +++ b/tests/components/vizio/const.py @@ -68,6 +68,7 @@ CURRENT_INPUT = "HDMI" INPUT_LIST = ["HDMI", "USB", "Bluetooth", "AUX"] CURRENT_APP = "Hulu" +CURRENT_APP_CONFIG = {CONF_APP_ID: "3", CONF_NAME_SPACE: 4, CONF_MESSAGE: None} APP_LIST = ["Hulu", "Netflix"] INPUT_LIST_WITH_APPS = INPUT_LIST + ["CAST"] CUSTOM_CONFIG = {CONF_APP_ID: "test", CONF_MESSAGE: None, CONF_NAME_SPACE: 10} @@ -75,6 +76,11 @@ ADDITIONAL_APP_CONFIG = { "name": CURRENT_APP, CONF_CONFIG: CUSTOM_CONFIG, } +UNKNOWN_APP_CONFIG = { + "APP_ID": "UNKNOWN", + "NAME_SPACE": 10, + "MESSAGE": None, +} ENTITY_ID = f"{MP_DOMAIN}.{slugify(NAME)}" diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index ebeef1661ed..f860c1cec4f 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -1,13 +1,13 @@ """Tests for Vizio config flow.""" from datetime import timedelta import logging -from typing import Any, Dict +from typing import Any, Dict, Optional from unittest.mock import call from asynctest import patch import pytest from pytest import raises -from pyvizio._api.apps import AppConfig +from pyvizio.api.apps import AppConfig from pyvizio.const import ( DEVICE_CLASS_SPEAKER as VIZIO_DEVICE_CLASS_SPEAKER, DEVICE_CLASS_TV as VIZIO_DEVICE_CLASS_TV, @@ -57,6 +57,7 @@ from .const import ( ADDITIONAL_APP_CONFIG, APP_LIST, CURRENT_APP, + CURRENT_APP_CONFIG, CURRENT_INPUT, CUSTOM_CONFIG, ENTITY_ID, @@ -71,6 +72,7 @@ from .const import ( MOCK_USER_VALID_TV_CONFIG, NAME, UNIQUE_ID, + UNKNOWN_APP_CONFIG, VOLUME_STEP, ) @@ -80,7 +82,7 @@ _LOGGER = logging.getLogger(__name__) async def _test_setup( - hass: HomeAssistantType, ha_device_class: str, vizio_power_state: bool + hass: HomeAssistantType, ha_device_class: str, vizio_power_state: Optional[bool] ) -> None: """Test Vizio Device entity setup.""" if vizio_power_state: @@ -112,7 +114,7 @@ async def _test_setup( "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", return_value=vizio_power_state, ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_app", + "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", ) as service_call: config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -136,7 +138,10 @@ async def _test_setup( async def _test_setup_with_apps( - hass: HomeAssistantType, device_config: Dict[str, Any], app: str + hass: HomeAssistantType, + device_config: Dict[str, Any], + app: Optional[str], + app_config: Dict[str, Any], ) -> None: """Test Vizio Device with apps entity setup.""" config_entry = MockConfigEntry( @@ -152,12 +157,9 @@ async def _test_setup_with_apps( ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_power_state", return_value=True, - ), patch( - "homeassistant.components.vizio.media_player.VizioAsync.get_current_app", - return_value=app, ), patch( "homeassistant.components.vizio.media_player.VizioAsync.get_current_app_config", - return_value=AppConfig(**ADDITIONAL_APP_CONFIG["config"]), + return_value=AppConfig(**app_config), ): config_entry.add_to_hass(hass) assert await hass.config_entries.async_setup(config_entry.entry_id) @@ -193,11 +195,20 @@ async def _test_setup_with_apps( list_to_test.remove(app_to_remove) assert attr["source_list"] == list_to_test - assert app in attr["source_list"] or app == UNKNOWN_APP - if app == UNKNOWN_APP: - assert attr["source"] == ADDITIONAL_APP_CONFIG["name"] - else: + + if app: + assert app in attr["source_list"] or app == UNKNOWN_APP assert attr["source"] == app + assert attr["app_name"] == app + if app == UNKNOWN_APP: + assert attr["app_id"] == app_config + else: + assert "app_id" not in attr + else: + assert attr["source"] == "CAST" + assert "app_id" not in attr + assert "app_name" not in attr + assert ( attr["volume_level"] == float(int(MAX_VOLUME[VIZIO_DEVICE_CLASS_TV] / 2)) @@ -222,7 +233,7 @@ async def _test_service( hass: HomeAssistantType, vizio_func_name: str, ha_service_name: str, - additional_service_data: dict, + additional_service_data: Optional[Dict[str, Any]], *args, **kwargs, ) -> None: @@ -363,8 +374,8 @@ async def test_options_update( async def _test_update_availability_switch( hass: HomeAssistantType, - initial_power_state: bool, - final_power_state: bool, + initial_power_state: Optional[bool], + final_power_state: Optional[bool], caplog: pytest.fixture, ) -> None: now = dt_util.utcnow() @@ -431,7 +442,9 @@ async def test_setup_with_apps( caplog: pytest.fixture, ) -> None: """Test device setup with apps.""" - await _test_setup_with_apps(hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP) + await _test_setup_with_apps( + hass, MOCK_USER_VALID_TV_CONFIG, CURRENT_APP, CURRENT_APP_CONFIG + ) await _test_service( hass, "launch_app", @@ -448,7 +461,9 @@ async def test_setup_with_apps_include( caplog: pytest.fixture, ) -> None: """Test device setup with apps and apps["include"] in config.""" - await _test_setup_with_apps(hass, MOCK_TV_WITH_INCLUDE_CONFIG, CURRENT_APP) + await _test_setup_with_apps( + hass, MOCK_TV_WITH_INCLUDE_CONFIG, CURRENT_APP, CURRENT_APP_CONFIG + ) async def test_setup_with_apps_exclude( @@ -458,7 +473,9 @@ async def test_setup_with_apps_exclude( caplog: pytest.fixture, ) -> None: """Test device setup with apps and apps["exclude"] in config.""" - await _test_setup_with_apps(hass, MOCK_TV_WITH_EXCLUDE_CONFIG, CURRENT_APP) + await _test_setup_with_apps( + hass, MOCK_TV_WITH_EXCLUDE_CONFIG, CURRENT_APP, CURRENT_APP_CONFIG + ) async def test_setup_with_apps_additional_apps_config( @@ -468,7 +485,12 @@ async def test_setup_with_apps_additional_apps_config( caplog: pytest.fixture, ) -> None: """Test device setup with apps and apps["additional_configs"] in config.""" - await _test_setup_with_apps(hass, MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, UNKNOWN_APP) + await _test_setup_with_apps( + hass, + MOCK_TV_WITH_ADDITIONAL_APPS_CONFIG, + ADDITIONAL_APP_CONFIG["name"], + ADDITIONAL_APP_CONFIG["config"], + ) await _test_service( hass, @@ -508,3 +530,27 @@ def test_invalid_apps_config(hass: HomeAssistantType): with raises(vol.Invalid): vol.Schema(vol.All(VIZIO_SCHEMA, validate_apps))(MOCK_SPEAKER_APPS_FAILURE) + + +async def test_setup_with_unknown_app_config( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update_with_apps: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device setup with apps where app config returned is unknown.""" + await _test_setup_with_apps( + hass, MOCK_USER_VALID_TV_CONFIG, UNKNOWN_APP, UNKNOWN_APP_CONFIG + ) + + +async def test_setup_with_no_running_app( + hass: HomeAssistantType, + vizio_connect: pytest.fixture, + vizio_update_with_apps: pytest.fixture, + caplog: pytest.fixture, +) -> None: + """Test device setup with apps where no app is running.""" + await _test_setup_with_apps( + hass, MOCK_USER_VALID_TV_CONFIG, None, vars(AppConfig()) + ) From eb8e8d00a6c8039ec7adeeec70cf0c981d8db82c Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 25 Mar 2020 13:29:40 -0400 Subject: [PATCH 252/431] Add group entities for ZHA switches (#33207) --- .../components/zha/core/registries.py | 2 +- homeassistant/components/zha/switch.py | 106 +++++++++--- tests/components/zha/test_switch.py | 156 ++++++++++++++++++ 3 files changed, 243 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index 34ae32c01c8..b596eefb71a 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -32,7 +32,7 @@ from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType -GROUP_ENTITY_DOMAINS = [LIGHT] +GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH] SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 6be3a9b3347..90ec98ce1e3 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,13 +1,16 @@ """Switches on Zigbee Home Automation networks.""" import functools import logging +from typing import Any, List, Optional +from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status from homeassistant.components.switch import DOMAIN, SwitchDevice -from homeassistant.const import STATE_ON -from homeassistant.core import callback +from homeassistant.const import STATE_ON, STATE_UNAVAILABLE +from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_state_change from .core import discovery from .core.const import ( @@ -16,12 +19,14 @@ from .core.const import ( DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, + SIGNAL_REMOVE_GROUP, ) from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity +from .entity import BaseZhaEntity, ZhaEntity _LOGGER = logging.getLogger(__name__) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -38,14 +43,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) -class Switch(ZhaEntity, SwitchDevice): - """ZHA switch.""" +class BaseSwitch(BaseZhaEntity, SwitchDevice): + """Common base class for zha switches.""" - def __init__(self, unique_id, zha_device, channels, **kwargs): + def __init__(self, *args, **kwargs): """Initialize the ZHA switch.""" - super().__init__(unique_id, zha_device, channels, **kwargs) - self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + self._on_off_channel = None + self._state = None + super().__init__(*args, **kwargs) @property def is_on(self) -> bool: @@ -54,7 +59,7 @@ class Switch(ZhaEntity, SwitchDevice): return False return self._state - async def async_turn_on(self, **kwargs): + async def async_turn_on(self, **kwargs) -> None: """Turn the entity on.""" result = await self._on_off_channel.on() if not isinstance(result, list) or result[1] is not Status.SUCCESS: @@ -62,7 +67,7 @@ class Switch(ZhaEntity, SwitchDevice): self._state = True self.async_write_ha_state() - async def async_turn_off(self, **kwargs): + async def async_turn_off(self, **kwargs) -> None: """Turn the entity off.""" result = await self._on_off_channel.off() if not isinstance(result, list) or result[1] is not Status.SUCCESS: @@ -70,18 +75,23 @@ class Switch(ZhaEntity, SwitchDevice): self._state = False self.async_write_ha_state() + +@STRICT_MATCH(channel_names=CHANNEL_ON_OFF) +class Switch(ZhaEntity, BaseSwitch): + """ZHA switch.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Initialize the ZHA switch.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._on_off_channel = self.cluster_channels.get(CHANNEL_ON_OFF) + @callback - def async_set_state(self, attr_id, attr_name, value): + def async_set_state(self, attr_id: int, attr_name: str, value: Any): """Handle state update from channel.""" self._state = bool(value) self.async_write_ha_state() - @property - def device_state_attributes(self): - """Return state attributes.""" - return self.state_attributes - - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Run when about to be added to hass.""" await super().async_added_to_hass() await self.async_accept_signal( @@ -89,14 +99,70 @@ class Switch(ZhaEntity, SwitchDevice): ) @callback - def async_restore_last_state(self, last_state): + def async_restore_last_state(self, last_state) -> None: """Restore previous state.""" self._state = last_state.state == STATE_ON - async def async_update(self): + async def async_update(self) -> None: """Attempt to retrieve on off state from the switch.""" await super().async_update() if self._on_off_channel: state = await self._on_off_channel.get_attribute_value("on_off") if state is not None: self._state = state + + +@GROUP_MATCH() +class SwitchGroup(BaseSwitch): + """Representation of a switch group.""" + + def __init__( + self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + ) -> None: + """Initialize a switch group.""" + super().__init__(unique_id, zha_device, **kwargs) + self._name: str = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" + self._group_id: int = group_id + self._available: bool = False + self._entity_ids: List[str] = entity_ids + group = self.zha_device.gateway.get_group(self._group_id) + self._on_off_channel = group.endpoint[OnOff.cluster_id] + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + await self.async_accept_signal( + None, + f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", + self.async_remove, + signal_override=True, + ) + + @callback + def async_state_changed_listener( + entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener + ) + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Handle removal from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._async_unsub_state_changed is not None: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + async def async_update(self) -> None: + """Query all members and determine the light group state.""" + all_states = [self.hass.states.get(x) for x in self._entity_ids] + states: List[State] = list(filter(None, all_states)) + on_states = [state for state in states if state.state == STATE_ON] + + self._state = len(on_states) > 0 + self._available = any(state.state != STATE_UNAVAILABLE for state in states) diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index 98f661cc1ab..adaaa7c2a2f 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -2,6 +2,7 @@ from unittest.mock import call, patch import pytest +import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general import zigpy.zcl.foundation as zcl_f @@ -10,8 +11,10 @@ from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from .common import ( async_enable_traffic, + async_find_group_entity_id, async_test_rejoin, find_entity_id, + get_zha_gateway, send_attributes_report, ) @@ -19,6 +22,8 @@ from tests.common import mock_coro ON = 1 OFF = 0 +IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @pytest.fixture @@ -34,6 +39,64 @@ def zigpy_device(zigpy_device_mock): return zigpy_device_mock(endpoints) +@pytest.fixture +async def coordinator(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee="00:15:8d:00:02:32:4f:32", + nwk=0x0000, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_switch_1(hass, zigpy_device_mock, zha_device_joined): + """Test zha switch platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [general.OnOff.cluster_id], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_switch_2(hass, zigpy_device_mock, zha_device_joined): + """Test zha switch platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [general.OnOff.cluster_id], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE2, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + async def test_switch(hass, zha_device_joined_restored, zigpy_device): """Test zha switch platform.""" @@ -89,3 +152,96 @@ async def test_switch(hass, zha_device_joined_restored, zigpy_device): # test joining a new switch to the network and HA await async_test_rejoin(hass, zigpy_device, [cluster], (1,)) + + +async def async_test_zha_group_switch_entity( + hass, device_switch_1, device_switch_2, coordinator +): + """Test the switch entity for a ZHA group.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_switch_1._zha_gateway = zha_gateway + device_switch_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_switch_1.ieee, device_switch_2.ieee] + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group( + "Test Group", member_ieee_addresses + ) + await hass.async_block_till_done() + + assert zha_group is not None + assert zha_group.entity_domain == DOMAIN + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.ieee in member_ieee_addresses + + entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + assert hass.states.get(entity_id) is not None + + group_cluster_on_off = zha_group.endpoint[general.OnOff.cluster_id] + dev1_cluster_on_off = device_switch_1.endpoints[1].on_off + dev2_cluster_on_off = device_switch_2.endpoints[1].on_off + + # test that the lights were created and that they are 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_group.members) + + # test that the lights were created and are 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(group_cluster_on_off.request.mock_calls) == 1 + assert group_cluster_on_off.request.call_args == call( + False, ON, (), expect_reply=True, manufacturer=None, tsn=None + ) + 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(group_cluster_on_off.request.mock_calls) == 1 + assert group_cluster_on_off.request.call_args == call( + False, OFF, (), expect_reply=True, manufacturer=None, tsn=None + ) + assert hass.states.get(entity_id).state == STATE_OFF + + # test some of the group logic to make sure we key off states correctly + await dev1_cluster_on_off.on() + await dev2_cluster_on_off.on() + + # test that group light is on + assert hass.states.get(entity_id).state == STATE_ON + + await dev1_cluster_on_off.off() + + # test that group light is still on + assert hass.states.get(entity_id).state == STATE_ON + + await dev2_cluster_on_off.off() + + # test that group light is now off + assert hass.states.get(entity_id).state == STATE_OFF + + await dev1_cluster_on_off.on() + + # test that group light is now back on + assert hass.states.get(entity_id).state == STATE_ON From 16670a38a432e556cdd406becef9ac6b084fbc86 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Wed, 25 Mar 2020 19:13:28 +0100 Subject: [PATCH 253/431] Dynamic update interval for Airly integration (#31459) * Initial commit * dynamic update * Don't update when add entities * Cleaning * Fix MAX_REQUESTS_PER_DAY const * Fix pylint errors * Fix comment * Migrate to DataUpdateCoordinator * Cleaning * Suggested change * Change try..except as suggested * Remove unnecessary self._attrs variable * Cleaning * Fix typo * Change update_interval method * Add comments * Add ConfigEntryNotReady --- homeassistant/components/airly/__init__.py | 132 +++++++++++------- homeassistant/components/airly/air_quality.py | 69 ++++----- homeassistant/components/airly/const.py | 2 +- homeassistant/components/airly/sensor.py | 36 +++-- 4 files changed, 138 insertions(+), 101 deletions(-) diff --git a/homeassistant/components/airly/__init__.py b/homeassistant/components/airly/__init__.py index bad5a48c05f..85071925357 100644 --- a/homeassistant/components/airly/__init__.py +++ b/homeassistant/components/airly/__init__.py @@ -2,6 +2,7 @@ import asyncio from datetime import timedelta import logging +from math import ceil from aiohttp.client_exceptions import ClientConnectorError from airly import Airly @@ -10,28 +11,40 @@ import async_timeout from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import ( ATTR_API_ADVICE, ATTR_API_CAQI, ATTR_API_CAQI_DESCRIPTION, ATTR_API_CAQI_LEVEL, - DATA_CLIENT, DOMAIN, + MAX_REQUESTS_PER_DAY, NO_AIRLY_SENSORS, ) +PLATFORMS = ["air_quality", "sensor"] + _LOGGER = logging.getLogger(__name__) -DEFAULT_SCAN_INTERVAL = timedelta(minutes=10) + +def set_update_interval(hass, instances): + """Set update_interval to another configured Airly instances.""" + # We check how many Airly configured instances are and calculate interval to not + # exceed allowed numbers of requests. + interval = timedelta(minutes=ceil(24 * 60 / MAX_REQUESTS_PER_DAY) * instances) + + if hass.data.get(DOMAIN): + for instance in hass.data[DOMAIN].values(): + instance.update_interval = interval + + return interval async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured Airly.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} return True @@ -48,70 +61,85 @@ async def async_setup_entry(hass, config_entry): ) websession = async_get_clientsession(hass) - - airly = AirlyData(websession, api_key, latitude, longitude) - - await airly.async_update() - - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = airly - - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") + # Change update_interval for other Airly instances + update_interval = set_update_interval( + hass, len(hass.config_entries.async_entries(DOMAIN)) ) - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + + coordinator = AirlyDataUpdateCoordinator( + hass, websession, api_key, latitude, longitude, update_interval ) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = coordinator + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, component) + ) + return True async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) - await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") - await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") - return True + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(config_entry.entry_id) + + # Change update_interval for other Airly instances + set_update_interval(hass, len(hass.data[DOMAIN])) + + return unload_ok -class AirlyData: +class AirlyDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold Airly data.""" - def __init__(self, session, api_key, latitude, longitude): + def __init__(self, hass, session, api_key, latitude, longitude, update_interval): """Initialize.""" self.latitude = latitude self.longitude = longitude self.airly = Airly(api_key, session) - self.data = {} - @Throttle(DEFAULT_SCAN_INTERVAL) - async def async_update(self): - """Update Airly data.""" + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=update_interval) - try: - with async_timeout.timeout(20): - measurements = self.airly.create_measurements_session_point( - self.latitude, self.longitude - ) + async def _async_update_data(self): + """Update data via library.""" + data = {} + with async_timeout.timeout(20): + measurements = self.airly.create_measurements_session_point( + self.latitude, self.longitude + ) + try: await measurements.update() + except (AirlyError, ClientConnectorError) as error: + raise UpdateFailed(error) - values = measurements.current["values"] - index = measurements.current["indexes"][0] - standards = measurements.current["standards"] + values = measurements.current["values"] + index = measurements.current["indexes"][0] + standards = measurements.current["standards"] - if index["description"] == NO_AIRLY_SENSORS: - _LOGGER.error("Can't retrieve data: no Airly sensors in this area") - return - for value in values: - self.data[value["name"]] = value["value"] - for standard in standards: - self.data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] - self.data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] - self.data[ATTR_API_CAQI] = index["value"] - self.data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") - self.data[ATTR_API_CAQI_DESCRIPTION] = index["description"] - self.data[ATTR_API_ADVICE] = index["advice"] - _LOGGER.debug("Data retrieved from Airly") - except asyncio.TimeoutError: - _LOGGER.error("Asyncio Timeout Error") - except (ValueError, AirlyError, ClientConnectorError) as error: - _LOGGER.error(error) - self.data = {} + if index["description"] == NO_AIRLY_SENSORS: + raise UpdateFailed("Can't retrieve data: no Airly sensors in this area") + for value in values: + data[value["name"]] = value["value"] + for standard in standards: + data[f"{standard['pollutant']}_LIMIT"] = standard["limit"] + data[f"{standard['pollutant']}_PERCENT"] = standard["percent"] + data[ATTR_API_CAQI] = index["value"] + data[ATTR_API_CAQI_LEVEL] = index["level"].lower().replace("_", " ") + data[ATTR_API_CAQI_DESCRIPTION] = index["description"] + data[ATTR_API_ADVICE] = index["advice"] + return data diff --git a/homeassistant/components/airly/air_quality.py b/homeassistant/components/airly/air_quality.py index 45b4dfa3a37..fa42e58e9ad 100644 --- a/homeassistant/components/airly/air_quality.py +++ b/homeassistant/components/airly/air_quality.py @@ -18,13 +18,13 @@ from .const import ( ATTR_API_PM25, ATTR_API_PM25_LIMIT, ATTR_API_PM25_PERCENT, - DATA_CLIENT, DOMAIN, ) ATTRIBUTION = "Data provided by Airly" LABEL_ADVICE = "advice" +LABEL_AQI_DESCRIPTION = f"{ATTR_AQI}_description" LABEL_AQI_LEVEL = f"{ATTR_AQI}_level" LABEL_PM_2_5_LIMIT = f"{ATTR_PM_2_5}_limit" LABEL_PM_2_5_PERCENT = f"{ATTR_PM_2_5}_percent_of_limit" @@ -36,9 +36,11 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly air_quality entity based on a config entry.""" name = config_entry.data[CONF_NAME] - data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([AirlyAirQuality(data, name, config_entry.unique_id)], True) + async_add_entities( + [AirlyAirQuality(coordinator, name, config_entry.unique_id)], False + ) def round_state(func): @@ -56,23 +58,23 @@ def round_state(func): class AirlyAirQuality(AirQualityEntity): """Define an Airly air quality.""" - def __init__(self, airly, name, unique_id): + def __init__(self, coordinator, name, unique_id): """Initialize.""" - self.airly = airly - self.data = airly.data + self.coordinator = coordinator self._name = name self._unique_id = unique_id - self._pm_2_5 = None - self._pm_10 = None - self._aqi = None self._icon = "mdi:blur" - self._attrs = {} @property def name(self): """Return the name.""" return self._name + @property + def should_poll(self): + """Return the polling requirement of the entity.""" + return False + @property def icon(self): """Return the icon.""" @@ -82,30 +84,25 @@ class AirlyAirQuality(AirQualityEntity): @round_state def air_quality_index(self): """Return the air quality index.""" - return self._aqi + return self.coordinator.data[ATTR_API_CAQI] @property @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self._pm_2_5 + return self.coordinator.data[ATTR_API_PM25] @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self._pm_10 + return self.coordinator.data[ATTR_API_PM10] @property def attribution(self): """Return the attribution.""" return ATTRIBUTION - @property - def state(self): - """Return the CAQI description.""" - return self.data[ATTR_API_CAQI_DESCRIPTION] - @property def unique_id(self): """Return a unique_id for this entity.""" @@ -114,25 +111,29 @@ class AirlyAirQuality(AirQualityEntity): @property def available(self): """Return True if entity is available.""" - return bool(self.data) + return self.coordinator.last_update_success @property def device_state_attributes(self): """Return the state attributes.""" - self._attrs[LABEL_ADVICE] = self.data[ATTR_API_ADVICE] - self._attrs[LABEL_AQI_LEVEL] = self.data[ATTR_API_CAQI_LEVEL] - self._attrs[LABEL_PM_2_5_LIMIT] = self.data[ATTR_API_PM25_LIMIT] - self._attrs[LABEL_PM_2_5_PERCENT] = round(self.data[ATTR_API_PM25_PERCENT]) - self._attrs[LABEL_PM_10_LIMIT] = self.data[ATTR_API_PM10_LIMIT] - self._attrs[LABEL_PM_10_PERCENT] = round(self.data[ATTR_API_PM10_PERCENT]) - return self._attrs + return { + LABEL_AQI_DESCRIPTION: self.coordinator.data[ATTR_API_CAQI_DESCRIPTION], + LABEL_ADVICE: self.coordinator.data[ATTR_API_ADVICE], + LABEL_AQI_LEVEL: self.coordinator.data[ATTR_API_CAQI_LEVEL], + LABEL_PM_2_5_LIMIT: self.coordinator.data[ATTR_API_PM25_LIMIT], + LABEL_PM_2_5_PERCENT: round(self.coordinator.data[ATTR_API_PM25_PERCENT]), + LABEL_PM_10_LIMIT: self.coordinator.data[ATTR_API_PM10_LIMIT], + LABEL_PM_10_PERCENT: round(self.coordinator.data[ATTR_API_PM10_PERCENT]), + } + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) async def async_update(self): - """Update the entity.""" - await self.airly.async_update() - - if self.airly.data: - self.data = self.airly.data - self._pm_10 = self.data[ATTR_API_PM10] - self._pm_2_5 = self.data[ATTR_API_PM25] - self._aqi = self.data[ATTR_API_CAQI] + """Update Airly entity.""" + await self.coordinator.async_request_refresh() diff --git a/homeassistant/components/airly/const.py b/homeassistant/components/airly/const.py index 2040faea6b6..d7f8fc12797 100644 --- a/homeassistant/components/airly/const.py +++ b/homeassistant/components/airly/const.py @@ -13,7 +13,7 @@ ATTR_API_PM25_LIMIT = "PM25_LIMIT" ATTR_API_PM25_PERCENT = "PM25_PERCENT" ATTR_API_PRESSURE = "PRESSURE" ATTR_API_TEMPERATURE = "TEMPERATURE" -DATA_CLIENT = "client" DEFAULT_NAME = "Airly" DOMAIN = "airly" +MAX_REQUESTS_PER_DAY = 100 NO_AIRLY_SENSORS = "There are no Airly sensors in this area yet." diff --git a/homeassistant/components/airly/sensor.py b/homeassistant/components/airly/sensor.py index a6754b4a00d..0ee9fb3aac5 100644 --- a/homeassistant/components/airly/sensor.py +++ b/homeassistant/components/airly/sensor.py @@ -18,7 +18,6 @@ from .const import ( ATTR_API_PM1, ATTR_API_PRESSURE, ATTR_API_TEMPERATURE, - DATA_CLIENT, DOMAIN, ) @@ -60,14 +59,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): """Set up Airly sensor entities based on a config entry.""" name = config_entry.data[CONF_NAME] - data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id] sensors = [] for sensor in SENSOR_TYPES: unique_id = f"{config_entry.unique_id}-{sensor.lower()}" - sensors.append(AirlySensor(data, name, sensor, unique_id)) + sensors.append(AirlySensor(coordinator, name, sensor, unique_id)) - async_add_entities(sensors, True) + async_add_entities(sensors, False) def round_state(func): @@ -85,10 +84,9 @@ def round_state(func): class AirlySensor(Entity): """Define an Airly sensor.""" - def __init__(self, airly, name, kind, unique_id): + def __init__(self, coordinator, name, kind, unique_id): """Initialize.""" - self.airly = airly - self.data = airly.data + self.coordinator = coordinator self._name = name self._unique_id = unique_id self.kind = kind @@ -103,10 +101,15 @@ class AirlySensor(Entity): """Return the name.""" return f"{self._name} {SENSOR_TYPES[self.kind][ATTR_LABEL]}" + @property + def should_poll(self): + """Return the polling requirement of the entity.""" + return False + @property def state(self): """Return the state.""" - self._state = self.data[self.kind] + self._state = self.coordinator.data[self.kind] if self.kind in [ATTR_API_PM1, ATTR_API_PRESSURE]: self._state = round(self._state) if self.kind in [ATTR_API_TEMPERATURE, ATTR_API_HUMIDITY]: @@ -142,11 +145,16 @@ class AirlySensor(Entity): @property def available(self): """Return True if entity is available.""" - return bool(self.data) + return self.coordinator.last_update_success + + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) async def async_update(self): - """Update the sensor.""" - await self.airly.async_update() - - if self.airly.data: - self.data = self.airly.data + """Update Airly entity.""" + await self.coordinator.async_request_refresh() From 4bbc0a03ca7af0ff85ea6eb7ce45fa4eaff82fd9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 25 Mar 2020 13:03:56 -0700 Subject: [PATCH 254/431] Activate asyncio debug when HA run in debug mode (#33251) --- homeassistant/__main__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/__main__.py b/homeassistant/__main__.py index d1d59482e6d..a6d4e0c7bc9 100644 --- a/homeassistant/__main__.py +++ b/homeassistant/__main__.py @@ -339,7 +339,7 @@ def main() -> int: if args.pid_file: write_pid(args.pid_file) - exit_code = asyncio.run(setup_and_run_hass(config_dir, args)) + exit_code = asyncio.run(setup_and_run_hass(config_dir, args), debug=args.debug) if exit_code == RESTART_EXIT_CODE and not args.runner: try_to_restart() From 6d311a31ddc865cc45d95247ec4a76fcd2e89120 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 25 Mar 2020 15:08:20 -0500 Subject: [PATCH 255/431] =?UTF-8?q?Ensure=20recorder=20event=20loop=20reco?= =?UTF-8?q?vers=20if=20the=20database=20server=20dis=E2=80=A6=20(#33253)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit If the database server disconnects there were exceptions that were not trapped which would cause the recorder event loop to collapse. As we never want the loop to end we trap exceptions broadly. Fix a bug in the new commit interval setting which caused it to always commit after 1s --- homeassistant/components/recorder/__init__.py | 47 +++++++++++++------ 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/recorder/__init__.py b/homeassistant/components/recorder/__init__.py index a662a457add..ffd37720053 100644 --- a/homeassistant/components/recorder/__init__.py +++ b/homeassistant/components/recorder/__init__.py @@ -342,7 +342,6 @@ class Recorder(threading.Thread): # has changed. This reduces the disk io. while True: event = self.queue.get() - if event is None: self._close_run() self._close_connection() @@ -356,7 +355,7 @@ class Recorder(threading.Thread): self.queue.task_done() if self.commit_interval: self._timechanges_seen += 1 - if self.commit_interval >= self._timechanges_seen: + if self._timechanges_seen >= self.commit_interval: self._timechanges_seen = 0 self._commit_event_session_or_retry() continue @@ -376,6 +375,9 @@ class Recorder(threading.Thread): self.event_session.flush() except (TypeError, ValueError): _LOGGER.warning("Event is not JSON serializable: %s", event) + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Error adding event: %s", err) if dbevent and event.event_type == EVENT_STATE_CHANGED: try: @@ -387,6 +389,9 @@ class Recorder(threading.Thread): "State is not JSON serializable: %s", event.data.get("new_state"), ) + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Error adding state change: %s", err) # If they do not have a commit interval # than we commit right away @@ -404,17 +409,26 @@ class Recorder(threading.Thread): try: self._commit_event_session() return - - except exc.OperationalError as err: - _LOGGER.error( - "Error in database connectivity: %s. " "(retrying in %s seconds)", - err, - self.db_retry_wait, - ) + except (exc.InternalError, exc.OperationalError) as err: + if err.connection_invalidated: + _LOGGER.error( + "Database connection invalidated: %s. " + "(retrying in %s seconds)", + err, + self.db_retry_wait, + ) + else: + _LOGGER.error( + "Error in database connectivity: %s. " + "(retrying in %s seconds)", + err, + self.db_retry_wait, + ) tries += 1 - except exc.SQLAlchemyError: - _LOGGER.exception("Error saving events") + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Error saving events: %s", err) return _LOGGER.error( @@ -423,10 +437,15 @@ class Recorder(threading.Thread): ) try: self.event_session.close() - except exc.SQLAlchemyError: - _LOGGER.exception("Failed to close event session.") + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Error while closing event session: %s", err) - self.event_session = self.get_session() + try: + self.event_session = self.get_session() + except Exception as err: # pylint: disable=broad-except + # Must catch the exception to prevent the loop from collapsing + _LOGGER.exception("Error while creating new event session: %s", err) def _commit_event_session(self): try: From 2647296475d8d35050744428f2de0fe6c79d897e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Wed, 25 Mar 2020 21:21:04 +0100 Subject: [PATCH 256/431] Refactor API documentation (#33217) * Upgrade Sphinx to 2.4.4 * Refactor API documentation * Remove left-over * Remove Markdown from docstring * Update titels --- docs/source/api/auth.rst | 29 + docs/source/api/bootstrap.rst | 2 +- docs/source/api/components.rst | 170 +++++ docs/source/api/config_entries.rst | 7 + docs/source/api/core.rst | 33 +- docs/source/api/data_entry_flow.rst | 7 + docs/source/api/device_tracker.rst | 10 - docs/source/api/entity.rst | 12 - docs/source/api/event.rst | 20 - docs/source/api/exceptions.rst | 7 + docs/source/api/helpers.rst | 618 ++++++++++-------- docs/source/api/homeassistant.rst | 70 -- docs/source/api/loader.rst | 7 + docs/source/api/util.rst | 241 ++++--- docs/source/conf.py | 2 +- .../helpers/config_entry_oauth2_flow.py | 3 +- requirements_docs.txt | 2 +- 17 files changed, 722 insertions(+), 518 deletions(-) create mode 100644 docs/source/api/auth.rst create mode 100644 docs/source/api/components.rst create mode 100644 docs/source/api/config_entries.rst create mode 100644 docs/source/api/data_entry_flow.rst delete mode 100644 docs/source/api/device_tracker.rst delete mode 100644 docs/source/api/entity.rst delete mode 100644 docs/source/api/event.rst create mode 100644 docs/source/api/exceptions.rst delete mode 100644 docs/source/api/homeassistant.rst create mode 100644 docs/source/api/loader.rst diff --git a/docs/source/api/auth.rst b/docs/source/api/auth.rst new file mode 100644 index 00000000000..16a1dc69b6b --- /dev/null +++ b/docs/source/api/auth.rst @@ -0,0 +1,29 @@ +:mod:`homeassistant.auth` +========================= + +.. automodule:: homeassistant.auth + :members: + +homeassistant.auth.auth\_store +------------------------------ + +.. automodule:: homeassistant.auth.auth_store + :members: + :undoc-members: + :show-inheritance: + +homeassistant.auth.const +------------------------ + +.. automodule:: homeassistant.auth.const + :members: + :undoc-members: + :show-inheritance: + +homeassistant.auth.models +------------------------- + +.. automodule:: homeassistant.auth.models + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/bootstrap.rst b/docs/source/api/bootstrap.rst index 363f7969961..fdc0b1c731d 100644 --- a/docs/source/api/bootstrap.rst +++ b/docs/source/api/bootstrap.rst @@ -1,7 +1,7 @@ .. _bootstrap_module: :mod:`homeassistant.bootstrap` -------------------------- +------------------------------ .. automodule:: homeassistant.bootstrap :members: diff --git a/docs/source/api/components.rst b/docs/source/api/components.rst new file mode 100644 index 00000000000..a27f93765b4 --- /dev/null +++ b/docs/source/api/components.rst @@ -0,0 +1,170 @@ +:mod:`homeassistant.components` +=============================== + +air\_quality +-------------------------------------------- + +.. automodule:: homeassistant.components.air_quality + :members: + :undoc-members: + :show-inheritance: + +alarm\_control\_panel +-------------------------------------------- + +.. automodule:: homeassistant.components.alarm_control_panel + :members: + :undoc-members: + :show-inheritance: + +binary\_sensor +-------------------------------------------- + +.. automodule:: homeassistant.components.binary_sensor + :members: + :undoc-members: + :show-inheritance: + +camera +--------------------------- + +.. automodule:: homeassistant.components.camera + :members: + :undoc-members: + :show-inheritance: + +calendar +--------------------------- + +.. automodule:: homeassistant.components.calendar + :members: + :undoc-members: + :show-inheritance: + +climate +--------------------------- + +.. automodule:: homeassistant.components.climate + :members: + :undoc-members: + :show-inheritance: + +conversation +--------------------------- + +.. automodule:: homeassistant.components.conversation + :members: + :undoc-members: + :show-inheritance: + +cover +--------------------------- + +.. automodule:: homeassistant.components.cover + :members: + :undoc-members: + :show-inheritance: + +device\_tracker +--------------------------- + +.. automodule:: homeassistant.components.device_tracker + :members: + :undoc-members: + :show-inheritance: + +fan +--------------------------- + +.. automodule:: homeassistant.components.fan + :members: + :undoc-members: + :show-inheritance: + +light +--------------------------- + +.. automodule:: homeassistant.components.light + :members: + :undoc-members: + :show-inheritance: + +lock +--------------------------- + +.. automodule:: homeassistant.components.lock + :members: + :undoc-members: + :show-inheritance: + +media\_player +--------------------------- + +.. automodule:: homeassistant.components.media_player + :members: + :undoc-members: + :show-inheritance: + +notify +--------------------------- + +.. automodule:: homeassistant.components.notify + :members: + :undoc-members: + :show-inheritance: + +remote +--------------------------- + +.. automodule:: homeassistant.components.remote + :members: + :undoc-members: + :show-inheritance: + +switch +--------------------------- + +.. automodule:: homeassistant.components.switch + :members: + :undoc-members: + :show-inheritance: + +sensor +------------------------------------- + +.. automodule:: homeassistant.components.sensor + :members: + :undoc-members: + :show-inheritance: + +vacuum +------------------------------------- + +.. automodule:: homeassistant.components.vacuum + :members: + :undoc-members: + :show-inheritance: + +water\_heater +------------------------------------- + +.. automodule:: homeassistant.components.water_heater + :members: + :undoc-members: + :show-inheritance: + +weather +--------------------------- + +.. automodule:: homeassistant.components.weather + :members: + :undoc-members: + :show-inheritance: + +webhook +--------------------------- + +.. automodule:: homeassistant.components.webhook + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/config_entries.rst b/docs/source/api/config_entries.rst new file mode 100644 index 00000000000..4a207b82e16 --- /dev/null +++ b/docs/source/api/config_entries.rst @@ -0,0 +1,7 @@ +.. _config_entries_module: + +:mod:`homeassistant.config_entries` +----------------------------------- + +.. automodule:: homeassistant.config_entries + :members: diff --git a/docs/source/api/core.rst b/docs/source/api/core.rst index bbaf591052c..7928655b8a1 100644 --- a/docs/source/api/core.rst +++ b/docs/source/api/core.rst @@ -4,35 +4,4 @@ ------------------------- .. automodule:: homeassistant.core - -.. autoclass:: Config - :members: - -.. autoclass:: Event - :members: - -.. autoclass:: EventBus - :members: - -.. autoclass:: HomeAssistant - :members: - -.. autoclass:: State - :members: - -.. autoclass:: StateMachine - :members: - -.. autoclass:: ServiceCall - :members: - -.. autoclass:: ServiceRegistry - :members: - -Module contents ---------------- - -.. automodule:: homeassistant.core - :members: - :undoc-members: - :show-inheritance: + :members: \ No newline at end of file diff --git a/docs/source/api/data_entry_flow.rst b/docs/source/api/data_entry_flow.rst new file mode 100644 index 00000000000..7252780b870 --- /dev/null +++ b/docs/source/api/data_entry_flow.rst @@ -0,0 +1,7 @@ +.. _data_entry_flow_module: + +:mod:`homeassistant.data_entry_flow` +----------------------------- + +.. automodule:: homeassistant.data_entry_flow + :members: diff --git a/docs/source/api/device_tracker.rst b/docs/source/api/device_tracker.rst deleted file mode 100644 index e3d65174815..00000000000 --- a/docs/source/api/device_tracker.rst +++ /dev/null @@ -1,10 +0,0 @@ -.. _components_device_tracker_module: - -:mod:`homeassistant.components.device_tracker` ----------------------------------------------- - -.. automodule:: homeassistant.components.device_tracker - :members: - -.. autoclass:: Device - :members: diff --git a/docs/source/api/entity.rst b/docs/source/api/entity.rst deleted file mode 100644 index 99ae43dc3ae..00000000000 --- a/docs/source/api/entity.rst +++ /dev/null @@ -1,12 +0,0 @@ -.. _helpers_entity_module: - -:mod:`homeassistant.helpers.entity` ------------------------------------ - -.. automodule:: homeassistant.helpers.entity - -.. autoclass:: Entity - :members: - -.. autoclass:: ToggleEntity - :members: diff --git a/docs/source/api/event.rst b/docs/source/api/event.rst deleted file mode 100644 index b1295b81409..00000000000 --- a/docs/source/api/event.rst +++ /dev/null @@ -1,20 +0,0 @@ -.. _helpers_event_module: - -:mod:`homeassistant.helpers.event` ----------------------------------- - -.. automodule:: homeassistant.helpers.event - -.. autofunction:: track_state_change - -.. autofunction:: track_point_in_time - -.. autofunction:: track_point_in_utc_time - -.. autofunction:: track_sunrise - -.. autofunction:: track_sunset - -.. autofunction:: track_utc_time_change - -.. autofunction:: track_time_change diff --git a/docs/source/api/exceptions.rst b/docs/source/api/exceptions.rst new file mode 100644 index 00000000000..e2977c51dae --- /dev/null +++ b/docs/source/api/exceptions.rst @@ -0,0 +1,7 @@ +.. _exceptions_module: + +:mod:`homeassistant.exceptions` +------------------------------- + +.. automodule:: homeassistant.exceptions + :members: diff --git a/docs/source/api/helpers.rst b/docs/source/api/helpers.rst index 8ad645b7977..1b0b529c655 100644 --- a/docs/source/api/helpers.rst +++ b/docs/source/api/helpers.rst @@ -1,287 +1,335 @@ -homeassistant.helpers package -============================= - -Submodules ----------- - -homeassistant.helpers.aiohttp_client module -------------------------------------------- - -.. automodule:: homeassistant.helpers.aiohttp_client - :members: - :undoc-members: - :show-inheritance: - - -homeassistant.helpers.area_registry module ------------------------------------------- - -.. automodule:: homeassistant.helpers.area_registry - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.condition module --------------------------------------- - -.. automodule:: homeassistant.helpers.condition - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.config_entry_flow module ----------------------------------------------- - -.. automodule:: homeassistant.helpers.config_entry_flow - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.config_validation module ----------------------------------------------- - -.. automodule:: homeassistant.helpers.config_validation - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.data_entry_flow module --------------------------------------------- - -.. automodule:: homeassistant.helpers.data_entry_flow - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.deprecation module ----------------------------------------- - -.. automodule:: homeassistant.helpers.deprecation - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.device_registry module --------------------------------------------- - -.. automodule:: homeassistant.helpers.device_registry - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.discovery module --------------------------------------- - -.. automodule:: homeassistant.helpers.discovery - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.dispatcher module ---------------------------------------- - -.. automodule:: homeassistant.helpers.dispatcher - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.entity module ------------------------------------ - -.. automodule:: homeassistant.helpers.entity - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.entity_component module ---------------------------------------------- - -.. automodule:: homeassistant.helpers.entity_component - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.entity_platform module --------------------------------------------- - -.. automodule:: homeassistant.helpers.entity_platform - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.entity_registry module --------------------------------------------- - -.. automodule:: homeassistant.helpers.entity_registry - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.entity_values module ------------------------------------------- - -.. automodule:: homeassistant.helpers.entity_values - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.entityfilter module ------------------------------------------ - -.. automodule:: homeassistant.helpers.entityfilter - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.event module ----------------------------------- - -.. automodule:: homeassistant.helpers.event - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.icon module ---------------------------------- - -.. automodule:: homeassistant.helpers.icon - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.intent module ------------------------------------ - -.. automodule:: homeassistant.helpers.intent - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.json module ---------------------------------- - -.. automodule:: homeassistant.helpers.json - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.location module -------------------------------------- - -.. automodule:: homeassistant.helpers.location - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.logging module ------------------------------------- - -.. automodule:: homeassistant.helpers.logging - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.restore_state module ------------------------------------------- - -.. automodule:: homeassistant.helpers.restore_state - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.script module ------------------------------------ - -.. automodule:: homeassistant.helpers.script - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.service module ------------------------------------- - -.. automodule:: homeassistant.helpers.service - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.signal module ------------------------------------ - -.. automodule:: homeassistant.helpers.signal - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.state module ----------------------------------- - -.. automodule:: homeassistant.helpers.state - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.storage module ------------------------------------- - -.. automodule:: homeassistant.helpers.storage - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.sun module --------------------------------- - -.. automodule:: homeassistant.helpers.sun - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.system_info module ----------------------------------------- - -.. automodule:: homeassistant.helpers.system_info - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.temperature module ----------------------------------------- - -.. automodule:: homeassistant.helpers.temperature - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.template module -------------------------------------- - -.. automodule:: homeassistant.helpers.template - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.translation module ------------------------------------------ - -.. automodule:: homeassistant.helpers.translation - :members: - :undoc-members: - :show-inheritance: - -homeassistant.helpers.typing module ------------------------------------ - -.. automodule:: homeassistant.helpers.typing - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- +:mod:`homeassistant.helpers` +============================ .. automodule:: homeassistant.helpers - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.aiohttp\_client +------------------------------------- + +.. automodule:: homeassistant.helpers.aiohttp_client + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.area\_registry +------------------------------------ + +.. automodule:: homeassistant.helpers.area_registry + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.check\_config +----------------------------------- + +.. automodule:: homeassistant.helpers.check_config + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.collection +-------------------------------- + +.. automodule:: homeassistant.helpers.collection + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.condition +------------------------------- + +.. automodule:: homeassistant.helpers.condition + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.config\_entry\_flow +----------------------------------------- + +.. automodule:: homeassistant.helpers.config_entry_flow + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.config\_entry\_oauth2\_flow +------------------------------------------------- + +.. automodule:: homeassistant.helpers.config_entry_oauth2_flow + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.config\_validation +---------------------------------------- + +.. automodule:: homeassistant.helpers.config_validation + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.data\_entry\_flow +--------------------------------------- + +.. automodule:: homeassistant.helpers.data_entry_flow + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.debounce +------------------------------ + +.. automodule:: homeassistant.helpers.debounce + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.deprecation +--------------------------------- + +.. automodule:: homeassistant.helpers.deprecation + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.device\_registry +-------------------------------------- + +.. automodule:: homeassistant.helpers.device_registry + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.discovery +------------------------------- + +.. automodule:: homeassistant.helpers.discovery + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.dispatcher +-------------------------------- + +.. automodule:: homeassistant.helpers.dispatcher + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity +---------------------------- + +.. automodule:: homeassistant.helpers.entity + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity\_component +--------------------------------------- + +.. automodule:: homeassistant.helpers.entity_component + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity\_platform +-------------------------------------- + +.. automodule:: homeassistant.helpers.entity_platform + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity\_registry +-------------------------------------- + +.. automodule:: homeassistant.helpers.entity_registry + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entity\_values +------------------------------------ + +.. automodule:: homeassistant.helpers.entity_values + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.entityfilter +---------------------------------- + +.. automodule:: homeassistant.helpers.entityfilter + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.event +--------------------------- + +.. automodule:: homeassistant.helpers.event + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.icon +-------------------------- + +.. automodule:: homeassistant.helpers.icon + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.integration\_platform +------------------------------------------- + +.. automodule:: homeassistant.helpers.integration_platform + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.intent +---------------------------- + +.. automodule:: homeassistant.helpers.intent + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.json +-------------------------- + +.. automodule:: homeassistant.helpers.json + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.location +------------------------------ + +.. automodule:: homeassistant.helpers.location + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.logging +----------------------------- + +.. automodule:: homeassistant.helpers.logging + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.network +----------------------------- + +.. automodule:: homeassistant.helpers.network + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.restore\_state +------------------------------------ + +.. automodule:: homeassistant.helpers.restore_state + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.script +---------------------------- + +.. automodule:: homeassistant.helpers.script + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.service +----------------------------- + +.. automodule:: homeassistant.helpers.service + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.signal +----------------------------- + +.. automodule:: homeassistant.helpers.signal + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.state +--------------------------- + +.. automodule:: homeassistant.helpers.state + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.storage +----------------------------- + +.. automodule:: homeassistant.helpers.storage + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.sun +------------------------- + +.. automodule:: homeassistant.helpers.sun + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.system\_info +---------------------------------- + +.. automodule:: homeassistant.helpers.system_info + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.temperature +--------------------------------- + +.. automodule:: homeassistant.helpers.temperature + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.template +------------------------------ + +.. automodule:: homeassistant.helpers.template + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.translation +--------------------------------- + +.. automodule:: homeassistant.helpers.translation + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.typing +---------------------------- + +.. automodule:: homeassistant.helpers.typing + :members: + :undoc-members: + :show-inheritance: + +homeassistant.helpers.update\_coordinator +----------------------------------------- + +.. automodule:: homeassistant.helpers.update_coordinator + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/api/homeassistant.rst b/docs/source/api/homeassistant.rst deleted file mode 100644 index 599f5fb8019..00000000000 --- a/docs/source/api/homeassistant.rst +++ /dev/null @@ -1,70 +0,0 @@ -homeassistant package -===================== - -Subpackages ------------ - -.. toctree:: - - helpers - util - -Submodules ----------- - -bootstrap module ------------------------------- - -.. automodule:: homeassistant.bootstrap - :members: - :undoc-members: - :show-inheritance: - -config module ---------------------------- - -.. automodule:: homeassistant.config - :members: - :undoc-members: - :show-inheritance: - -const module --------------------------- - -.. automodule:: homeassistant.const - :members: - :undoc-members: - :show-inheritance: - -core module -------------------------- - -.. automodule:: homeassistant.core - :members: - :undoc-members: - :show-inheritance: - -exceptions module -------------------------------- - -.. automodule:: homeassistant.exceptions - :members: - :undoc-members: - :show-inheritance: - -loader module ---------------------------- - -.. automodule:: homeassistant.loader - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- - -.. automodule:: homeassistant - :members: - :undoc-members: - :show-inheritance: diff --git a/docs/source/api/loader.rst b/docs/source/api/loader.rst new file mode 100644 index 00000000000..91594a8a774 --- /dev/null +++ b/docs/source/api/loader.rst @@ -0,0 +1,7 @@ +.. _loader_module: + +:mod:`homeassistant.loader` +--------------------------- + +.. automodule:: homeassistant.loader + :members: diff --git a/docs/source/api/util.rst b/docs/source/api/util.rst index fb61cd94fe6..52ae8eacdd3 100644 --- a/docs/source/api/util.rst +++ b/docs/source/api/util.rst @@ -1,86 +1,159 @@ -homeassistant.util package -========================== - -Submodules ----------- - -homeassistant.util.async_ module -------------------------------- - -.. automodule:: homeassistant.util.async_ - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.color module -------------------------------- - -.. automodule:: homeassistant.util.color - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.distance module ----------------------------------- - -.. automodule:: homeassistant.util.distance - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.dt module ----------------------------- - -.. automodule:: homeassistant.util.dt - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.location module ----------------------------------- - -.. automodule:: homeassistant.util.location - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.package module ---------------------------------- - -.. automodule:: homeassistant.util.package - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.temperature module -------------------------------------- - -.. automodule:: homeassistant.util.temperature - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.unit_system module -------------------------------------- - -.. automodule:: homeassistant.util.unit_system - :members: - :undoc-members: - :show-inheritance: - -homeassistant.util.yaml module ------------------------------- - -.. automodule:: homeassistant.util.yaml - :members: - :undoc-members: - :show-inheritance: - - -Module contents ---------------- +:mod:`homeassistant.util` +========================= .. automodule:: homeassistant.util - :members: - :undoc-members: - :show-inheritance: + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.yaml +----------------------- + +.. automodule:: homeassistant.util.yaml + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.aiohttp +-------------------------- + +.. automodule:: homeassistant.util.aiohttp + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.async\_ +-------------------------- + +.. automodule:: homeassistant.util.async_ + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.color +------------------------ + +.. automodule:: homeassistant.util.color + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.decorator +---------------------------- + +.. automodule:: homeassistant.util.decorator + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.distance +--------------------------- + +.. automodule:: homeassistant.util.distance + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.dt +--------------------- + +.. automodule:: homeassistant.util.dt + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.json +----------------------- + +.. automodule:: homeassistant.util.json + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.location +--------------------------- + +.. automodule:: homeassistant.util.location + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.logging +-------------------------- + +.. automodule:: homeassistant.util.logging + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.network +-------------------------- + +.. automodule:: homeassistant.util.network + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.package +-------------------------- + +.. automodule:: homeassistant.util.package + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.pil +---------------------- + +.. automodule:: homeassistant.util.pil + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.pressure +--------------------------- + +.. automodule:: homeassistant.util.pressure + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.ruamel\_yaml +------------------------------- + +.. automodule:: homeassistant.util.ruamel_yaml + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.ssl +---------------------- + +.. automodule:: homeassistant.util.ssl + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.temperature +------------------------------ + +.. automodule:: homeassistant.util.temperature + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.unit\_system +------------------------------- + +.. automodule:: homeassistant.util.unit_system + :members: + :undoc-members: + :show-inheritance: + +homeassistant.util.volume +------------------------- + +.. automodule:: homeassistant.util.volume + :members: + :undoc-members: + :show-inheritance: diff --git a/docs/source/conf.py b/docs/source/conf.py index f36b5b8124a..3aa30965c95 100644 --- a/docs/source/conf.py +++ b/docs/source/conf.py @@ -26,7 +26,7 @@ from homeassistant.const import __short_version__, __version__ PROJECT_NAME = 'Home Assistant' PROJECT_PACKAGE_NAME = 'homeassistant' PROJECT_AUTHOR = 'The Home Assistant Authors' -PROJECT_COPYRIGHT = ' 2013-2018, {}'.format(PROJECT_AUTHOR) +PROJECT_COPYRIGHT = ' 2013-2020, {}'.format(PROJECT_AUTHOR) PROJECT_LONG_DESCRIPTION = ('Home Assistant is an open-source ' 'home automation platform running on Python 3. ' 'Track and control all devices at home and ' diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index 5214c8cbc3c..0ae91ad5591 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -58,11 +58,10 @@ class AbstractOAuth2Implementation(ABC): Pass external data in with: - ```python await hass.config_entries.flow.async_configure( flow_id=flow_id, user_input=external_data ) - ``` + """ @abstractmethod diff --git a/requirements_docs.txt b/requirements_docs.txt index a27f3a4a306..17b38d6ebc3 100644 --- a/requirements_docs.txt +++ b/requirements_docs.txt @@ -1,3 +1,3 @@ -Sphinx==2.3.1 +Sphinx==2.4.4 sphinx-autodoc-typehints==1.10.3 sphinx-autodoc-annotation==1.0.post1 \ No newline at end of file From 8f4d3146c14c3c5036a2acd890b446045a9c9314 Mon Sep 17 00:00:00 2001 From: Pascal Vizeli Date: Wed, 25 Mar 2020 21:43:29 +0100 Subject: [PATCH 257/431] Revert "Fix issue with smhi-pkg" (#33259) * Revert "Fix issue with smhi-pkg (#33248)" This reverts commit 6990c70123417de46859e33199113d712b45311b. * Bump version to 1.0.13 --- azure-pipelines-ci.yml | 4 ++-- azure-pipelines-wheels.yml | 2 +- homeassistant/components/smhi/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/azure-pipelines-ci.yml b/azure-pipelines-ci.yml index 745330c630a..8fb014f80a7 100644 --- a/azure-pipelines-ci.yml +++ b/azure-pipelines-ci.yml @@ -127,7 +127,7 @@ stages: . venv/bin/activate pip install -U pip setuptools pytest-azurepipelines pytest-xdist -c homeassistant/package_constraints.txt - pip install --no-binary smhi-pkg -r requirements_test_all.txt -c homeassistant/package_constraints.txt + pip install -r requirements_test_all.txt -c homeassistant/package_constraints.txt # This is a TEMP. Eventually we should make sure our 4 dependencies drop typing. # Find offending deps with `pipdeptree -r -p typing` pip uninstall -y typing @@ -171,7 +171,7 @@ stages: . venv/bin/activate pip install -U pip setuptools wheel - pip install --no-binary smhi-pkg -r requirements_all.txt -c homeassistant/package_constraints.txt + pip install -r requirements_all.txt -c homeassistant/package_constraints.txt pip install -r requirements_test.txt -c homeassistant/package_constraints.txt - script: | . venv/bin/activate diff --git a/azure-pipelines-wheels.yml b/azure-pipelines-wheels.yml index 77ca2a7a866..b4ad0a556b2 100644 --- a/azure-pipelines-wheels.yml +++ b/azure-pipelines-wheels.yml @@ -33,7 +33,7 @@ jobs: builderVersion: '$(versionWheels)' builderApk: 'build-base;cmake;git;linux-headers;bluez-dev;libffi-dev;openssl-dev;glib-dev;eudev-dev;libxml2-dev;libxslt-dev;libpng-dev;libjpeg-turbo-dev;tiff-dev;autoconf;automake;cups-dev;gmp-dev;mpfr-dev;mpc1-dev;ffmpeg-dev;gammu-dev' builderPip: 'Cython;numpy' - skipBinary: 'aiohttp,smhi-pkg' + skipBinary: 'aiohttp' wheelsRequirement: 'requirements_wheels.txt' wheelsRequirementDiff: 'requirements_diff.txt' wheelsConstraint: 'homeassistant/package_constraints.txt' diff --git a/homeassistant/components/smhi/manifest.json b/homeassistant/components/smhi/manifest.json index e6bb655dc59..af8c64ac06f 100644 --- a/homeassistant/components/smhi/manifest.json +++ b/homeassistant/components/smhi/manifest.json @@ -3,7 +3,7 @@ "name": "SMHI", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/smhi", - "requirements": ["smhi-pkg==1.0.10"], + "requirements": ["smhi-pkg==1.0.13"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 7defeea4c9c..fd841669bf0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1896,7 +1896,7 @@ smarthab==0.20 # smbus-cffi==0.5.1 # homeassistant.components.smhi -smhi-pkg==1.0.10 +smhi-pkg==1.0.13 # homeassistant.components.snapcast snapcast==2.0.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ff686fe5e30..b5fef60303e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -683,7 +683,7 @@ simplisafe-python==9.0.5 sleepyq==0.7 # homeassistant.components.smhi -smhi-pkg==1.0.10 +smhi-pkg==1.0.13 # homeassistant.components.solaredge solaredge==0.0.2 From 5650b390b9f39121e1e94f8ff4757e2975fdecc8 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Wed, 25 Mar 2020 18:03:26 -0500 Subject: [PATCH 258/431] Schedule Unifi shutdown callback earlier (#33257) --- homeassistant/components/unifi/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/unifi/__init__.py b/homeassistant/components/unifi/__init__.py index 27a11760461..e9f534360d7 100644 --- a/homeassistant/components/unifi/__init__.py +++ b/homeassistant/components/unifi/__init__.py @@ -40,6 +40,8 @@ async def async_setup_entry(hass, config_entry): controller_id = get_controller_id_from_config_entry(config_entry) hass.data[DOMAIN][controller_id] = controller + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) + if controller.mac is None: return True @@ -53,8 +55,6 @@ async def async_setup_entry(hass, config_entry): # sw_version=config.raw['swversion'], ) - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, controller.shutdown) - return True From 815d153e5568a1914f87db97291e58c0ffcc2a11 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 26 Mar 2020 00:24:28 +0100 Subject: [PATCH 259/431] Bump gios library to version 0.0.5 (#33266) --- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index a48ec7b5c2f..67fcbebe9a2 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gios", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["gios==0.0.4"], + "requirements": ["gios==0.0.5"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index fd841669bf0..b6683294d85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -611,7 +611,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.4 +gios==0.0.5 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b5fef60303e..35c8a868b5d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -240,7 +240,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.4 +gios==0.0.5 # homeassistant.components.glances glances_api==0.2.0 From 6c4b4ad1e0586a49e813d1378ca129765fd7e086 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Thu, 26 Mar 2020 00:47:06 +0000 Subject: [PATCH 260/431] [ci skip] Translation update --- .../airvisual/.translations/no.json | 2 +- .../components/directv/.translations/de.json | 3 +- .../components/directv/.translations/lb.json | 20 ++++++++++- .../components/doorbird/.translations/de.json | 30 ++++++++++++++++ .../components/doorbird/.translations/es.json | 6 ++-- .../components/freebox/.translations/lb.json | 25 +++++++++++++ .../components/harmony/.translations/lb.json | 4 ++- .../components/icloud/.translations/lb.json | 3 +- .../monoprice/.translations/ca.json | 15 ++++++++ .../monoprice/.translations/de.json | 35 +++++++++++++++++++ .../monoprice/.translations/es.json | 17 ++++++++- .../monoprice/.translations/lb.json | 15 ++++++++ .../monoprice/.translations/zh-Hant.json | 15 ++++++++ .../components/nuheat/.translations/de.json | 23 ++++++++++++ .../components/nuheat/.translations/es.json | 6 ++-- .../pvpc_hourly_pricing/.translations/de.json | 14 ++++++++ .../pvpc_hourly_pricing/.translations/es.json | 2 +- .../components/rachio/.translations/lb.json | 7 ++++ .../components/roku/.translations/lb.json | 1 + .../simplisafe/.translations/lb.json | 10 ++++++ .../components/toon/.translations/no.json | 2 +- .../components/vizio/.translations/lb.json | 2 ++ 22 files changed, 243 insertions(+), 14 deletions(-) create mode 100644 homeassistant/components/doorbird/.translations/de.json create mode 100644 homeassistant/components/freebox/.translations/lb.json create mode 100644 homeassistant/components/monoprice/.translations/de.json create mode 100644 homeassistant/components/nuheat/.translations/de.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/de.json diff --git a/homeassistant/components/airvisual/.translations/no.json b/homeassistant/components/airvisual/.translations/no.json index de2991f0757..7c0b540f16a 100644 --- a/homeassistant/components/airvisual/.translations/no.json +++ b/homeassistant/components/airvisual/.translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Denne API-n\u00f8kkelen er allerede i bruk." + "already_configured": "Disse koordinatene er allerede registrert." }, "error": { "invalid_api_key": "Ugyldig API-n\u00f8kkel" diff --git a/homeassistant/components/directv/.translations/de.json b/homeassistant/components/directv/.translations/de.json index 6482216a67c..4fecc58dafb 100644 --- a/homeassistant/components/directv/.translations/de.json +++ b/homeassistant/components/directv/.translations/de.json @@ -14,7 +14,8 @@ "data": { "one": "eins", "other": "andere" - } + }, + "description": "M\u00f6chten Sie {name} einrichten?" }, "user": { "data": { diff --git a/homeassistant/components/directv/.translations/lb.json b/homeassistant/components/directv/.translations/lb.json index 3cd7d7e20cd..4a0c1267d2b 100644 --- a/homeassistant/components/directv/.translations/lb.json +++ b/homeassistant/components/directv/.translations/lb.json @@ -3,6 +3,24 @@ "abort": { "already_configured": "DirecTV ass scho konfigur\u00e9iert", "unknown": "Onerwaarte Feeler" - } + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "Soll {name} konfigur\u00e9iert ginn?", + "title": "Mam DirecTV Receiver verbannen" + }, + "user": { + "data": { + "host": "Numm oder IP Adresse" + }, + "title": "Mam DirecTV Receiver verbannen" + } + }, + "title": "DirecTV" } } \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/de.json b/homeassistant/components/doorbird/.translations/de.json new file mode 100644 index 00000000000..4582c469cb9 --- /dev/null +++ b/homeassistant/components/doorbird/.translations/de.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "host": "Host (IP-Adresse)", + "name": "Ger\u00e4tename", + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Stellen Sie eine Verbindung zu DoorBird her" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Durch Kommas getrennte Liste von Ereignissen." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/es.json b/homeassistant/components/doorbird/.translations/es.json index a7cddd18582..e7cf75f38fb 100644 --- a/homeassistant/components/doorbird/.translations/es.json +++ b/homeassistant/components/doorbird/.translations/es.json @@ -4,7 +4,7 @@ "already_configured": "DoorBird ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "cannot_connect": "No se pudo conectar, por favor int\u00e9ntalo de nuevo", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, @@ -16,7 +16,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "title": "Con\u00e9ctese a DoorBird" + "title": "Conectar con DoorBird" } }, "title": "DoorBird" @@ -27,7 +27,7 @@ "data": { "events": "Lista de eventos separados por comas." }, - "description": "Agregue un nombre de evento separado por comas para cada evento del que desee realizar un seguimiento. Despu\u00e9s de introducirlos aqu\u00ed, utilice la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulte la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, movimiento" + "description": "A\u00f1ade un nombre de evento separado por comas para cada evento del que deseas realizar un seguimiento. Despu\u00e9s de introducirlos aqu\u00ed, utiliza la aplicaci\u00f3n DoorBird para asignarlos a un evento espec\u00edfico. Consulta la documentaci\u00f3n en https://www.home-assistant.io/integrations/doorbird/#events. Ejemplo: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/freebox/.translations/lb.json b/homeassistant/components/freebox/.translations/lb.json new file mode 100644 index 00000000000..fdf08c0a5fe --- /dev/null +++ b/homeassistant/components/freebox/.translations/lb.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "connection_failed": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "register_failed": "Feeler beim registr\u00e9ieren, prob\u00e9ier w.e.g. nach emol", + "unknown": "Onbekannte Feeler: prob\u00e9iertsp\u00e9ider nach emol" + }, + "step": { + "link": { + "title": "Freebox Router verbannen" + }, + "user": { + "data": { + "host": "Apparat", + "port": "Port" + }, + "title": "Freebox" + } + }, + "title": "Freebox" + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/lb.json b/homeassistant/components/harmony/.translations/lb.json index 8401853fd57..64536a01407 100644 --- a/homeassistant/components/harmony/.translations/lb.json +++ b/homeassistant/components/harmony/.translations/lb.json @@ -28,8 +28,10 @@ "step": { "init": { "data": { + "activity": "Standard Aktivit\u00e9it d\u00e9i ausgef\u00e9iert g\u00ebtt wann keng uginn ass.", "delay_secs": "Delai zw\u00ebschen dem versch\u00e9cken vun Kommandoen" - } + }, + "description": "Harmony Hub Optioune ajust\u00e9ieren" } } } diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json index f90ec545c39..8ecc49a5ad9 100644 --- a/homeassistant/components/icloud/.translations/lb.json +++ b/homeassistant/components/icloud/.translations/lb.json @@ -19,7 +19,8 @@ "user": { "data": { "password": "Passwuert", - "username": "E-Mail" + "username": "E-Mail", + "with_family": "Mat der Famill" }, "description": "F\u00ebllt \u00e4r Umeldungs Informatiounen aus", "title": "iCloud Umeldungs Informatiounen" diff --git a/homeassistant/components/monoprice/.translations/ca.json b/homeassistant/components/monoprice/.translations/ca.json index b4cd7143fc9..ce766671763 100644 --- a/homeassistant/components/monoprice/.translations/ca.json +++ b/homeassistant/components/monoprice/.translations/ca.json @@ -22,5 +22,20 @@ } }, "title": "Amplificador Monoprice de 6 zones" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nom de la font #1", + "source_2": "Nom de la font #2", + "source_3": "Nom de la font #3", + "source_4": "Nom de la font #4", + "source_5": "Nom de la font #5", + "source_6": "Nom de la font #6" + }, + "title": "Configuraci\u00f3 de les fonts" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/de.json b/homeassistant/components/monoprice/.translations/de.json new file mode 100644 index 00000000000..176b1f5c1ac --- /dev/null +++ b/homeassistant/components/monoprice/.translations/de.json @@ -0,0 +1,35 @@ +{ + "config": { + "error": { + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "source_1": "Name der Quelle #1", + "source_2": "Name der Quelle #2", + "source_3": "Name der Quelle #3", + "source_4": "Name der Quelle #4", + "source_5": "Name der Quelle #5", + "source_6": "Name der Quelle #6" + }, + "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Name der Quelle #1", + "source_2": "Name der Quelle #2", + "source_3": "Name der Quelle #3", + "source_4": "Name der Quelle #4", + "source_5": "Name der Quelle #5", + "source_6": "Name der Quelle #6" + }, + "title": "Quellen konfigurieren" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/es.json b/homeassistant/components/monoprice/.translations/es.json index 1996d116f76..31a72fc0b9f 100644 --- a/homeassistant/components/monoprice/.translations/es.json +++ b/homeassistant/components/monoprice/.translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntelo de nuevo.", + "cannot_connect": "No se pudo conectar, por favor int\u00e9ntalo de nuevo", "unknown": "Error inesperado" }, "step": { @@ -22,5 +22,20 @@ } }, "title": "Amplificador Monoprice de 6 zonas" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nombre de la fuente #1", + "source_2": "Nombre de la fuente #2", + "source_3": "Nombre de la fuente #3", + "source_4": "Nombre de la fuente #4", + "source_5": "Nombre de la fuente #5", + "source_6": "Nombre de la fuente #6" + }, + "title": "Configurar fuentes" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/lb.json b/homeassistant/components/monoprice/.translations/lb.json index 9b1ef75ef1d..6f530fa8a80 100644 --- a/homeassistant/components/monoprice/.translations/lb.json +++ b/homeassistant/components/monoprice/.translations/lb.json @@ -21,5 +21,20 @@ "title": "Mam Apparat verbannen" } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Numm vun der Quell #1", + "source_2": "Numm vun der Quell #2", + "source_3": "Numm vun der Quell #3", + "source_4": "Numm vun der Quell #4", + "source_5": "Numm vun der Quell #5", + "source_6": "Numm vun der Quell #6" + }, + "title": "Quelle konfigur\u00e9ieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/zh-Hant.json b/homeassistant/components/monoprice/.translations/zh-Hant.json index b37ab4158a0..81230eee728 100644 --- a/homeassistant/components/monoprice/.translations/zh-Hant.json +++ b/homeassistant/components/monoprice/.translations/zh-Hant.json @@ -22,5 +22,20 @@ } }, "title": "Monoprice 6-Zone \u653e\u5927\u5668" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "\u4f86\u6e90 #1 \u540d\u7a31", + "source_2": "\u4f86\u6e90 #2 \u540d\u7a31", + "source_3": "\u4f86\u6e90 #3 \u540d\u7a31", + "source_4": "\u4f86\u6e90 #4 \u540d\u7a31", + "source_5": "\u4f86\u6e90 #5 \u540d\u7a31", + "source_6": "\u4f86\u6e90 #6 \u540d\u7a31" + }, + "title": "\u8a2d\u5b9a\u4f86\u6e90" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/de.json b/homeassistant/components/nuheat/.translations/de.json new file mode 100644 index 00000000000..358b5a76254 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/de.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "Der Thermostat ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "invalid_thermostat": "Die Seriennummer des Thermostats ist ung\u00fcltig.", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "password": "Passwort", + "serial_number": "Seriennummer des Thermostats.", + "username": "Benutzername" + } + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/es.json b/homeassistant/components/nuheat/.translations/es.json index 70e4b03ca70..3a37b65b9dd 100644 --- a/homeassistant/components/nuheat/.translations/es.json +++ b/homeassistant/components/nuheat/.translations/es.json @@ -4,7 +4,7 @@ "already_configured": "El termostato ya est\u00e1 configurado." }, "error": { - "cannot_connect": "No se pudo conectar, por favor int\u00e9ntelo de nuevo", + "cannot_connect": "No se pudo conectar, por favor int\u00e9ntalo de nuevo", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "invalid_thermostat": "El n\u00famero de serie del termostato no es v\u00e1lido.", "unknown": "Error inesperado" @@ -16,8 +16,8 @@ "serial_number": "N\u00famero de serie del termostato.", "username": "Nombre de usuario" }, - "description": "Deber\u00e1 obtener el n\u00famero de serie o el ID de su termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando su(s) termostato(s).", - "title": "Con\u00e9ctese a NuHeat" + "description": "Necesitas obtener el n\u00famero de serie o el ID de tu termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando tu(s) termostato(s).", + "title": "ConectarNuHeat" } }, "title": "NuHeat" diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/de.json b/homeassistant/components/pvpc_hourly_pricing/.translations/de.json new file mode 100644 index 00000000000..f8bf787b685 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/de.json @@ -0,0 +1,14 @@ +{ + "config": { + "step": { + "user": { + "data": { + "name": "Sensorname", + "tariff": "Vertragstarif (1, 2 oder 3 Perioden)" + }, + "title": "Tarifauswahl" + } + }, + "title": "St\u00fcndlicher Strompreis in Spanien (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/es.json b/homeassistant/components/pvpc_hourly_pricing/.translations/es.json index 53617a3c83d..8951c46b75d 100644 --- a/homeassistant/components/pvpc_hourly_pricing/.translations/es.json +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/es.json @@ -9,7 +9,7 @@ "name": "Nombre del sensor", "tariff": "Tarifa contratada (1, 2 o 3 per\u00edodos)" }, - "description": "Este sensor utiliza la API oficial para obtener [precios por hora de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visite [integration docs](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSeleccione la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", + "description": "Este sensor utiliza la API oficial para obtener [el precio horario de la electricidad (PVPC)](https://www.esios.ree.es/es/pvpc) en Espa\u00f1a.\nPara obtener una explicaci\u00f3n m\u00e1s precisa, visita los [documentos de la integraci\u00f3n](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelecciona la tarifa contratada en funci\u00f3n del n\u00famero de per\u00edodos de facturaci\u00f3n por d\u00eda:\n- 1 per\u00edodo: normal\n- 2 per\u00edodos: discriminaci\u00f3n (tarifa nocturna)\n- 3 per\u00edodos: coche el\u00e9ctrico (tarifa nocturna de 3 per\u00edodos)", "title": "Selecci\u00f3n de tarifa" } }, diff --git a/homeassistant/components/rachio/.translations/lb.json b/homeassistant/components/rachio/.translations/lb.json index b6180e4ffce..24c1b8c382d 100644 --- a/homeassistant/components/rachio/.translations/lb.json +++ b/homeassistant/components/rachio/.translations/lb.json @@ -1,5 +1,12 @@ { "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun" + }, "step": { "user": { "title": "Mam Rachio Apparat verbannen" diff --git a/homeassistant/components/roku/.translations/lb.json b/homeassistant/components/roku/.translations/lb.json index 9808f207f30..2e532bf6a93 100644 --- a/homeassistant/components/roku/.translations/lb.json +++ b/homeassistant/components/roku/.translations/lb.json @@ -11,6 +11,7 @@ "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { + "description": "Soll {name} konfigur\u00e9iert ginn?", "title": "Roku" }, "user": { diff --git a/homeassistant/components/simplisafe/.translations/lb.json b/homeassistant/components/simplisafe/.translations/lb.json index c0e9faf08f6..81f4c82fcc7 100644 --- a/homeassistant/components/simplisafe/.translations/lb.json +++ b/homeassistant/components/simplisafe/.translations/lb.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Code (den am Home Assistant Interface benotzt g\u00ebtt)" + }, + "title": "Simplisafe konfigur\u00e9ieren" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/toon/.translations/no.json b/homeassistant/components/toon/.translations/no.json index a033d2954d9..ed2af3ac379 100644 --- a/homeassistant/components/toon/.translations/no.json +++ b/homeassistant/components/toon/.translations/no.json @@ -5,7 +5,7 @@ "client_secret": "Klient hemmeligheten fra konfigurasjonen er ugyldig.", "no_agreements": "Denne kontoen har ingen Toon skjermer.", "no_app": "Du m\u00e5 konfigurere Toon f\u00f8r du kan autentisere den. [Vennligst les instruksjonene](https://www.home-assistant.io/components/toon/).", - "unknown_auth_fail": "Uventet feil oppstod under autentisering." + "unknown_auth_fail": "Det oppstod en uventet feil under godkjenning." }, "error": { "credentials": "Den oppgitte kontoinformasjonen er ugyldig.", diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json index 11df333ce4b..5788776049d 100644 --- a/homeassistant/components/vizio/.translations/lb.json +++ b/homeassistant/components/vizio/.translations/lb.json @@ -50,6 +50,8 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", + "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?", "timeout": "Z\u00e4itiwwerscheidung bei der Ufro vun der API (sekonnen)", "volume_step": "Lautst\u00e4erkt Schr\u00ebtt Gr\u00e9isst" }, From c267946284a77eec0c2aa2b4de956f81b69c9d0a Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 26 Mar 2020 12:58:11 +0100 Subject: [PATCH 261/431] Upgrade discord.py to 1.3.2 (#33280) --- homeassistant/components/discord/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/discord/manifest.json b/homeassistant/components/discord/manifest.json index e496ad0d532..939138bf999 100644 --- a/homeassistant/components/discord/manifest.json +++ b/homeassistant/components/discord/manifest.json @@ -2,7 +2,7 @@ "domain": "discord", "name": "Discord", "documentation": "https://www.home-assistant.io/integrations/discord", - "requirements": ["discord.py==1.3.1"], + "requirements": ["discord.py==1.3.2"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index b6683294d85..fd26c9aa66a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -453,7 +453,7 @@ directpy==0.7 discogs_client==2.2.2 # homeassistant.components.discord -discord.py==1.3.1 +discord.py==1.3.2 # homeassistant.components.updater distro==1.4.0 From b598ff94d1d376b0414f37a8dab3c73e62d2aafe Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 26 Mar 2020 16:12:12 +0100 Subject: [PATCH 262/431] Upgrade jinja2 to >=2.11.1 (#33244) --- homeassistant/package_constraints.txt | 2 +- requirements_all.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 4b16757a361..8547aa9d510 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -14,7 +14,7 @@ distro==1.4.0 hass-nabucasa==0.32.2 home-assistant-frontend==20200318.1 importlib-metadata==1.5.0 -jinja2>=2.10.3 +jinja2>=2.11.1 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index fd26c9aa66a..c81f4ef5eec 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -7,7 +7,7 @@ bcrypt==3.1.7 certifi>=2019.11.28 ciso8601==2.1.3 importlib-metadata==1.5.0 -jinja2>=2.10.3 +jinja2>=2.11.1 PyJWT==1.7.1 cryptography==2.8 pip>=8.0.3 diff --git a/setup.py b/setup.py index 7794a177b1f..b8bf55d9f74 100755 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ REQUIRES = [ "certifi>=2019.11.28", "ciso8601==2.1.3", "importlib-metadata==1.5.0", - "jinja2>=2.10.3", + "jinja2>=2.11.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==2.8", From 558cccc68cc7fd20451f87af560234488586583e Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Thu, 26 Mar 2020 16:14:07 +0100 Subject: [PATCH 263/431] Upgrade pyyaml to 5.3.1 (#33246) --- homeassistant/package_constraints.txt | 2 +- requirements_all.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 8547aa9d510..26571ab7a27 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -19,7 +19,7 @@ netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.3 +pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.15 diff --git a/requirements_all.txt b/requirements_all.txt index c81f4ef5eec..eb6820b81aa 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -13,7 +13,7 @@ cryptography==2.8 pip>=8.0.3 python-slugify==4.0.0 pytz>=2019.03 -pyyaml==5.3 +pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 voluptuous==0.11.7 diff --git a/setup.py b/setup.py index b8bf55d9f74..e0daacd98bf 100755 --- a/setup.py +++ b/setup.py @@ -47,7 +47,7 @@ REQUIRES = [ "pip>=8.0.3", "python-slugify==4.0.0", "pytz>=2019.03", - "pyyaml==5.3", + "pyyaml==5.3.1", "requests==2.23.0", "ruamel.yaml==0.15.100", "voluptuous==0.11.7", From a38db1f677d1768611ff5eae62f4c1bb7bd54095 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Fri, 27 Mar 2020 05:05:48 +1100 Subject: [PATCH 264/431] Fix GDACS integration to remove stale entities from entity registry (#33283) * remove entities from entity registry * added test for removing entities from entity registry --- homeassistant/components/gdacs/geo_location.py | 4 ++++ tests/components/gdacs/test_geo_location.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index 31c3ba4138c..f434645ca20 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -11,6 +11,7 @@ from homeassistant.const import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util.unit_system import IMPERIAL_SYSTEM from .const import DEFAULT_ICON, DOMAIN, FEED @@ -107,6 +108,9 @@ class GdacsEvent(GeolocationEvent): """Call when entity will be removed from hass.""" self._remove_signal_delete() self._remove_signal_update() + # Remove from entity registry. + entity_registry = await async_get_registry(self.hass) + entity_registry.async_remove(self.entity_id) @callback def _delete_callback(self): diff --git a/tests/components/gdacs/test_geo_location.py b/tests/components/gdacs/test_geo_location.py index c426b081e21..255a2c50946 100644 --- a/tests/components/gdacs/test_geo_location.py +++ b/tests/components/gdacs/test_geo_location.py @@ -29,6 +29,7 @@ from homeassistant.const import ( CONF_RADIUS, EVENT_HOMEASSISTANT_START, ) +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import IMPERIAL_SYSTEM @@ -97,6 +98,8 @@ async def test_setup(hass): all_states = hass.states.async_all() # 3 geolocation and 1 sensor entities assert len(all_states) == 4 + entity_registry = await async_get_registry(hass) + assert len(entity_registry.entities) == 4 state = hass.states.get("geo_location.drought_name_1") assert state is not None @@ -184,6 +187,7 @@ async def test_setup(hass): all_states = hass.states.async_all() assert len(all_states) == 1 + assert len(entity_registry.entities) == 1 async def test_setup_imperial(hass): From 3dc6612cd9acae1f0cd5e471a422939c0a3dd203 Mon Sep 17 00:00:00 2001 From: Kris Bennett <1435262+i00@users.noreply.github.com> Date: Fri, 27 Mar 2020 04:31:23 +1000 Subject: [PATCH 265/431] Add Android TV cover art (screen content) (#33232) * Android TV * Android TV * Android TV * Android TV * Android TV * Android TV * Android TV * Android TV * Android TV --- .../components/androidtv/media_player.py | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/homeassistant/components/androidtv/media_player.py b/homeassistant/components/androidtv/media_player.py index 93666958919..f9ec68c8742 100644 --- a/homeassistant/components/androidtv/media_player.py +++ b/homeassistant/components/androidtv/media_player.py @@ -1,4 +1,6 @@ """Support for functionality to interact with Android TV / Fire TV devices.""" +import binascii +from datetime import datetime import functools import logging import os @@ -475,6 +477,34 @@ class ADBDevice(MediaPlayerDevice): """Return the device unique id.""" return self._unique_id + async def async_get_media_image(self): + """Fetch current playing image.""" + if self.state in [STATE_OFF, None] or not self.available: + return None, None + + media_data = await self.hass.async_add_executor_job(self.get_raw_media_data) + if media_data: + return media_data, "image/png" + return None, None + + @adb_decorator() + def get_raw_media_data(self): + """Raw base64 image data.""" + try: + response = self.aftv.adb_shell("screencap -p | base64") + except UnicodeDecodeError: + return None + + if isinstance(response, str) and response.strip(): + return binascii.a2b_base64(response.strip().replace("\n", "")) + + return None + + @property + def media_image_hash(self): + """Hash value for media image.""" + return f"{datetime.now().timestamp()}" + @adb_decorator() def media_play(self): """Send play command.""" From 6c6318d18f00539fc98036789523a87f8a6806ee Mon Sep 17 00:00:00 2001 From: Jaryl Chng Date: Fri, 27 Mar 2020 03:55:00 +0800 Subject: [PATCH 266/431] Update PySwitchbot to 0.8.0 and add password configuration option (#33252) * Update PySwitchbot to 0.8.0 and added password configuration * Removed unchanged retry_count attribute * Fix dependencies and formatting * Removed default value for password and use config.get instead --- homeassistant/components/switchbot/manifest.json | 2 +- homeassistant/components/switchbot/switch.py | 10 ++++++---- requirements_all.txt | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 4193a88e3ed..b076b254b9f 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.6.2"], + "requirements": ["PySwitchbot==0.8.0"], "dependencies": [], "codeowners": ["@danielhiversen"] } diff --git a/homeassistant/components/switchbot/switch.py b/homeassistant/components/switchbot/switch.py index ed7fba570a8..f0cbecc8968 100644 --- a/homeassistant/components/switchbot/switch.py +++ b/homeassistant/components/switchbot/switch.py @@ -7,7 +7,7 @@ import switchbot import voluptuous as vol from homeassistant.components.switch import PLATFORM_SCHEMA, SwitchDevice -from homeassistant.const import CONF_MAC, CONF_NAME +from homeassistant.const import CONF_MAC, CONF_NAME, CONF_PASSWORD import homeassistant.helpers.config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity @@ -19,6 +19,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_MAC): cv.string, vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_PASSWORD): cv.string, } ) @@ -27,20 +28,21 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Perform the setup for Switchbot devices.""" name = config.get(CONF_NAME) mac_addr = config[CONF_MAC] - add_entities([SwitchBot(mac_addr, name)]) + password = config.get(CONF_PASSWORD) + add_entities([SwitchBot(mac_addr, name, password)]) class SwitchBot(SwitchDevice, RestoreEntity): """Representation of a Switchbot.""" - def __init__(self, mac, name) -> None: + def __init__(self, mac, name, password) -> None: """Initialize the Switchbot.""" self._state = None self._last_run_success = None self._name = name self._mac = mac - self._device = switchbot.Switchbot(mac=mac) + self._device = switchbot.Switchbot(mac=mac, password=password) async def async_added_to_hass(self): """Run when entity about to be added.""" diff --git a/requirements_all.txt b/requirements_all.txt index eb6820b81aa..c70cca86e85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -72,7 +72,7 @@ PyRMVtransport==0.2.9 PySocks==1.7.1 # homeassistant.components.switchbot -# PySwitchbot==0.6.2 +# PySwitchbot==0.8.0 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 From f1e58d07847c183de40f2459b22a7a21a4bc9afa Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 26 Mar 2020 15:18:23 -0500 Subject: [PATCH 267/431] Handle empty Plex client username (#33271) --- homeassistant/components/plex/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 54a248309b6..788c96e15d2 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -171,7 +171,7 @@ class PlexServer: continue session_username = session.usernames[0] for player in session.players: - if session_username not in monitored_users: + if session_username and session_username not in monitored_users: ignored_clients.add(player.machineIdentifier) _LOGGER.debug("Ignoring Plex client owned by %s", session_username) continue From 262ed9ed2ab6a65e9bbe6c17336fec3b6d52e3ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20H=C3=B8yer=20Iversen?= Date: Thu, 26 Mar 2020 22:27:05 +0100 Subject: [PATCH 268/431] Upgrade tibber to 0.13.6 (#33293) --- homeassistant/components/tibber/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 78b358d70ec..48ff76a2b34 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.13.3"], + "requirements": ["pyTibber==0.13.6"], "dependencies": [], "codeowners": ["@danielhiversen"], "quality_scale": "silver" diff --git a/requirements_all.txt b/requirements_all.txt index c70cca86e85..4c001f0fa5b 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1134,7 +1134,7 @@ pyRFXtrx==0.25 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.13.3 +pyTibber==0.13.6 # homeassistant.components.dlink pyW215==0.6.0 From b8afb9277a0eb08d5f88232d6a7a02caf9c1fcaf Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2020 17:14:35 -0500 Subject: [PATCH 269/431] Bump nexia to 0.7.2 (#33292) * Bump nexia to 0.7.2 * Fixes zones on the same thermostat showing as active when the damper was closed. * Update test for nexia 0.7.2 --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/nexia/test_climate.py | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index aec1f7c3e7b..d98dbcb2272 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -2,7 +2,7 @@ "domain": "nexia", "name": "Nexia", "requirements": [ - "nexia==0.7.1" + "nexia==0.7.2" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 4c001f0fa5b..d1d6e2447ed 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.7.1 +nexia==0.7.2 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 35c8a868b5d..f94245b6df5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -354,7 +354,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.7.1 +nexia==0.7.2 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 diff --git a/tests/components/nexia/test_climate.py b/tests/components/nexia/test_climate.py index e7675ff68b1..7f3ed900d3c 100644 --- a/tests/components/nexia/test_climate.py +++ b/tests/components/nexia/test_climate.py @@ -71,8 +71,9 @@ async def test_climate_zones(hass): "target_temp_low": 17.2, "target_temp_step": 1.0, "temperature": None, - "zone_status": "", + "zone_status": "Idle", } + # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all( From f5a6c3484d1ecf4f1ffe4d5b7a471a08862aeffc Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2020 17:14:52 -0500 Subject: [PATCH 270/431] Abort rachio homekit config when one is already setup (#33285) * Abort rachio homekit config when one is already setup. We can see rachio on the network to tell them to configure it, but since the device will not give up the account it is bound to and there can be multiple rachio controllers on a single account, we avoid showing the device as discovered once they already have one configured as they can always add a new one via "+" * doc string freshness * doc string freshness * Update tests/components/rachio/test_config_flow.py Co-Authored-By: Franck Nijhof Co-authored-by: Franck Nijhof --- .../components/rachio/config_flow.py | 8 +++++++ tests/components/rachio/test_config_flow.py | 22 +++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 3a4c2a1c171..9eff7c99334 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -84,6 +84,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_homekit(self, homekit_info): """Handle HomeKit discovery.""" + if self._async_current_entries(): + # We can see rachio on the network to tell them to configure + # it, but since the device will not give up the account it is + # bound to and there can be multiple rachio systems on a single + # account, we avoid showing the device as discovered once + # they already have one configured as they can always + # add a new one via "+" + return self.async_abort(reason="already_configured") return await self.async_step_user() async def async_step_import(self, user_input): diff --git a/tests/components/rachio/test_config_flow.py b/tests/components/rachio/test_config_flow.py index f5df0817846..57575fe5501 100644 --- a/tests/components/rachio/test_config_flow.py +++ b/tests/components/rachio/test_config_flow.py @@ -10,6 +10,8 @@ from homeassistant.components.rachio.const import ( ) from homeassistant.const import CONF_API_KEY +from tests.common import MockConfigEntry + def _mock_rachio_return_value(get=None, getInfo=None): rachio_mock = MagicMock() @@ -102,3 +104,23 @@ async def test_form_cannot_connect(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_homekit(hass): + """Test that we abort from homekit if rachio is already setup.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "homekit"} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + entry = MockConfigEntry(domain=DOMAIN, data={CONF_API_KEY: "api_key"}) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "homekit"} + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" From f93e4e3de76734af53bfd3ed7b6cdf11d3bbf30f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 26 Mar 2020 17:15:35 -0500 Subject: [PATCH 271/431] Support homekit matches that have a dash after the model (#33274) --- homeassistant/components/zeroconf/__init__.py | 6 +++++- tests/components/zeroconf/test_init.py | 19 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 206f529344f..16a7d2f000c 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -132,7 +132,11 @@ def handle_homekit(hass, info) -> bool: return False for test_model in HOMEKIT: - if model != test_model and not model.startswith(test_model + " "): + if ( + model != test_model + and not model.startswith(test_model + " ") + and not model.startswith(test_model + "-") + ): continue hass.add_job( diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index a74c81ba307..4e086978be1 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -68,7 +68,7 @@ async def test_setup(hass, mock_zeroconf): assert len(mock_config_flow.mock_calls) == expected_flow_calls * 2 -async def test_homekit_match_partial(hass, mock_zeroconf): +async def test_homekit_match_partial_space(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True @@ -83,6 +83,23 @@ async def test_homekit_match_partial(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "lifx" +async def test_homekit_match_partial_dash(hass, mock_zeroconf): + """Test configured options for a device are loaded via config entry.""" + with patch.dict( + zc_gen.ZEROCONF, {zeroconf.HOMEKIT_TYPE: ["homekit_controller"]}, clear=True + ), patch.object(hass.config_entries, "flow") as mock_config_flow, patch.object( + zeroconf, "ServiceBrowser", side_effect=service_update_mock + ) as mock_service_browser: + mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + "Rachio-fa46ba" + ) + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 2 + assert mock_config_flow.mock_calls[0][1][0] == "rachio" + + async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( From a6d5ed01604d5f6dc813de43eb5881ba653171e9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 27 Mar 2020 00:23:46 +0100 Subject: [PATCH 272/431] Add support for dashboards to lovelace cast service (#32913) --- .../components/cast/home_assistant_cast.py | 10 ++++++- homeassistant/components/cast/media_player.py | 8 ++++-- homeassistant/components/cast/services.yaml | 3 +++ .../cast/test_home_assistant_cast.py | 27 ++++++++++++++++++- 4 files changed, 44 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/cast/home_assistant_cast.py b/homeassistant/components/cast/home_assistant_cast.py index 0b8633e1916..c933136d140 100644 --- a/homeassistant/components/cast/home_assistant_cast.py +++ b/homeassistant/components/cast/home_assistant_cast.py @@ -12,6 +12,7 @@ from .const import DOMAIN, SIGNAL_HASS_CAST_SHOW_VIEW SERVICE_SHOW_VIEW = "show_lovelace_view" ATTR_VIEW_PATH = "view_path" +ATTR_URL_PATH = "dashboard_path" async def async_setup_ha_cast( @@ -63,11 +64,18 @@ async def async_setup_ha_cast( controller, call.data[ATTR_ENTITY_ID], call.data[ATTR_VIEW_PATH], + call.data.get(ATTR_URL_PATH), ) hass.helpers.service.async_register_admin_service( DOMAIN, SERVICE_SHOW_VIEW, handle_show_view, - vol.Schema({ATTR_ENTITY_ID: cv.entity_id, ATTR_VIEW_PATH: str}), + vol.Schema( + { + ATTR_ENTITY_ID: cv.entity_id, + ATTR_VIEW_PATH: str, + vol.Optional(ATTR_URL_PATH): str, + } + ), ) diff --git a/homeassistant/components/cast/media_player.py b/homeassistant/components/cast/media_player.py index 4e259038f14..e0c48062dfb 100644 --- a/homeassistant/components/cast/media_player.py +++ b/homeassistant/components/cast/media_player.py @@ -948,7 +948,11 @@ class CastDevice(MediaPlayerDevice): await self._async_disconnect() def _handle_signal_show_view( - self, controller: HomeAssistantController, entity_id: str, view_path: str + self, + controller: HomeAssistantController, + entity_id: str, + view_path: str, + url_path: Optional[str], ): """Handle a show view signal.""" if entity_id != self.entity_id: @@ -958,4 +962,4 @@ class CastDevice(MediaPlayerDevice): self._hass_cast_controller = controller self._chromecast.register_handler(controller) - self._hass_cast_controller.show_lovelace_view(view_path) + self._hass_cast_controller.show_lovelace_view(view_path, url_path) diff --git a/homeassistant/components/cast/services.yaml b/homeassistant/components/cast/services.yaml index 24bc7b16a90..d1c29281aad 100644 --- a/homeassistant/components/cast/services.yaml +++ b/homeassistant/components/cast/services.yaml @@ -4,6 +4,9 @@ show_lovelace_view: entity_id: description: Media Player entity to show the Lovelace view on. example: "media_player.kitchen" + dashboard_path: + description: The url path of the Lovelace dashboard to show. + example: lovelace-cast view_path: description: The path of the Lovelace view to show. example: downstairs diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 10dd253704e..2ec02da7669 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -20,13 +20,38 @@ async def test_service_show_view(hass): ) assert len(calls) == 1 - controller, entity_id, view_path = calls[0] + controller, entity_id, view_path, url_path = calls[0] assert controller.hass_url == "http://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 entity_id == "media_player.kitchen" assert view_path == "mock_path" + assert url_path is None + + +async def test_service_show_view_dashboard(hass): + """Test casting a specific dashboard.""" + hass.config.api = Mock(base_url="http://example.com") + await home_assistant_cast.async_setup_ha_cast(hass, MockConfigEntry()) + calls = async_mock_signal(hass, home_assistant_cast.SIGNAL_HASS_CAST_SHOW_VIEW) + + await hass.services.async_call( + "cast", + "show_lovelace_view", + { + "entity_id": "media_player.kitchen", + "view_path": "mock_path", + "dashboard_path": "mock-dashboard", + }, + blocking=True, + ) + + assert len(calls) == 1 + _controller, entity_id, view_path, url_path = calls[0] + assert entity_id == "media_player.kitchen" + assert view_path == "mock_path" + assert url_path == "mock-dashboard" async def test_use_cloud_url(hass): From 867630a4a7ce4733bd764e8707229f01340e07fd Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Fri, 27 Mar 2020 00:33:50 +0100 Subject: [PATCH 273/431] Remove state when entity is removed from registry (#32184) --- homeassistant/helpers/entity.py | 1 + 1 file changed, 1 insertion(+) diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 6a238ff84b1..62d46500451 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -542,6 +542,7 @@ class Entity(ABC): data = event.data if data["action"] == "remove" and data["entity_id"] == self.entity_id: await self.async_removed_from_registry() + await self.async_remove() if ( data["action"] != "update" From c89975adf69fa4574980abe84d97808332cab7aa Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Fri, 27 Mar 2020 00:46:57 +0000 Subject: [PATCH 274/431] [ci skip] Translation update --- .../components/airvisual/.translations/de.json | 2 +- .../airvisual/.translations/zh-Hant.json | 2 +- .../ambient_station/.translations/fr.json | 3 +++ .../components/august/.translations/fr.json | 11 +++++++++-- .../components/doorbird/.translations/fr.json | 9 +++++++++ .../components/konnected/.translations/fr.json | 3 +++ .../components/monoprice/.translations/fr.json | 15 +++++++++++++++ .../components/notion/.translations/fr.json | 3 +++ .../components/plex/.translations/fr.json | 2 ++ .../pvpc_hourly_pricing/.translations/ko.json | 18 ++++++++++++++++++ .../rainmachine/.translations/fr.json | 3 +++ .../simplisafe/.translations/fr.json | 3 +++ .../components/tesla/.translations/fr.json | 1 + .../components/tesla/.translations/ko.json | 1 + .../components/unifi/.translations/fr.json | 8 ++++++-- 15 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/ko.json diff --git a/homeassistant/components/airvisual/.translations/de.json b/homeassistant/components/airvisual/.translations/de.json index 116e5ff500c..fc603318ab4 100644 --- a/homeassistant/components/airvisual/.translations/de.json +++ b/homeassistant/components/airvisual/.translations/de.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieser API-Schl\u00fcssel wird bereits verwendet." + "already_configured": "Diese Koordinaten wurden bereits registriert." }, "error": { "invalid_api_key": "Ung\u00fcltiger API-Schl\u00fcssel" diff --git a/homeassistant/components/airvisual/.translations/zh-Hant.json b/homeassistant/components/airvisual/.translations/zh-Hant.json index c1e9777d860..5c347e3b251 100644 --- a/homeassistant/components/airvisual/.translations/zh-Hant.json +++ b/homeassistant/components/airvisual/.translations/zh-Hant.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u6b64 API \u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002" + "already_configured": "\u6b64\u4e9b\u5ea7\u6a19\u5df2\u8a3b\u518a\u3002" }, "error": { "invalid_api_key": "API \u5bc6\u78bc\u7121\u6548" diff --git a/homeassistant/components/ambient_station/.translations/fr.json b/homeassistant/components/ambient_station/.translations/fr.json index b28cb374eac..00f4e3d02fc 100644 --- a/homeassistant/components/ambient_station/.translations/fr.json +++ b/homeassistant/components/ambient_station/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Cette cl\u00e9 d'application est d\u00e9j\u00e0 utilis\u00e9e." + }, "error": { "identifier_exists": "Cl\u00e9 d'application et / ou cl\u00e9 API d\u00e9j\u00e0 enregistr\u00e9e", "invalid_key": "Cl\u00e9 d'API et / ou cl\u00e9 d'application non valide", diff --git a/homeassistant/components/august/.translations/fr.json b/homeassistant/components/august/.translations/fr.json index 3a116c7bc06..89a35b28f1d 100644 --- a/homeassistant/components/august/.translations/fr.json +++ b/homeassistant/components/august/.translations/fr.json @@ -1,6 +1,10 @@ { "config": { + "abort": { + "already_configured": "Le compte est d\u00e9j\u00e0 configur\u00e9" + }, "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, @@ -9,14 +13,17 @@ "data": { "login_method": "M\u00e9thode de connexion", "password": "Mot de passe", + "timeout": "D\u00e9lai d'expiration (secondes)", "username": "Nom d'utilisateur" } }, "validation": { "data": { "code": "Code de v\u00e9rification" - } + }, + "title": "Authentification \u00e0 deux facteurs" } - } + }, + "title": "August" } } \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/fr.json b/homeassistant/components/doorbird/.translations/fr.json index 4090d94099b..21f67a9471e 100644 --- a/homeassistant/components/doorbird/.translations/fr.json +++ b/homeassistant/components/doorbird/.translations/fr.json @@ -20,5 +20,14 @@ } }, "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Liste d'\u00e9v\u00e9nements s\u00e9par\u00e9s par des virgules." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/fr.json b/homeassistant/components/konnected/.translations/fr.json index e6c0cded9fc..c41819369a0 100644 --- a/homeassistant/components/konnected/.translations/fr.json +++ b/homeassistant/components/konnected/.translations/fr.json @@ -9,6 +9,9 @@ "confirm": { "title": "Appareil Konnected pr\u00eat" }, + "import_confirm": { + "title": "Importer un appareil connect\u00e9" + }, "user": { "data": { "host": "Adresse IP de l\u2019appareil Konnected" diff --git a/homeassistant/components/monoprice/.translations/fr.json b/homeassistant/components/monoprice/.translations/fr.json index 16ab039e347..f93fb82d444 100644 --- a/homeassistant/components/monoprice/.translations/fr.json +++ b/homeassistant/components/monoprice/.translations/fr.json @@ -20,5 +20,20 @@ } } } + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nom de la source #1", + "source_2": "Nom de la source #2", + "source_3": "Nom de la source #3", + "source_4": "Nom de la source #4", + "source_5": "Nom de la source #5", + "source_6": "Nom de la source #6" + }, + "title": "Configurer les sources" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/fr.json b/homeassistant/components/notion/.translations/fr.json index 5f0bdd48a8a..4477c692993 100644 --- a/homeassistant/components/notion/.translations/fr.json +++ b/homeassistant/components/notion/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ce nom d'utilisateur est d\u00e9j\u00e0 utilis\u00e9." + }, "error": { "identifier_exists": "Nom d'utilisateur d\u00e9j\u00e0 enregistr\u00e9", "invalid_credentials": "Nom d'utilisateur ou mot de passe invalide", diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index bcd53d2ffae..4c1af21aaf1 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -53,6 +53,8 @@ "step": { "plex_mp_settings": { "data": { + "ignore_new_shared_users": "Ignorer les nouveaux utilisateurs g\u00e9r\u00e9s/partag\u00e9s", + "monitored_users": "Utilisateurs surveill\u00e9s", "show_all_controls": "Afficher tous les contr\u00f4les", "use_episode_art": "Utiliser l'art de l'\u00e9pisode" }, diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/ko.json b/homeassistant/components/pvpc_hourly_pricing/.translations/ko.json new file mode 100644 index 00000000000..e2bd6caaa8d --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/ko.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "\ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \uc774\ubbf8 \ud574\ub2f9 \uc694\uae08\uc81c \uc13c\uc11c\ub85c \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "name": "\uc13c\uc11c \uc774\ub984", + "tariff": "\uacc4\uc57d \uc694\uae08\uc81c (1, 2 \ub610\ub294 3 \uad6c\uac04)" + }, + "description": "\uc774 \uc13c\uc11c\ub294 \uacf5\uc2dd API \ub97c \uc0ac\uc6a9\ud558\uc5ec \uc2a4\ud398\uc778\uc758 [\uc2dc\uac04\ub2f9 \uc804\uae30 \uc694\uae08 (PVPC)](https://www.esios.ree.es/es/pvpc) \uc744 \uac00\uc838\uc635\ub2c8\ub2e4.\n\ubcf4\ub2e4 \uc790\uc138\ud55c \uc124\uba85\uc740 [\uc548\ub0b4](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/)\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694.\n\n1\uc77c\ub2f9 \uccad\uad6c \uad6c\uac04\uc5d0 \ub530\ub77c \uacc4\uc57d \uc694\uae08\uc81c\ub97c \uc120\ud0dd\ud574\uc8fc\uc138\uc694.\n - 1 \uad6c\uac04: \uc77c\ubc18 \uc694\uae08\uc81c\n - 2 \uad6c\uac04: \ucc28\ub4f1 \uc694\uae08\uc81c (\uc57c\uac04 \uc694\uae08) \n - 3 \uad6c\uac04: \uc804\uae30\uc790\ub3d9\ucc28 (3 \uad6c\uac04 \uc57c\uac04 \uc694\uae08)", + "title": "\uc694\uae08\uc81c \uc120\ud0dd" + } + }, + "title": "\uc2a4\ud398\uc778 \uc2dc\uac04\ub2f9 \uc804\uae30\uc694\uae08 (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/.translations/fr.json b/homeassistant/components/rainmachine/.translations/fr.json index 64d8f582ad3..48ae0c049c2 100644 --- a/homeassistant/components/rainmachine/.translations/fr.json +++ b/homeassistant/components/rainmachine/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ce contr\u00f4leur RainMachine est d\u00e9j\u00e0 configur\u00e9." + }, "error": { "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_credentials": "Informations d'identification invalides" diff --git a/homeassistant/components/simplisafe/.translations/fr.json b/homeassistant/components/simplisafe/.translations/fr.json index 576c66ab970..0f5049ecce4 100644 --- a/homeassistant/components/simplisafe/.translations/fr.json +++ b/homeassistant/components/simplisafe/.translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Ce compte SimpliSafe est d\u00e9j\u00e0 utilis\u00e9." + }, "error": { "identifier_exists": "Compte d\u00e9j\u00e0 enregistr\u00e9", "invalid_credentials": "Informations d'identification invalides" diff --git a/homeassistant/components/tesla/.translations/fr.json b/homeassistant/components/tesla/.translations/fr.json index 69742d3370c..ef9d5162899 100644 --- a/homeassistant/components/tesla/.translations/fr.json +++ b/homeassistant/components/tesla/.translations/fr.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Forcer les voitures \u00e0 se r\u00e9veiller au d\u00e9marrage", "scan_interval": "Secondes entre les scans" } } diff --git a/homeassistant/components/tesla/.translations/ko.json b/homeassistant/components/tesla/.translations/ko.json index 8b7dc9ce93c..a0f8d353349 100644 --- a/homeassistant/components/tesla/.translations/ko.json +++ b/homeassistant/components/tesla/.translations/ko.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "\uc2dc\ub3d9 \uc2dc \ucc28\ub7c9 \uae68\uc6b0\uae30", "scan_interval": "\uc2a4\uce94 \uac04\uaca9(\ucd08)" } } diff --git a/homeassistant/components/unifi/.translations/fr.json b/homeassistant/components/unifi/.translations/fr.json index 3b8a2996887..659a567a91f 100644 --- a/homeassistant/components/unifi/.translations/fr.json +++ b/homeassistant/components/unifi/.translations/fr.json @@ -42,7 +42,9 @@ "track_clients": "Suivre les clients du r\u00e9seau", "track_devices": "Suivre les p\u00e9riph\u00e9riques r\u00e9seau (p\u00e9riph\u00e9riques Ubiquiti)", "track_wired_clients": "Inclure les clients du r\u00e9seau filaire" - } + }, + "description": "Configurer le suivi des appareils", + "title": "Options UniFi 1/3" }, "init": { "data": { @@ -53,7 +55,9 @@ "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Cr\u00e9er des capteurs d'utilisation de la bande passante pour les clients r\u00e9seau" - } + }, + "description": "Configurer des capteurs de statistiques", + "title": "Options UniFi 3/3" } } } From 4f767dd3ef1b9474486bc5991307fcd6c72a02a8 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Thu, 26 Mar 2020 22:19:48 -0400 Subject: [PATCH 275/431] Add entities for ZHA fan groups (#33291) * start of fan groups * update fan classes * update group entity domains * add set speed * update discovery for multiple entities for groups * add fan group entity tests * cleanup const * cleanup entity_domain usage * remove bad super call * remove bad update line * fix set speed on fan group * change comparison * pythonic list * discovery guards * Update homeassistant/components/zha/core/discovery.py Co-Authored-By: Alexei Chetroi Co-authored-by: Alexei Chetroi --- homeassistant/components/zha/core/const.py | 1 - .../components/zha/core/discovery.py | 69 ++++---- homeassistant/components/zha/core/gateway.py | 9 +- homeassistant/components/zha/core/group.py | 25 +-- .../components/zha/core/registries.py | 2 +- homeassistant/components/zha/core/store.py | 79 +-------- homeassistant/components/zha/fan.py | 153 +++++++++++++---- tests/components/zha/test_fan.py | 162 +++++++++++++++++- tests/components/zha/test_gateway.py | 2 - tests/components/zha/test_light.py | 1 - tests/components/zha/test_switch.py | 1 - 11 files changed, 316 insertions(+), 188 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 2eb567ab4c4..43b1634cba7 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -23,7 +23,6 @@ ATTR_COMMAND_TYPE = "command_type" ATTR_DEVICE_IEEE = "device_ieee" ATTR_DEVICE_TYPE = "device_type" ATTR_ENDPOINT_ID = "endpoint_id" -ATTR_ENTITY_DOMAIN = "entity_domain" ATTR_IEEE = "ieee" ATTR_LAST_SEEN = "last_seen" ATTR_LEVEL = "level" diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 7202fd869fa..19a83c3b6bc 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -6,6 +6,7 @@ from typing import Callable, List, Tuple from homeassistant import const as ha_const from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_device from homeassistant.helpers.typing import HomeAssistantType @@ -182,59 +183,48 @@ class GroupProbe: ) return - if group.entity_domain is None: - _LOGGER.debug( - "Group: %s:0x%04x has no user set entity domain - attempting entity domain discovery", - group.name, - group.group_id, - ) - group.entity_domain = GroupProbe.determine_default_entity_domain( - self._hass, group - ) + entity_domains = GroupProbe.determine_entity_domains(self._hass, group) - if group.entity_domain is None: + if not entity_domains: return - _LOGGER.debug( - "Group: %s:0x%04x has an entity domain of: %s after discovery", - group.name, - group.group_id, - group.entity_domain, - ) - zha_gateway = self._hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] - entity_class = zha_regs.ZHA_ENTITIES.get_group_entity(group.entity_domain) - if entity_class is None: - return - - self._hass.data[zha_const.DATA_ZHA][group.entity_domain].append( - ( - entity_class, + for domain in entity_domains: + entity_class = zha_regs.ZHA_ENTITIES.get_group_entity(domain) + if entity_class is None: + continue + self._hass.data[zha_const.DATA_ZHA][domain].append( ( - group.domain_entity_ids, - f"{group.entity_domain}_group_{group.group_id}", - group.group_id, - zha_gateway.coordinator_zha_device, - ), + entity_class, + ( + group.get_domain_entity_ids(domain), + f"{domain}_group_{group.group_id}", + group.group_id, + zha_gateway.coordinator_zha_device, + ), + ) ) - ) + async_dispatcher_send(self._hass, zha_const.SIGNAL_ADD_ENTITIES) @staticmethod - def determine_default_entity_domain( + def determine_entity_domains( hass: HomeAssistantType, group: zha_typing.ZhaGroupType - ): - """Determine the default entity domain for this group.""" + ) -> List[str]: + """Determine the entity domains for this group.""" + entity_domains: List[str] = [] if len(group.members) < 2: _LOGGER.debug( "Group: %s:0x%04x has less than 2 members so cannot default an entity domain", group.name, group.group_id, ) - return None + return entity_domains zha_gateway = hass.data[zha_const.DATA_ZHA][zha_const.DATA_ZHA_GATEWAY] all_domain_occurrences = [] for device in group.members: + if device.is_coordinator: + continue entities = async_entries_for_device( zha_gateway.ha_entity_registry, device.device_id ) @@ -245,15 +235,18 @@ class GroupProbe: if entity.domain in zha_regs.GROUP_ENTITY_DOMAINS ] ) + if not all_domain_occurrences: + return entity_domains + # get all domains we care about if there are more than 2 entities of this domain counts = Counter(all_domain_occurrences) - domain = counts.most_common(1)[0][0] + entity_domains = [domain[0] for domain in counts.items() if domain[1] >= 2] _LOGGER.debug( - "The default entity domain is: %s for group: %s:0x%04x", - domain, + "The entity domains are: %s for group: %s:0x%04x", + entity_domains, group.name, group.group_id, ) - return domain + return entity_domains PROBE = ProbeEndpoint() diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 9d5bf609ed2..bc7ff42d25f 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -445,8 +445,6 @@ class ZHAGateway: if zha_group is None: zha_group = ZHAGroup(self._hass, self, zigpy_group) self._groups[zigpy_group.group_id] = zha_group - group_entry = self.zha_storage.async_get_or_create_group(zha_group) - zha_group.entity_domain = group_entry.entity_domain return zha_group @callback @@ -469,8 +467,6 @@ class ZHAGateway: """Update the devices in the store.""" for device in self.devices.values(): self.zha_storage.async_update_device(device) - for group in self.groups.values(): - self.zha_storage.async_update_group(group) await self.zha_storage.async_save() async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): @@ -559,9 +555,7 @@ class ZHAGateway: zha_group.group_id, ) discovery.GROUP_PROBE.discover_group_entities(zha_group) - if zha_group.entity_domain is not None: - self.zha_storage.async_update_group(zha_group) - async_dispatcher_send(self._hass, SIGNAL_ADD_ENTITIES) + return zha_group async def async_remove_zigpy_group(self, group_id: int) -> None: @@ -577,7 +571,6 @@ class ZHAGateway: if tasks: await asyncio.gather(*tasks) self.application_controller.groups.pop(group_id) - self.zha_storage.async_delete_group(group) async def shutdown(self): """Stop ZHA Controller Application.""" diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index e6b2dee0625..4fc86012d1a 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -1,7 +1,7 @@ """Group for Zigbee Home Automation.""" import asyncio import logging -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List from zigpy.types.named import EUI64 @@ -28,7 +28,6 @@ class ZHAGroup(LogMixin): self.hass: HomeAssistantType = hass self._zigpy_group: ZigpyGroupType = zigpy_group self._zha_gateway: ZhaGatewayType = zha_gateway - self._entity_domain: str = None @property def name(self) -> str: @@ -45,16 +44,6 @@ class ZHAGroup(LogMixin): """Return the endpoint for this group.""" return self._zigpy_group.endpoint - @property - def entity_domain(self) -> Optional[str]: - """Return the domain that will be used for the entity representing this group.""" - return self._entity_domain - - @entity_domain.setter - def entity_domain(self, domain: Optional[str]) -> None: - """Set the domain that will be used for the entity representing this group.""" - self._entity_domain = domain - @property def members(self) -> List[ZhaDeviceType]: """Return the ZHA devices that are members of this group.""" @@ -106,22 +95,15 @@ class ZHAGroup(LogMixin): all_entity_ids.append(entity.entity_id) return all_entity_ids - @property - def domain_entity_ids(self) -> List[str]: + def get_domain_entity_ids(self, domain) -> List[str]: """Return entity ids from the entity domain for this group.""" - if self.entity_domain is None: - return domain_entity_ids: List[str] = [] for device in self.members: entities = async_entries_for_device( self._zha_gateway.ha_entity_registry, device.device_id ) domain_entity_ids.extend( - [ - entity.entity_id - for entity in entities - if entity.domain == self.entity_domain - ] + [entity.entity_id for entity in entities if entity.domain == domain] ) return domain_entity_ids @@ -130,7 +112,6 @@ class ZHAGroup(LogMixin): """Get ZHA group info.""" group_info: Dict[str, Any] = {} group_info["group_id"] = self.group_id - group_info["entity_domain"] = self.entity_domain group_info["name"] = self.name group_info["members"] = [ zha_device.async_get_info() for zha_device in self.members diff --git a/homeassistant/components/zha/core/registries.py b/homeassistant/components/zha/core/registries.py index b596eefb71a..29b71343245 100644 --- a/homeassistant/components/zha/core/registries.py +++ b/homeassistant/components/zha/core/registries.py @@ -32,7 +32,7 @@ from .const import CONTROLLER, ZHA_GW_RADIO, ZHA_GW_RADIO_DESCRIPTION, RadioType from .decorators import CALLABLE_T, DictRegistry, SetRegistry from .typing import ChannelType -GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH] +GROUP_ENTITY_DOMAINS = [LIGHT, SWITCH, FAN] SMARTTHINGS_ACCELERATION_CLUSTER = 0xFC02 SMARTTHINGS_ARRIVAL_SENSOR_DEVICE_TYPE = 0x8000 diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 0cd9e045cb6..00a4942c7b7 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -10,7 +10,7 @@ from homeassistant.core import callback from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass -from .typing import ZhaDeviceType, ZhaGroupType +from .typing import ZhaDeviceType _LOGGER = logging.getLogger(__name__) @@ -30,15 +30,6 @@ class ZhaDeviceEntry: last_seen = attr.ib(type=float, default=None) -@attr.s(slots=True, frozen=True) -class ZhaGroupEntry: - """Zha Group storage Entry.""" - - name = attr.ib(type=str, default=None) - group_id = attr.ib(type=int, default=None) - entity_domain = attr.ib(type=float, default=None) - - class ZhaStorage: """Class to hold a registry of zha devices.""" @@ -46,7 +37,6 @@ class ZhaStorage: """Initialize the zha device storage.""" self.hass: HomeAssistantType = hass self.devices: MutableMapping[str, ZhaDeviceEntry] = {} - self.groups: MutableMapping[str, ZhaGroupEntry] = {} self._store = hass.helpers.storage.Store(STORAGE_VERSION, STORAGE_KEY) @callback @@ -59,17 +49,6 @@ class ZhaStorage: return self.async_update_device(device) - @callback - def async_create_group(self, group: ZhaGroupType) -> ZhaGroupEntry: - """Create a new ZhaGroupEntry.""" - group_entry: ZhaGroupEntry = ZhaGroupEntry( - name=group.name, - group_id=str(group.group_id), - entity_domain=group.entity_domain, - ) - self.groups[str(group.group_id)] = group_entry - return self.async_update_group(group) - @callback def async_get_or_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Create a new ZhaDeviceEntry.""" @@ -78,14 +57,6 @@ class ZhaStorage: return self.devices[ieee_str] return self.async_create_device(device) - @callback - def async_get_or_create_group(self, group: ZhaGroupType) -> ZhaGroupEntry: - """Create a new ZhaGroupEntry.""" - group_id: str = str(group.group_id) - if group_id in self.groups: - return self.groups[group_id] - return self.async_create_group(group) - @callback def async_create_or_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Create or update a ZhaDeviceEntry.""" @@ -93,13 +64,6 @@ class ZhaStorage: return self.async_update_device(device) return self.async_create_device(device) - @callback - def async_create_or_update_group(self, group: ZhaGroupType) -> ZhaGroupEntry: - """Create or update a ZhaGroupEntry.""" - if str(group.group_id) in self.groups: - return self.async_update_group(group) - return self.async_create_group(group) - @callback def async_delete_device(self, device: ZhaDeviceType) -> None: """Delete ZhaDeviceEntry.""" @@ -108,14 +72,6 @@ class ZhaStorage: del self.devices[ieee_str] self.async_schedule_save() - @callback - def async_delete_group(self, group: ZhaGroupType) -> None: - """Delete ZhaGroupEntry.""" - group_id: str = str(group.group_id) - if group_id in self.groups: - del self.groups[group_id] - self.async_schedule_save() - @callback def async_update_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: """Update name of ZhaDeviceEntry.""" @@ -129,25 +85,11 @@ class ZhaStorage: self.async_schedule_save() return new - @callback - def async_update_group(self, group: ZhaGroupType) -> ZhaGroupEntry: - """Update name of ZhaGroupEntry.""" - group_id: str = str(group.group_id) - old = self.groups[group_id] - - changes = {} - changes["entity_domain"] = group.entity_domain - - new = self.groups[group_id] = attr.evolve(old, **changes) - self.async_schedule_save() - return new - async def async_load(self) -> None: """Load the registry of zha device entries.""" data = await self._store.async_load() devices: "OrderedDict[str, ZhaDeviceEntry]" = OrderedDict() - groups: "OrderedDict[str, ZhaGroupEntry]" = OrderedDict() if data is not None: for device in data["devices"]: @@ -157,18 +99,7 @@ class ZhaStorage: last_seen=device["last_seen"] if "last_seen" in device else None, ) - if "groups" in data: - for group in data["groups"]: - groups[group["group_id"]] = ZhaGroupEntry( - name=group["name"], - group_id=group["group_id"], - entity_domain=group["entity_domain"] - if "entity_domain" in group - else None, - ) - self.devices = devices - self.groups = groups @callback def async_schedule_save(self) -> None: @@ -189,14 +120,6 @@ class ZhaStorage: for entry in self.devices.values() ] - data["groups"] = [ - { - "name": entry.name, - "group_id": entry.group_id, - "entity_domain": entry.entity_domain, - } - for entry in self.groups.values() - ] return data diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index d04453cd675..027d0f8a1ee 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,6 +1,10 @@ """Fans on Zigbee Home Automation networks.""" import functools import logging +from typing import List, Optional + +from zigpy.exceptions import DeliveryError +import zigpy.zcl.clusters.hvac as hvac from homeassistant.components.fan import ( DOMAIN, @@ -11,8 +15,10 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, FanEntity, ) -from homeassistant.core import callback +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_state_change from .core import discovery from .core.const import ( @@ -21,9 +27,10 @@ from .core.const import ( DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, + SIGNAL_REMOVE_GROUP, ) from .core.registries import ZHA_ENTITIES -from .entity import ZhaEntity +from .entity import BaseZhaEntity, ZhaEntity _LOGGER = logging.getLogger(__name__) @@ -49,6 +56,7 @@ SPEED_LIST = [ VALUE_TO_SPEED = dict(enumerate(SPEED_LIST)) SPEED_TO_VALUE = {speed: i for i, speed in enumerate(SPEED_LIST)} STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) +GROUP_MATCH = functools.partial(ZHA_ENTITIES.group_match, DOMAIN) async def async_setup_entry(hass, config_entry, async_add_entities): @@ -65,31 +73,14 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -@STRICT_MATCH(channel_names=CHANNEL_FAN) -class ZhaFan(ZhaEntity, FanEntity): - """Representation of a ZHA fan.""" +class BaseFan(BaseZhaEntity, FanEntity): + """Base representation of a ZHA fan.""" - def __init__(self, unique_id, zha_device, channels, **kwargs): - """Init this sensor.""" - super().__init__(unique_id, zha_device, channels, **kwargs) - self._fan_channel = self.cluster_channels.get(CHANNEL_FAN) - - async def async_added_to_hass(self): - """Run when about to be added to hass.""" - await super().async_added_to_hass() - await self.async_accept_signal( - self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state - ) - - @callback - def async_restore_last_state(self, last_state): - """Restore previous state.""" - self._state = VALUE_TO_SPEED.get(last_state.state, last_state.state) - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_SET_SPEED + def __init__(self, *args, **kwargs): + """Initialize the fan.""" + super().__init__(*args, **kwargs) + self._state = None + self._fan_channel = None @property def speed_list(self) -> list: @@ -109,15 +100,9 @@ class ZhaFan(ZhaEntity, FanEntity): return self._state != SPEED_OFF @property - def device_state_attributes(self): - """Return state attributes.""" - return self.state_attributes - - @callback - def async_set_state(self, attr_id, attr_name, value): - """Handle state update from channel.""" - self._state = VALUE_TO_SPEED.get(value, self._state) - self.async_write_ha_state() + def supported_features(self) -> int: + """Flag supported features.""" + return SUPPORT_SET_SPEED async def async_turn_on(self, speed: str = None, **kwargs) -> None: """Turn the entity on.""" @@ -135,6 +120,34 @@ class ZhaFan(ZhaEntity, FanEntity): await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed]) self.async_set_state(0, "fan_mode", speed) + +@STRICT_MATCH(channel_names=CHANNEL_FAN) +class ZhaFan(ZhaEntity, BaseFan): + """Representation of a ZHA fan.""" + + def __init__(self, unique_id, zha_device, channels, **kwargs): + """Init this sensor.""" + super().__init__(unique_id, zha_device, channels, **kwargs) + self._fan_channel = self.cluster_channels.get(CHANNEL_FAN) + + async def async_added_to_hass(self): + """Run when about to be added to hass.""" + await super().async_added_to_hass() + await self.async_accept_signal( + self._fan_channel, SIGNAL_ATTR_UPDATED, self.async_set_state + ) + + @callback + def async_restore_last_state(self, last_state): + """Restore previous state.""" + self._state = VALUE_TO_SPEED.get(last_state.state, last_state.state) + + @callback + def async_set_state(self, attr_id, attr_name, value): + """Handle state update from channel.""" + self._state = VALUE_TO_SPEED.get(value, self._state) + self.async_write_ha_state() + async def async_update(self): """Attempt to retrieve on off state from the fan.""" await super().async_update() @@ -142,3 +155,73 @@ class ZhaFan(ZhaEntity, FanEntity): state = await self._fan_channel.get_attribute_value("fan_mode") if state is not None: self._state = VALUE_TO_SPEED.get(state, self._state) + + +@GROUP_MATCH() +class FanGroup(BaseFan): + """Representation of a fan group.""" + + def __init__( + self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + ) -> None: + """Initialize a fan group.""" + super().__init__(unique_id, zha_device, **kwargs) + self._name: str = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" + self._group_id: int = group_id + self._available: bool = False + self._entity_ids: List[str] = entity_ids + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None + group = self.zha_device.gateway.get_group(self._group_id) + self._fan_channel = group.endpoint[hvac.Fan.cluster_id] + + # what should we do with this hack? + async def async_set_speed(value) -> None: + """Set the speed of the fan.""" + try: + await self._fan_channel.write_attributes({"fan_mode": value}) + except DeliveryError as ex: + self.error("Could not set speed: %s", ex) + return + + self._fan_channel.async_set_speed = async_set_speed + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + await self.async_accept_signal( + None, + f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", + self.async_remove, + signal_override=True, + ) + + @callback + def async_state_changed_listener( + entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener + ) + await self.async_update() + + async def async_will_remove_from_hass(self) -> None: + """Handle removal from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._async_unsub_state_changed is not None: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + async def async_update(self): + """Attempt to retrieve on off state from the fan.""" + all_states = [self.hass.states.get(x) for x in self._entity_ids] + states: List[State] = list(filter(None, all_states)) + on_states: List[State] = [state for state in states if state.state != SPEED_OFF] + self._available = any(state.state != STATE_UNAVAILABLE for state in states) + # for now just use first non off state since its kind of arbitrary + if not on_states: + self._state = SPEED_OFF + else: + self._state = states[0].state diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 5011a847a4e..399982df37a 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -2,10 +2,21 @@ from unittest.mock import call import pytest +import zigpy.profiles.zha as zha +import zigpy.zcl.clusters.general as general import zigpy.zcl.clusters.hvac as hvac from homeassistant.components import fan -from homeassistant.components.fan import ATTR_SPEED, DOMAIN, SERVICE_SET_SPEED +from homeassistant.components.fan import ( + ATTR_SPEED, + DOMAIN, + SERVICE_SET_SPEED, + SPEED_HIGH, + SPEED_MEDIUM, + SPEED_OFF, +) +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.zha.core.discovery import GROUP_PROBE from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TURN_OFF, @@ -17,11 +28,16 @@ from homeassistant.const import ( from .common import ( async_enable_traffic, + async_find_group_entity_id, async_test_rejoin, find_entity_id, + get_zha_gateway, send_attributes_report, ) +IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" + @pytest.fixture def zigpy_device(zigpy_device_mock): @@ -32,6 +48,66 @@ def zigpy_device(zigpy_device_mock): return zigpy_device_mock(endpoints) +@pytest.fixture +async def coordinator(hass, zigpy_device_mock, zha_device_joined): + """Test zha fan platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee="00:15:8d:00:02:32:4f:32", + nwk=0x0000, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_fan_1(hass, zigpy_device_mock, zha_device_joined): + """Test zha fan platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [general.OnOff.cluster_id, hvac.Fan.cluster_id], + "out_clusters": [], + } + }, + ieee=IEEE_GROUPABLE_DEVICE, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + +@pytest.fixture +async def device_fan_2(hass, zigpy_device_mock, zha_device_joined): + """Test zha fan platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + hvac.Fan.cluster_id, + general.LevelControl.cluster_id, + ], + "out_clusters": [], + } + }, + ieee=IEEE_GROUPABLE_DEVICE2, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + async def test_fan(hass, zha_device_joined_restored, zigpy_device): """Test zha fan platform.""" @@ -106,3 +182,87 @@ async def async_set_speed(hass, entity_id, speed=None): } await hass.services.async_call(DOMAIN, SERVICE_SET_SPEED, data, blocking=True) + + +async def async_test_zha_group_fan_entity( + hass, device_fan_1, device_fan_2, coordinator +): + """Test the fan entity for a ZHA group.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + zha_gateway.coordinator_zha_device = coordinator + coordinator._zha_gateway = zha_gateway + device_fan_1._zha_gateway = zha_gateway + device_fan_2._zha_gateway = zha_gateway + member_ieee_addresses = [device_fan_1.ieee, device_fan_2.ieee] + + # test creating a group with 2 members + zha_group = await zha_gateway.async_create_zigpy_group( + "Test Group", member_ieee_addresses + ) + await hass.async_block_till_done() + + assert zha_group is not None + assert len(zha_group.members) == 2 + for member in zha_group.members: + assert member.ieee in member_ieee_addresses + + entity_domains = GROUP_PROBE.determine_entity_domains(zha_group) + assert len(entity_domains) == 2 + + assert LIGHT_DOMAIN in entity_domains + assert DOMAIN in entity_domains + + entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + assert hass.states.get(entity_id) is not None + + group_fan_cluster = zha_group.endpoint[hvac.Fan.cluster_id] + dev1_fan_cluster = device_fan_1.endpoints[1].fan + dev2_fan_cluster = device_fan_2.endpoints[1].fan + + # test that the lights were created and that they are 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_group.members) + + # test that the fan group entity was created and is off + assert hass.states.get(entity_id).state == STATE_OFF + + # turn on from HA + group_fan_cluster.write_attributes.reset_mock() + await async_turn_on(hass, entity_id) + assert len(group_fan_cluster.write_attributes.mock_calls) == 1 + assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 2}) + assert hass.states.get(entity_id).state == SPEED_MEDIUM + + # turn off from HA + group_fan_cluster.write_attributes.reset_mock() + await async_turn_off(hass, entity_id) + assert len(group_fan_cluster.write_attributes.mock_calls) == 1 + assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 0}) + assert hass.states.get(entity_id).state == STATE_OFF + + # change speed from HA + group_fan_cluster.write_attributes.reset_mock() + await async_set_speed(hass, entity_id, speed=fan.SPEED_HIGH) + assert len(group_fan_cluster.write_attributes.mock_calls) == 1 + assert group_fan_cluster.write_attributes.call_args == call({"fan_mode": 3}) + assert hass.states.get(entity_id).state == SPEED_HIGH + + # test some of the group logic to make sure we key off states correctly + await dev1_fan_cluster.async_set_speed(SPEED_OFF) + await dev2_fan_cluster.async_set_speed(SPEED_OFF) + + # test that group fan is off + assert hass.states.get(entity_id).state == STATE_OFF + + await dev1_fan_cluster.async_set_speed(SPEED_MEDIUM) + + # test that group fan is speed medium + assert hass.states.get(entity_id).state == SPEED_MEDIUM + + await dev1_fan_cluster.async_set_speed(SPEED_OFF) + + # test that group fan is now off + assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 3bb98522814..80d96fa55bd 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -134,7 +134,6 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord await hass.async_block_till_done() assert zha_group is not None - assert zha_group.entity_domain == LIGHT_DOMAIN assert len(zha_group.members) == 2 for member in zha_group.members: assert member.ieee in member_ieee_addresses @@ -162,7 +161,6 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord await hass.async_block_till_done() assert zha_group is not None - assert zha_group.entity_domain is None assert len(zha_group.members) == 1 for member in zha_group.members: assert member.ieee in [device_light_1.ieee] diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index c6bafa45aea..f832b9e86e0 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -432,7 +432,6 @@ async def async_test_zha_group_light_entity( await hass.async_block_till_done() assert zha_group is not None - assert zha_group.entity_domain == DOMAIN assert len(zha_group.members) == 2 for member in zha_group.members: assert member.ieee in member_ieee_addresses diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index adaaa7c2a2f..ed5d228ab88 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -173,7 +173,6 @@ async def async_test_zha_group_switch_entity( await hass.async_block_till_done() assert zha_group is not None - assert zha_group.entity_domain == DOMAIN assert len(zha_group.members) == 2 for member in zha_group.members: assert member.ieee in member_ieee_addresses From 6cafc45d2b8a946b3b3f8d503b5f1e45b4e06b15 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Mar 2020 20:23:01 -0700 Subject: [PATCH 276/431] Fix ZHA light crashing on color loop effect (#33298) --- homeassistant/components/zha/light.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 2192ec1a909..c65d3c47de6 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -321,6 +321,7 @@ class Light(ZhaEntity, BaseLight): self._color_channel = self.cluster_channels.get(CHANNEL_COLOR) self._identify_channel = self.zha_device.channels.identify_ch self._cancel_refresh_handle = None + effect_list = [] if self._level_channel: self._supported_features |= light.SUPPORT_BRIGHTNESS @@ -338,11 +339,14 @@ class Light(ZhaEntity, BaseLight): if color_capabilities & CAPABILITIES_COLOR_LOOP: self._supported_features |= light.SUPPORT_EFFECT - self._effect_list.append(light.EFFECT_COLORLOOP) + effect_list.append(light.EFFECT_COLORLOOP) if self._identify_channel: self._supported_features |= light.SUPPORT_FLASH + if effect_list: + self._effect_list = effect_list + @callback def async_set_state(self, attr_id, attr_name, value): """Set the state.""" From 73c52e668e7ef239a20f0287c66f368a41f65a94 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 26 Mar 2020 20:44:44 -0700 Subject: [PATCH 277/431] Fix dispatcher logging (#33299) --- homeassistant/helpers/dispatcher.py | 5 ++++- tests/helpers/test_dispatcher.py | 9 +++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/homeassistant/helpers/dispatcher.py b/homeassistant/helpers/dispatcher.py index a4e624f119f..bb6fa3a735d 100644 --- a/homeassistant/helpers/dispatcher.py +++ b/homeassistant/helpers/dispatcher.py @@ -47,7 +47,10 @@ def async_dispatcher_connect( wrapped_target = catch_log_exception( target, lambda *args: "Exception in {} when dispatching '{}': {}".format( - target.__name__, signal, args + # Functions wrapped in partial do not have a __name__ + getattr(target, "__name__", None) or str(target), + signal, + args, ), ) diff --git a/tests/helpers/test_dispatcher.py b/tests/helpers/test_dispatcher.py index 4cf266e88a2..539d9ad1651 100644 --- a/tests/helpers/test_dispatcher.py +++ b/tests/helpers/test_dispatcher.py @@ -1,5 +1,6 @@ """Test dispatcher helpers.""" import asyncio +from functools import partial from homeassistant.core import callback from homeassistant.helpers.dispatcher import ( @@ -147,9 +148,13 @@ async def test_callback_exception_gets_logged(hass, caplog): """Record calls.""" raise Exception("This is a bad message callback") - async_dispatcher_connect(hass, "test", bad_handler) + # wrap in partial to test message logging. + async_dispatcher_connect(hass, "test", partial(bad_handler)) dispatcher_send(hass, "test", "bad") await hass.async_block_till_done() await hass.async_block_till_done() - assert "Exception in bad_handler when dispatching 'test': ('bad',)" in caplog.text + assert ( + f"Exception in functools.partial({bad_handler}) when dispatching 'test': ('bad',)" + in caplog.text + ) From c629e7dc0e346d4f51eda17c9c30ef27fb37b671 Mon Sep 17 00:00:00 2001 From: Adam Michaleski <38081677+prairieapps@users.noreply.github.com> Date: Fri, 27 Mar 2020 12:53:36 -0500 Subject: [PATCH 278/431] Add integration for Schluter (#31088) * Add integration for Schluter New integration for Schluter DITRA-HEAT thermostats. Interacts with Schluter API via py-schluter to implement climate platform. * Fix for manifest issue on build Removed unnecessary configurator logic * Flake8 & ISort issues fixed * Code review modifications for Schluter integration - Removed unnecessary strings.json file - Removed unnecessary DEFAULT_SCAN_INTERVAL from __init__.py - Removed persistent notification for authentication error in __init__.py - Removed string casts from log statements in __init__.py - Removed unnecessary logging of entity creations in climate.py - Removed unnecessary lookup for hvac_mode in climate.py * Work started on Update Coordinator * Further Coordinator work * Update to DataUpdateCoordinator complete * Flake8 fix * Removal of unnecessary SchluterPlatformData Coordinator also now using SCAN_INTERVAL * Disable polling, use coordinator listeners * Isort on climate.py * Code review modifications - Hass data stored under domain - Since platform only likely to be climate, removed array, load explicitly - Remove unnecessary fan mode implementation * Switch to HVAC action for showing current heating status * Isort fix * HVAC Mode return list, set_hvac_mode function added * __init__.py modifications from code review * Climate.py code review modifications - Device info property removed - Write state function on set temp failure - Add "available" property - Delegate sync function in coordinator update to the executor * Serial number as unique identifier - Now using serial number & thermostat dictionary to protect against scenarios where a device is removed and enumerable index is no longer accurate - Removed async_write_ha_state() from set temp exception, not needed - Removed unnecessary SCAN_INTERVAL definition * Linting * Update homeassistant/components/schluter/climate.py Co-Authored-By: Paulus Schoutsen * Update homeassistant/components/schluter/climate.py Co-Authored-By: Paulus Schoutsen * Changed timeout from config, load platform discover to None * Proposed change for discover info issue Co-authored-by: Paulus Schoutsen --- .coveragerc | 1 + CODEOWNERS | 1 + homeassistant/components/schluter/__init__.py | 73 ++++++++ homeassistant/components/schluter/climate.py | 169 ++++++++++++++++++ homeassistant/components/schluter/const.py | 3 + .../components/schluter/manifest.json | 8 + requirements_all.txt | 3 + 7 files changed, 258 insertions(+) create mode 100644 homeassistant/components/schluter/__init__.py create mode 100644 homeassistant/components/schluter/climate.py create mode 100644 homeassistant/components/schluter/const.py create mode 100644 homeassistant/components/schluter/manifest.json diff --git a/.coveragerc b/.coveragerc index a218c812df4..14a731498b9 100644 --- a/.coveragerc +++ b/.coveragerc @@ -609,6 +609,7 @@ omit = homeassistant/components/saj/sensor.py homeassistant/components/salt/device_tracker.py homeassistant/components/satel_integra/* + homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py homeassistant/components/scsgate/* homeassistant/components/scsgate/cover.py diff --git a/CODEOWNERS b/CODEOWNERS index 1fda4d6f44b..62e6f0d8e66 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -313,6 +313,7 @@ homeassistant/components/saj/* @fredericvl homeassistant/components/salt/* @bjornorri homeassistant/components/samsungtv/* @escoand homeassistant/components/scene/* @home-assistant/core +homeassistant/components/schluter/* @prairieapps homeassistant/components/scrape/* @fabaff homeassistant/components/script/* @home-assistant/core homeassistant/components/search/* @home-assistant/core diff --git a/homeassistant/components/schluter/__init__.py b/homeassistant/components/schluter/__init__.py new file mode 100644 index 00000000000..9a78730d775 --- /dev/null +++ b/homeassistant/components/schluter/__init__.py @@ -0,0 +1,73 @@ +"""The Schluter DITRA-HEAT integration.""" +import logging + +from requests import RequestException, Session +from schluter.api import Api +from schluter.authenticator import AuthenticationState, Authenticator +import voluptuous as vol + +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers import discovery +import homeassistant.helpers.config_validation as cv + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) +DATA_SCHLUTER_SESSION = "schluter_session" +DATA_SCHLUTER_API = "schluter_api" +SCHLUTER_CONFIG_FILE = ".schluter.conf" +API_TIMEOUT = 10 + +CONFIG_SCHEMA = vol.Schema( + { + vol.Required(DOMAIN): vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + + +def setup(hass, config): + """Set up the Schluter component.""" + _LOGGER.debug("Starting setup of schluter") + + conf = config[DOMAIN] + api_http_session = Session() + api = Api(timeout=API_TIMEOUT, http_session=api_http_session) + + authenticator = Authenticator( + api, + conf.get(CONF_USERNAME), + conf.get(CONF_PASSWORD), + session_id_cache_file=hass.config.path(SCHLUTER_CONFIG_FILE), + ) + + authentication = None + try: + authentication = authenticator.authenticate() + except RequestException as ex: + _LOGGER.error("Unable to connect to Schluter service: %s", ex) + return + + state = authentication.state + + if state == AuthenticationState.AUTHENTICATED: + hass.data[DOMAIN] = { + DATA_SCHLUTER_API: api, + DATA_SCHLUTER_SESSION: authentication.session_id, + } + discovery.load_platform(hass, "climate", DOMAIN, {}, config) + return True + if state == AuthenticationState.BAD_PASSWORD: + _LOGGER.error("Invalid password provided") + return False + if state == AuthenticationState.BAD_EMAIL: + _LOGGER.error("Invalid email provided") + return False + + _LOGGER.error("Unknown set up error: %s", state) + return False diff --git a/homeassistant/components/schluter/climate.py b/homeassistant/components/schluter/climate.py new file mode 100644 index 00000000000..99dc5b0d495 --- /dev/null +++ b/homeassistant/components/schluter/climate.py @@ -0,0 +1,169 @@ +"""Support for Schluter thermostats.""" +import logging + +from requests import RequestException +import voluptuous as vol + +from homeassistant.components.climate import ( + PLATFORM_SCHEMA, + SCAN_INTERVAL, + ClimateDevice, +) +from homeassistant.components.climate.const import ( + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + HVAC_MODE_HEAT, + SUPPORT_TARGET_TEMPERATURE, +) +from homeassistant.const import ATTR_TEMPERATURE, CONF_SCAN_INTERVAL +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from . import DATA_SCHLUTER_API, DATA_SCHLUTER_SESSION, DOMAIN + +_LOGGER = logging.getLogger(__name__) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + {vol.Optional(CONF_SCAN_INTERVAL): vol.All(vol.Coerce(int), vol.Range(min=1))} +) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the Schluter thermostats.""" + if discovery_info is None: + return + session_id = hass.data[DOMAIN][DATA_SCHLUTER_SESSION] + api = hass.data[DOMAIN][DATA_SCHLUTER_API] + temp_unit = hass.config.units.temperature_unit + + async def async_update_data(): + try: + thermostats = await hass.async_add_executor_job( + api.get_thermostats, session_id + ) + except RequestException as err: + raise UpdateFailed(f"Error communicating with Schluter API: {err}") + + if thermostats is None: + return {} + + return {thermo.serial_number: thermo for thermo in thermostats} + + coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + name="schluter", + update_method=async_update_data, + update_interval=SCAN_INTERVAL, + ) + + await coordinator.async_refresh() + + async_add_entities( + SchluterThermostat(coordinator, serial_number, temp_unit, api, session_id) + for serial_number, thermostat in coordinator.data.items() + ) + + +class SchluterThermostat(ClimateDevice): + """Representation of a Schluter thermostat.""" + + def __init__(self, coordinator, serial_number, temp_unit, api, session_id): + """Initialize the thermostat.""" + self._unit = temp_unit + self._coordinator = coordinator + self._serial_number = serial_number + self._api = api + self._session_id = session_id + self._support_flags = SUPPORT_TARGET_TEMPERATURE + + @property + def available(self): + """Return if thermostat is available.""" + return self._coordinator.last_update_success + + @property + def should_poll(self): + """Return if platform should poll.""" + return False + + @property + def supported_features(self): + """Return the list of supported features.""" + return self._support_flags + + @property + def unique_id(self): + """Return unique ID for this device.""" + return self._serial_number + + @property + def name(self): + """Return the name of the thermostat.""" + return self._coordinator.data[self._serial_number].name + + @property + def temperature_unit(self): + """Return the unit of measurement.""" + return self._unit + + @property + def current_temperature(self): + """Return the current temperature.""" + return self._coordinator.data[self._serial_number].temperature + + @property + def hvac_mode(self): + """Return current mode. Only heat available for floor thermostat.""" + return HVAC_MODE_HEAT + + @property + def hvac_action(self): + """Return current operation. Can only be heating or idle.""" + return ( + CURRENT_HVAC_HEAT + if self._coordinator.data[self._serial_number].is_heating + else CURRENT_HVAC_IDLE + ) + + @property + def target_temperature(self): + """Return the temperature we try to reach.""" + return self._coordinator.data[self._serial_number].set_point_temp + + @property + def hvac_modes(self): + """List of available operation modes.""" + return [HVAC_MODE_HEAT] + + @property + def min_temp(self): + """Identify min_temp in Schluter API.""" + return self._coordinator.data[self._serial_number].min_temp + + @property + def max_temp(self): + """Identify max_temp in Schluter API.""" + return self._coordinator.data[self._serial_number].max_temp + + async def async_set_hvac_mode(self, hvac_mode): + """Mode is always heating, so do nothing.""" + + def set_temperature(self, **kwargs): + """Set new target temperature.""" + target_temp = None + target_temp = kwargs.get(ATTR_TEMPERATURE) + serial_number = self._coordinator.data[self._serial_number].serial_number + _LOGGER.debug("Setting thermostat temperature: %s", target_temp) + + try: + if target_temp is not None: + self._api.set_temperature(self._session_id, serial_number, target_temp) + except RequestException as ex: + _LOGGER.error("An error occurred while setting temperature: %s", ex) + + async def async_added_to_hass(self): + """When entity is added to hass.""" + self._coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + self._coordinator.async_remove_listener(self.async_write_ha_state) diff --git a/homeassistant/components/schluter/const.py b/homeassistant/components/schluter/const.py new file mode 100644 index 00000000000..e09c8cf66a9 --- /dev/null +++ b/homeassistant/components/schluter/const.py @@ -0,0 +1,3 @@ +"""Constants for the Schluter DITRA-HEAT integration.""" + +DOMAIN = "schluter" diff --git a/homeassistant/components/schluter/manifest.json b/homeassistant/components/schluter/manifest.json new file mode 100644 index 00000000000..1a7cebcf06a --- /dev/null +++ b/homeassistant/components/schluter/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "schluter", + "name": "Schluter", + "documentation": "https://www.home-assistant.io/integrations/schluter", + "requirements": ["py-schluter==0.1.7"], + "dependencies": [], + "codeowners": ["@prairieapps"] +} diff --git a/requirements_all.txt b/requirements_all.txt index d1d6e2447ed..f0ba017f574 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1111,6 +1111,9 @@ py-cpuinfo==5.0.0 # homeassistant.components.melissa py-melissa-climate==2.0.0 +# homeassistant.components.schluter +py-schluter==0.1.7 + # homeassistant.components.synology py-synology==0.2.0 From 18a4829314341de38238728beae07970c3f42858 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2020 15:38:35 -0500 Subject: [PATCH 279/431] Config flow for elkm1 (#33297) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Config flow for elkm1 * As entity ids can now be changed, the “alarm_control_panel” attribute “changed_by_entity_id” is now “changed_by_keypad” and will show the name of the Elk keypad instead of the entity id. * An auto configure mode has been introduced which avoids the need to setup the complex include and exclude filters. This functionality still exists when configuring from yaml for power users who want more control over which entities elkm1 generates. * restore _has_all_unique_prefixes * preserve legacy behavior of creating alarm_control_panels that have no linked keypads when auto_configure is False * unroll loop --- CODEOWNERS | 1 + .../components/elkm1/.translations/en.json | 28 ++ homeassistant/components/elkm1/__init__.py | 249 +++++++++++----- .../components/elkm1/alarm_control_panel.py | 127 ++++----- homeassistant/components/elkm1/climate.py | 23 +- homeassistant/components/elkm1/config_flow.py | 164 +++++++++++ homeassistant/components/elkm1/const.py | 31 ++ homeassistant/components/elkm1/light.py | 15 +- homeassistant/components/elkm1/manifest.json | 9 +- homeassistant/components/elkm1/scene.py | 16 +- homeassistant/components/elkm1/sensor.py | 52 ++-- homeassistant/components/elkm1/strings.json | 28 ++ homeassistant/components/elkm1/switch.py | 18 +- homeassistant/generated/config_flows.py | 1 + requirements_all.txt | 2 +- requirements_test_all.txt | 3 + tests/components/elkm1/__init__.py | 1 + tests/components/elkm1/test_config_flow.py | 269 ++++++++++++++++++ 18 files changed, 823 insertions(+), 214 deletions(-) create mode 100644 homeassistant/components/elkm1/.translations/en.json create mode 100644 homeassistant/components/elkm1/config_flow.py create mode 100644 homeassistant/components/elkm1/const.py create mode 100644 homeassistant/components/elkm1/strings.json create mode 100644 tests/components/elkm1/__init__.py create mode 100644 tests/components/elkm1/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 62e6f0d8e66..cbd4ae11c24 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -99,6 +99,7 @@ homeassistant/components/edl21/* @mtdcr homeassistant/components/egardia/* @jeroenterheerdt homeassistant/components/eight_sleep/* @mezz64 homeassistant/components/elgato/* @frenck +homeassistant/components/elkm1/* @bdraco homeassistant/components/elv/* @majuss homeassistant/components/emby/* @mezz64 homeassistant/components/emoncms/* @borpin diff --git a/homeassistant/components/elkm1/.translations/en.json b/homeassistant/components/elkm1/.translations/en.json new file mode 100644 index 00000000000..a5246a004c3 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/en.json @@ -0,0 +1,28 @@ +{ + "config": { + "title": "Elk-M1 Control", + "step": { + "user": { + "title": "Connect to Elk-M1 Control", + "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", + "data": { + "protocol": "Protocol", + "address": "The IP address or domain or serial port if connecting via serial.", + "username": "Username (secure only).", + "password": "Password (secure only).", + "prefix": "A unique prefix (leave blank if you only have one ElkM1).", + "temperature_unit": "The temperature unit ElkM1 uses." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "An ElkM1 with this prefix is already configured", + "address_already_configured": "An ElkM1 with this address is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 2acb8030cf1..2f08b046d9c 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -1,11 +1,13 @@ """Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels.""" +import asyncio import logging import re +import async_timeout import elkm1_lib as elkm1 -from elkm1_lib.const import Max import voluptuous as vol +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_EXCLUDE, CONF_HOST, @@ -15,23 +17,29 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers import config_validation as cv, discovery +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType -DOMAIN = "elkm1" +from .const import ( + CONF_AREA, + CONF_AUTO_CONFIGURE, + CONF_COUNTER, + CONF_ENABLED, + CONF_KEYPAD, + CONF_OUTPUT, + CONF_PLC, + CONF_PREFIX, + CONF_SETTING, + CONF_TASK, + CONF_THERMOSTAT, + CONF_ZONE, + DOMAIN, + ELK_ELEMENTS, +) -CONF_AREA = "area" -CONF_COUNTER = "counter" -CONF_ENABLED = "enabled" -CONF_KEYPAD = "keypad" -CONF_OUTPUT = "output" -CONF_PLC = "plc" -CONF_SETTING = "setting" -CONF_TASK = "task" -CONF_THERMOSTAT = "thermostat" -CONF_ZONE = "zone" -CONF_PREFIX = "prefix" +SYNC_TIMEOUT = 55 _LOGGER = logging.getLogger(__name__) @@ -110,6 +118,7 @@ DEVICE_SCHEMA = vol.Schema( vol.Optional(CONF_PREFIX, default=""): vol.All(cv.string, vol.Lower), vol.Optional(CONF_USERNAME, default=""): cv.string, vol.Optional(CONF_PASSWORD, default=""): cv.string, + vol.Optional(CONF_AUTO_CONFIGURE, default=False): cv.boolean, vol.Optional(CONF_TEMPERATURE_UNIT, default="F"): cv.temperature_unit, vol.Optional(CONF_AREA, default={}): DEVICE_SCHEMA_SUBDOMAIN, vol.Optional(CONF_COUNTER, default={}): DEVICE_SCHEMA_SUBDOMAIN, @@ -132,34 +141,53 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: """Set up the Elk M1 platform.""" - devices = {} - elk_datas = {} + hass.data.setdefault(DOMAIN, {}) + _create_elk_services(hass) - configs = { - CONF_AREA: Max.AREAS.value, - CONF_COUNTER: Max.COUNTERS.value, - CONF_KEYPAD: Max.KEYPADS.value, - CONF_OUTPUT: Max.OUTPUTS.value, - CONF_PLC: Max.LIGHTS.value, - CONF_SETTING: Max.SETTINGS.value, - CONF_TASK: Max.TASKS.value, - CONF_THERMOSTAT: Max.THERMOSTATS.value, - CONF_ZONE: Max.ZONES.value, - } - - def _included(ranges, set_to, values): - for rng in ranges: - if not rng[0] <= rng[1] <= len(values): - raise vol.Invalid(f"Invalid range {rng}") - values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1) + if DOMAIN not in hass_config: + return True for index, conf in enumerate(hass_config[DOMAIN]): - _LOGGER.debug("Setting up elkm1 #%d - %s", index, conf["host"]) + _LOGGER.debug("Importing elkm1 #%d - %s", index, conf[CONF_HOST]) + current_config_entry = _async_find_matching_config_entry( + hass, conf[CONF_PREFIX] + ) + if current_config_entry: + # If they alter the yaml config we import the changes + # since there currently is no practical way to do an options flow + # with the large amount of include/exclude/enabled options that elkm1 has. + hass.config_entries.async_update_entry(current_config_entry, data=conf) + continue - config = {"temperature_unit": conf[CONF_TEMPERATURE_UNIT]} + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=conf, + ) + ) + + return True + + +@callback +def _async_find_matching_config_entry(hass, prefix): + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.unique_id == prefix: + return entry + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Elk-M1 Control from a config entry.""" + + conf = entry.data + + _LOGGER.debug("Setting up elkm1 %s", conf["host"]) + + config = {"temperature_unit": conf[CONF_TEMPERATURE_UNIT]} + + if not conf[CONF_AUTO_CONFIGURE]: + # With elkm1-lib==0.7.16 and later auto configure is available config["panel"] = {"enabled": True, "included": [True]} - - for item, max_ in configs.items(): + for item, max_ in ELK_ELEMENTS.items(): config[item] = { "enabled": conf[item][CONF_ENABLED], "included": [not conf[item]["include"]] * max_, @@ -171,39 +199,92 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: _LOGGER.error("Config item: %s; %s", item, err) return False - prefix = conf[CONF_PREFIX] - elk = elkm1.Elk( - { - "url": conf[CONF_HOST], - "userid": conf[CONF_USERNAME], - "password": conf[CONF_PASSWORD], - } - ) - elk.connect() - - devices[prefix] = elk - elk_datas[prefix] = { - "elk": elk, - "prefix": prefix, - "config": config, - "keypads": {}, + elk = elkm1.Elk( + { + "url": conf[CONF_HOST], + "userid": conf[CONF_USERNAME], + "password": conf[CONF_PASSWORD], } + ) + elk.connect() - _create_elk_services(hass, devices) + if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT): + _LOGGER.error( + "Timed out after %d seconds while trying to sync with ElkM1", SYNC_TIMEOUT, + ) + elk.disconnect() + raise ConfigEntryNotReady + + if elk.invalid_auth: + _LOGGER.error("Authentication failed for ElkM1") + return False + + hass.data[DOMAIN][entry.entry_id] = { + "elk": elk, + "prefix": conf[CONF_PREFIX], + "auto_configure": conf[CONF_AUTO_CONFIGURE], + "config": config, + "keypads": {}, + } - hass.data[DOMAIN] = elk_datas for component in SUPPORTED_DOMAINS: hass.async_create_task( - discovery.async_load_platform(hass, component, DOMAIN, {}, hass_config) + hass.config_entries.async_forward_entry_setup(entry, component) ) return True -def _create_elk_services(hass, elks): +def _included(ranges, set_to, values): + for rng in ranges: + if not rng[0] <= rng[1] <= len(values): + raise vol.Invalid(f"Invalid range {rng}") + values[rng[0] - 1 : rng[1]] = [set_to] * (rng[1] - rng[0] + 1) + + +def _find_elk_by_prefix(hass, prefix): + """Search all config entries for a given prefix.""" + for entry_id in hass.data[DOMAIN]: + if hass.data[DOMAIN][entry_id]["prefix"] == prefix: + return hass.data[DOMAIN][entry_id]["elk"] + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in SUPPORTED_DOMAINS + ] + ) + ) + + # disconnect cleanly + hass.data[DOMAIN][entry.entry_id]["elk"].disconnect() + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +async def async_wait_for_elk_to_sync(elk, timeout): + """Wait until the elk system has finished sync.""" + try: + with async_timeout.timeout(timeout): + await elk.sync_complete() + return True + except asyncio.TimeoutError: + elk.disconnect() + + return False + + +def _create_elk_services(hass): def _speak_word_service(service): prefix = service.data["prefix"] - elk = elks.get(prefix) + elk = _find_elk_by_prefix(hass, prefix) if elk is None: _LOGGER.error("No elk m1 with prefix for speak_word: '%s'", prefix) return @@ -211,7 +292,7 @@ def _create_elk_services(hass, elks): def _speak_phrase_service(service): prefix = service.data["prefix"] - elk = elks.get(prefix) + elk = _find_elk_by_prefix(hass, prefix) if elk is None: _LOGGER.error("No elk m1 with prefix for speak_phrase: '%s'", prefix) return @@ -227,12 +308,23 @@ def _create_elk_services(hass, elks): def create_elk_entities(elk_data, elk_elements, element_type, class_, entities): """Create the ElkM1 devices of a particular class.""" - if elk_data["config"][element_type]["enabled"]: - elk = elk_data["elk"] - _LOGGER.debug("Creating elk entities for %s", elk) - for element in elk_elements: - if elk_data["config"][element_type]["included"][element.index]: - entities.append(class_(element, elk, elk_data)) + auto_configure = elk_data["auto_configure"] + + if not auto_configure and not elk_data["config"][element_type]["enabled"]: + return + + elk = elk_data["elk"] + _LOGGER.debug("Creating elk entities for %s", elk) + + for element in elk_elements: + if auto_configure: + if not element.configured: + continue + # Only check the included list if auto configure is not + elif not elk_data["config"][element_type]["included"][element.index]: + continue + + entities.append(class_(element, elk, elk_data)) return entities @@ -297,9 +389,34 @@ class ElkEntity(Entity): def _element_callback(self, element, changeset): """Handle callback from an Elk element that has changed.""" self._element_changed(element, changeset) - self.async_schedule_update_ha_state(True) + self.async_write_ha_state() async def async_added_to_hass(self): """Register callback for ElkM1 changes and update entity state.""" self._element.add_callback(self._element_callback) self._element_callback(self._element, {}) + + @property + def device_info(self): + """Device info connecting via the ElkM1 system.""" + return { + "via_device": (DOMAIN, f"{self._prefix}_system"), + } + + +class ElkAttachedEntity(ElkEntity): + """An elk entity that is attached to the elk system.""" + + @property + def device_info(self): + """Device info for the underlying ElkM1 system.""" + device_name = "ElkM1" + if self._prefix: + device_name += f" {self._prefix}" + return { + "name": device_name, + "identifiers": {(DOMAIN, f"{self._prefix}_system")}, + "sw_version": self._elk.panel.elkm1_version, + "manufacturer": "ELK Products, Inc.", + "model": "M1", + } diff --git a/homeassistant/components/elkm1/alarm_control_panel.py b/homeassistant/components/elkm1/alarm_control_panel.py index de1cb62234c..d7cd5cf2ad0 100644 --- a/homeassistant/components/elkm1/alarm_control_panel.py +++ b/homeassistant/components/elkm1/alarm_control_panel.py @@ -1,4 +1,6 @@ """Each ElkM1 area will be created as a separate alarm_control_panel.""" +import logging + from elkm1_lib.const import AlarmState, ArmedStatus, ArmLevel, ArmUpState import voluptuous as vol @@ -22,24 +24,18 @@ from homeassistant.const import ( STATE_ALARM_PENDING, STATE_ALARM_TRIGGERED, ) +from homeassistant.helpers import entity_platform import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.dispatcher import ( - async_dispatcher_connect, - async_dispatcher_send, -) from . import ( - DOMAIN, SERVICE_ALARM_ARM_HOME_INSTANT, SERVICE_ALARM_ARM_NIGHT_INSTANT, SERVICE_ALARM_ARM_VACATION, SERVICE_ALARM_DISPLAY_MESSAGE, - ElkEntity, + ElkAttachedEntity, create_elk_entities, ) - -SIGNAL_ARM_ENTITY = "elkm1_arm" -SIGNAL_DISPLAY_MESSAGE = "elkm1_display_message" +from .const import DOMAIN ELK_ALARM_SERVICE_SCHEMA = vol.Schema( { @@ -61,69 +57,57 @@ DISPLAY_MESSAGE_SERVICE_SCHEMA = vol.Schema( } ) +_LOGGER = logging.getLogger(__name__) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the ElkM1 alarm platform.""" - if discovery_info is None: - return - - elk_datas = hass.data[DOMAIN] + elk_data = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for elk_data in elk_datas.values(): - elk = elk_data["elk"] - entities = create_elk_entities(elk_data, elk.areas, "area", ElkArea, entities) + + elk = elk_data["elk"] + areas_with_keypad = set() + for keypad in elk.keypads: + areas_with_keypad.add(keypad.area) + + areas = [] + for area in elk.areas: + if area.index in areas_with_keypad or elk_data["auto_configure"] is False: + areas.append(area) + create_elk_entities(elk_data, areas, "area", ElkArea, entities) async_add_entities(entities, True) - def _dispatch(signal, entity_ids, *args): - for entity_id in entity_ids: - async_dispatcher_send(hass, f"{signal}_{entity_id}", *args) + platform = entity_platform.current_platform.get() - def _arm_service(service): - entity_ids = service.data.get(ATTR_ENTITY_ID, []) - arm_level = _arm_services().get(service.service) - args = (arm_level, service.data.get(ATTR_CODE)) - _dispatch(SIGNAL_ARM_ENTITY, entity_ids, *args) - - for service in _arm_services(): - hass.services.async_register( - DOMAIN, service, _arm_service, ELK_ALARM_SERVICE_SCHEMA - ) - - def _display_message_service(service): - entity_ids = service.data.get(ATTR_ENTITY_ID, []) - data = service.data - args = ( - data["clear"], - data["beep"], - data["timeout"], - data["line1"], - data["line2"], - ) - _dispatch(SIGNAL_DISPLAY_MESSAGE, entity_ids, *args) - - hass.services.async_register( - DOMAIN, + platform.async_register_entity_service( + SERVICE_ALARM_ARM_VACATION, + ELK_ALARM_SERVICE_SCHEMA, + "async_alarm_arm_vacation", + ) + platform.async_register_entity_service( + SERVICE_ALARM_ARM_HOME_INSTANT, + ELK_ALARM_SERVICE_SCHEMA, + "async_alarm_arm_home_instant", + ) + platform.async_register_entity_service( + SERVICE_ALARM_ARM_NIGHT_INSTANT, + ELK_ALARM_SERVICE_SCHEMA, + "async_alarm_arm_night_instant", + ) + platform.async_register_entity_service( SERVICE_ALARM_DISPLAY_MESSAGE, - _display_message_service, DISPLAY_MESSAGE_SERVICE_SCHEMA, + "async_display_message", ) -def _arm_services(): - return { - SERVICE_ALARM_ARM_VACATION: ArmLevel.ARMED_VACATION.value, - SERVICE_ALARM_ARM_HOME_INSTANT: ArmLevel.ARMED_STAY_INSTANT.value, - SERVICE_ALARM_ARM_NIGHT_INSTANT: ArmLevel.ARMED_NIGHT_INSTANT.value, - } - - -class ElkArea(ElkEntity, AlarmControlPanel): +class ElkArea(ElkAttachedEntity, AlarmControlPanel): """Representation of an Area / Partition within the ElkM1 alarm panel.""" def __init__(self, element, elk, elk_data): """Initialize Area as Alarm Control Panel.""" super().__init__(element, elk, elk_data) - self._changed_by_entity_id = "" + self._changed_by_keypad = None self._state = None async def async_added_to_hass(self): @@ -131,23 +115,13 @@ class ElkArea(ElkEntity, AlarmControlPanel): await super().async_added_to_hass() for keypad in self._elk.keypads: keypad.add_callback(self._watch_keypad) - async_dispatcher_connect( - self.hass, f"{SIGNAL_ARM_ENTITY}_{self.entity_id}", self._arm_service - ) - async_dispatcher_connect( - self.hass, - f"{SIGNAL_DISPLAY_MESSAGE}_{self.entity_id}", - self._display_message, - ) def _watch_keypad(self, keypad, changeset): if keypad.area != self._element.index: return if changeset.get("last_user") is not None: - self._changed_by_entity_id = self.hass.data[DOMAIN][self._prefix][ - "keypads" - ].get(keypad.index, "") - self.async_schedule_update_ha_state(True) + self._changed_by_keypad = keypad.name + self.async_write_ha_state() @property def code_format(self): @@ -178,7 +152,7 @@ class ElkArea(ElkEntity, AlarmControlPanel): attrs["arm_up_state"] = ArmUpState(elmt.arm_up_state).name.lower() if elmt.alarm_state is not None: attrs["alarm_state"] = AlarmState(elmt.alarm_state).name.lower() - attrs["changed_by_entity_id"] = self._changed_by_entity_id + attrs["changed_by_keypad"] = self._changed_by_keypad return attrs def _element_changed(self, element, changeset): @@ -225,9 +199,18 @@ class ElkArea(ElkEntity, AlarmControlPanel): """Send arm night command.""" self._element.arm(ArmLevel.ARMED_NIGHT.value, int(code)) - async def _arm_service(self, arm_level, code): - self._element.arm(arm_level, code) + async def async_alarm_arm_home_instant(self, code=None): + """Send arm stay instant command.""" + self._element.arm(ArmLevel.ARMED_STAY_INSTANT.value, int(code)) - async def _display_message(self, clear, beep, timeout, line1, line2): + async def async_alarm_arm_night_instant(self, code=None): + """Send arm night instant command.""" + self._element.arm(ArmLevel.ARMED_NIGHT_INSTANT.value, int(code)) + + async def async_alarm_arm_vacation(self, code=None): + """Send arm vacation command.""" + self._element.arm(ArmLevel.ARMED_VACATION.value, int(code)) + + async def async_display_message(self, clear, beep, timeout, line1, line2): """Display a message on all keypads for the area.""" self._element.display_message(clear, beep, timeout, line1, line2) diff --git a/homeassistant/components/elkm1/climate.py b/homeassistant/components/elkm1/climate.py index abc9dc0933c..3c5c70b2bd0 100644 --- a/homeassistant/components/elkm1/climate.py +++ b/homeassistant/components/elkm1/climate.py @@ -14,9 +14,10 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.const import PRECISION_WHOLE, STATE_ON +from homeassistant.const import PRECISION_WHOLE, STATE_ON, TEMP_CELSIUS, TEMP_FAHRENHEIT -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ElkEntity, create_elk_entities +from .const import DOMAIN SUPPORT_HVAC = [ HVAC_MODE_OFF, @@ -27,18 +28,14 @@ SUPPORT_HVAC = [ ] -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the Elk-M1 thermostat platform.""" - if discovery_info is None: - return - - elk_datas = hass.data[ELK_DOMAIN] + elk_data = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for elk_data in elk_datas.values(): - elk = elk_data["elk"] - entities = create_elk_entities( - elk_data, elk.thermostats, "thermostat", ElkThermostat, entities - ) + elk = elk_data["elk"] + create_elk_entities( + elk_data, elk.thermostats, "thermostat", ElkThermostat, entities + ) async_add_entities(entities, True) @@ -58,7 +55,7 @@ class ElkThermostat(ElkEntity, ClimateDevice): @property def temperature_unit(self): """Return the temperature unit.""" - return self._temperature_unit + return TEMP_FAHRENHEIT if self._temperature_unit == "F" else TEMP_CELSIUS @property def current_temperature(self): diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py new file mode 100644 index 00000000000..cad3ecac42a --- /dev/null +++ b/homeassistant/components/elkm1/config_flow.py @@ -0,0 +1,164 @@ +"""Config flow for Elk-M1 Control integration.""" +import logging +from urllib.parse import urlparse + +import elkm1_lib as elkm1 +import voluptuous as vol + +from homeassistant import config_entries, exceptions +from homeassistant.const import ( + CONF_ADDRESS, + CONF_HOST, + CONF_PASSWORD, + CONF_PROTOCOL, + CONF_TEMPERATURE_UNIT, + CONF_USERNAME, +) +from homeassistant.util import slugify + +from . import async_wait_for_elk_to_sync +from .const import CONF_AUTO_CONFIGURE, CONF_PREFIX +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +PROTOCOL_MAP = {"secure": "elks://", "non-secure": "elk://", "serial": "serial://"} + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PROTOCOL, default="secure"): vol.In( + ["secure", "non-secure", "serial"] + ), + vol.Required(CONF_ADDRESS): str, + vol.Optional(CONF_USERNAME, default=""): str, + vol.Optional(CONF_PASSWORD, default=""): str, + vol.Optional(CONF_PREFIX, default=""): str, + vol.Optional(CONF_TEMPERATURE_UNIT, default="F"): vol.In(["F", "C"]), + } +) + +VALIDATE_TIMEOUT = 35 + + +async def validate_input(data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + userid = data.get(CONF_USERNAME) + password = data.get(CONF_PASSWORD) + + prefix = data[CONF_PREFIX] + url = _make_url_from_data(data) + requires_password = url.startswith("elks://") + + if requires_password and (not userid or not password): + raise InvalidAuth + + elk = elkm1.Elk( + {"url": url, "userid": userid, "password": password, "element_list": ["panel"]} + ) + elk.connect() + + timed_out = False + if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT): + _LOGGER.error( + "Timed out after %d seconds while trying to sync with elkm1", + VALIDATE_TIMEOUT, + ) + timed_out = True + + elk.disconnect() + + if timed_out: + raise CannotConnect + if elk.invalid_auth: + raise InvalidAuth + + device_name = data[CONF_PREFIX] if data[CONF_PREFIX] else "ElkM1" + # Return info that you want to store in the config entry. + return {"title": device_name, CONF_HOST: url, CONF_PREFIX: slugify(prefix)} + + +def _make_url_from_data(data): + host = data.get(CONF_HOST) + if host: + return host + + protocol = PROTOCOL_MAP[data[CONF_PROTOCOL]] + address = data[CONF_ADDRESS] + return f"{protocol}{address}" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Elk-M1 Control.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + def __init__(self): + """Initialize the elkm1 config flow.""" + self.importing = False + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if self._url_already_configured(_make_url_from_data(user_input)): + return self.async_abort(reason="address_already_configured") + + try: + info = await validate_input(user_input) + + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + + if "base" not in errors: + await self.async_set_unique_id(user_input[CONF_PREFIX]) + self._abort_if_unique_id_configured() + + if self.importing: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_create_entry( + title=info["title"], + data={ + CONF_HOST: info[CONF_HOST], + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + CONF_AUTO_CONFIGURE: True, + CONF_TEMPERATURE_UNIT: user_input[CONF_TEMPERATURE_UNIT], + CONF_PREFIX: info[CONF_PREFIX], + }, + ) + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) + + async def async_step_import(self, user_input): + """Handle import.""" + self.importing = True + return await self.async_step_user(user_input) + + def _url_already_configured(self, url): + """See if we already have a elkm1 matching user input configured.""" + existing_hosts = { + urlparse(entry.data[CONF_HOST]).hostname + for entry in self._async_current_entries() + } + return urlparse(url).hostname in existing_hosts + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py new file mode 100644 index 00000000000..bad6d7fbcf1 --- /dev/null +++ b/homeassistant/components/elkm1/const.py @@ -0,0 +1,31 @@ +"""Support the ElkM1 Gold and ElkM1 EZ8 alarm/integration panels.""" + +from elkm1_lib.const import Max + +DOMAIN = "elkm1" + +CONF_AUTO_CONFIGURE = "auto_configure" +CONF_AREA = "area" +CONF_COUNTER = "counter" +CONF_ENABLED = "enabled" +CONF_KEYPAD = "keypad" +CONF_OUTPUT = "output" +CONF_PLC = "plc" +CONF_SETTING = "setting" +CONF_TASK = "task" +CONF_THERMOSTAT = "thermostat" +CONF_ZONE = "zone" +CONF_PREFIX = "prefix" + + +ELK_ELEMENTS = { + CONF_AREA: Max.AREAS.value, + CONF_COUNTER: Max.COUNTERS.value, + CONF_KEYPAD: Max.KEYPADS.value, + CONF_OUTPUT: Max.OUTPUTS.value, + CONF_PLC: Max.LIGHTS.value, + CONF_SETTING: Max.SETTINGS.value, + CONF_TASK: Max.TASKS.value, + CONF_THERMOSTAT: Max.THERMOSTATS.value, + CONF_ZONE: Max.ZONES.value, +} diff --git a/homeassistant/components/elkm1/light.py b/homeassistant/components/elkm1/light.py index 10a9ae1b931..b7cfe20dfd8 100644 --- a/homeassistant/components/elkm1/light.py +++ b/homeassistant/components/elkm1/light.py @@ -1,18 +1,17 @@ """Support for control of ElkM1 lighting (X10, UPB, etc).""" + from homeassistant.components.light import ATTR_BRIGHTNESS, SUPPORT_BRIGHTNESS, Light -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ElkEntity, create_elk_entities +from .const import DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the Elk light platform.""" - if discovery_info is None: - return - elk_datas = hass.data[ELK_DOMAIN] + elk_data = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for elk_data in elk_datas.values(): - elk = elk_data["elk"] - create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities) + elk = elk_data["elk"] + create_elk_entities(elk_data, elk.lights, "plc", ElkLight, entities) async_add_entities(entities, True) diff --git a/homeassistant/components/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index c75da1ef039..17b016fcb8b 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,12 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==0.7.15"], + "requirements": [ + "elkm1-lib==0.7.17" + ], "dependencies": [], - "codeowners": [] + "codeowners": [ + "@bdraco" + ], + "config_flow": true } diff --git a/homeassistant/components/elkm1/scene.py b/homeassistant/components/elkm1/scene.py index dc5ea39d154..1f894cc7681 100644 --- a/homeassistant/components/elkm1/scene.py +++ b/homeassistant/components/elkm1/scene.py @@ -1,22 +1,20 @@ """Support for control of ElkM1 tasks ("macros").""" from homeassistant.components.scene import Scene -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ElkAttachedEntity, create_elk_entities +from .const import DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the Elk-M1 scene platform.""" - if discovery_info is None: - return - elk_datas = hass.data[ELK_DOMAIN] + elk_data = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for elk_data in elk_datas.values(): - elk = elk_data["elk"] - entities = create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities) + elk = elk_data["elk"] + create_elk_entities(elk_data, elk.tasks, "task", ElkTask, entities) async_add_entities(entities, True) -class ElkTask(ElkEntity, Scene): +class ElkTask(ElkAttachedEntity, Scene): """Elk-M1 task as scene.""" async def async_activate(self): diff --git a/homeassistant/components/elkm1/sensor.py b/homeassistant/components/elkm1/sensor.py index df29e1cda7e..79987d806a1 100644 --- a/homeassistant/components/elkm1/sensor.py +++ b/homeassistant/components/elkm1/sensor.py @@ -7,31 +7,22 @@ from elkm1_lib.const import ( ) from elkm1_lib.util import pretty_const, username -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ElkAttachedEntity, create_elk_entities +from .const import DOMAIN + +UNDEFINED_TEMPATURE = -40 -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the Elk-M1 sensor platform.""" - if discovery_info is None: - return - - elk_datas = hass.data[ELK_DOMAIN] + elk_data = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for elk_data in elk_datas.values(): - elk = elk_data["elk"] - entities = create_elk_entities( - elk_data, elk.counters, "counter", ElkCounter, entities - ) - entities = create_elk_entities( - elk_data, elk.keypads, "keypad", ElkKeypad, entities - ) - entities = create_elk_entities( - elk_data, [elk.panel], "panel", ElkPanel, entities - ) - entities = create_elk_entities( - elk_data, elk.settings, "setting", ElkSetting, entities - ) - entities = create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) + elk = elk_data["elk"] + create_elk_entities(elk_data, elk.counters, "counter", ElkCounter, entities) + create_elk_entities(elk_data, elk.keypads, "keypad", ElkKeypad, entities) + create_elk_entities(elk_data, [elk.panel], "panel", ElkPanel, entities) + create_elk_entities(elk_data, elk.settings, "setting", ElkSetting, entities) + create_elk_entities(elk_data, elk.zones, "zone", ElkZone, entities) async_add_entities(entities, True) @@ -40,7 +31,7 @@ def temperature_to_state(temperature, undefined_temperature): return temperature if temperature > undefined_temperature else None -class ElkSensor(ElkEntity): +class ElkSensor(ElkAttachedEntity): """Base representation of Elk-M1 sensor.""" def __init__(self, element, elk, elk_data): @@ -89,7 +80,7 @@ class ElkKeypad(ElkSensor): """Attributes of the sensor.""" attrs = self.initial_attrs() attrs["area"] = self._element.area + 1 - attrs["temperature"] = self._element.temperature + attrs["temperature"] = self._state attrs["last_user_time"] = self._element.last_user_time.isoformat() attrs["last_user"] = self._element.last_user + 1 attrs["code"] = self._element.code @@ -98,14 +89,9 @@ class ElkKeypad(ElkSensor): return attrs def _element_changed(self, element, changeset): - self._state = temperature_to_state(self._element.temperature, -40) - - async def async_added_to_hass(self): - """Register callback for ElkM1 changes and update entity state.""" - await super().async_added_to_hass() - elk_datas = self.hass.data[ELK_DOMAIN] - for elk_data in elk_datas.values(): - elk_data["keypads"][self._element.index] = self.entity_id + self._state = temperature_to_state( + self._element.temperature, UNDEFINED_TEMPATURE + ) class ElkPanel(ElkSensor): @@ -214,7 +200,9 @@ class ElkZone(ElkSensor): def _element_changed(self, element, changeset): if self._element.definition == ZoneType.TEMPERATURE.value: - self._state = temperature_to_state(self._element.temperature, -60) + self._state = temperature_to_state( + self._element.temperature, UNDEFINED_TEMPATURE + ) elif self._element.definition == ZoneType.ANALOG_ZONE.value: self._state = self._element.voltage else: diff --git a/homeassistant/components/elkm1/strings.json b/homeassistant/components/elkm1/strings.json new file mode 100644 index 00000000000..a5246a004c3 --- /dev/null +++ b/homeassistant/components/elkm1/strings.json @@ -0,0 +1,28 @@ +{ + "config": { + "title": "Elk-M1 Control", + "step": { + "user": { + "title": "Connect to Elk-M1 Control", + "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", + "data": { + "protocol": "Protocol", + "address": "The IP address or domain or serial port if connecting via serial.", + "username": "Username (secure only).", + "password": "Password (secure only).", + "prefix": "A unique prefix (leave blank if you only have one ElkM1).", + "temperature_unit": "The temperature unit ElkM1 uses." + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "An ElkM1 with this prefix is already configured", + "address_already_configured": "An ElkM1 with this address is already configured" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/switch.py b/homeassistant/components/elkm1/switch.py index e6dd82dc0ac..af32e81bc4c 100644 --- a/homeassistant/components/elkm1/switch.py +++ b/homeassistant/components/elkm1/switch.py @@ -1,24 +1,20 @@ """Support for control of ElkM1 outputs (relays).""" from homeassistant.components.switch import SwitchDevice -from . import DOMAIN as ELK_DOMAIN, ElkEntity, create_elk_entities +from . import ElkAttachedEntity, create_elk_entities +from .const import DOMAIN -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, config_entry, async_add_entities): """Create the Elk-M1 switch platform.""" - if discovery_info is None: - return - elk_datas = hass.data[ELK_DOMAIN] + elk_data = hass.data[DOMAIN][config_entry.entry_id] entities = [] - for elk_data in elk_datas.values(): - elk = elk_data["elk"] - entities = create_elk_entities( - elk_data, elk.outputs, "output", ElkOutput, entities - ) + elk = elk_data["elk"] + create_elk_entities(elk_data, elk.outputs, "output", ElkOutput, entities) async_add_entities(entities, True) -class ElkOutput(ElkEntity, SwitchDevice): +class ElkOutput(ElkAttachedEntity, SwitchDevice): """Elk output as switch.""" @property diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 8c03702e8f9..05bc4a7ba4a 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -28,6 +28,7 @@ FLOWS = [ "dynalite", "ecobee", "elgato", + "elkm1", "emulated_roku", "esphome", "freebox", diff --git a/requirements_all.txt b/requirements_all.txt index f0ba017f574..8d377e3602d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -495,7 +495,7 @@ elgato==0.2.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.7.15 +elkm1-lib==0.7.17 # homeassistant.components.emulated_roku emulated_roku==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f94245b6df5..7bded1c1d21 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -198,6 +198,9 @@ eebrightbox==0.0.4 # homeassistant.components.elgato elgato==0.2.0 +# homeassistant.components.elkm1 +elkm1-lib==0.7.17 + # homeassistant.components.emulated_roku emulated_roku==0.2.1 diff --git a/tests/components/elkm1/__init__.py b/tests/components/elkm1/__init__.py new file mode 100644 index 00000000000..8ae7f6d7b49 --- /dev/null +++ b/tests/components/elkm1/__init__.py @@ -0,0 +1 @@ +"""Tests for the Elk-M1 Control integration.""" diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py new file mode 100644 index 00000000000..466005f3d43 --- /dev/null +++ b/tests/components/elkm1/test_config_flow.py @@ -0,0 +1,269 @@ +"""Test the Elk-M1 Control config flow.""" + +from asynctest import CoroutineMock, MagicMock, PropertyMock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.elkm1.const import DOMAIN + + +def mock_elk(invalid_auth=None, sync_complete=None): + """Mock m1lib Elk.""" + mocked_elk = MagicMock() + type(mocked_elk).invalid_auth = PropertyMock(return_value=invalid_auth) + type(mocked_elk).sync_complete = CoroutineMock() + return mocked_elk + + +async def test_form_user_with_secure_elk(hass): + """Test we can setup a secure elk.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False) + + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + ), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "temperature_unit": "F", + "prefix": "", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "elks://1.2.3.4", + "password": "test-password", + "prefix": "", + "temperature_unit": "F", + "username": "test-username", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_non_secure_elk(hass): + """Test we can setup a non-secure elk.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False) + + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + ), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "non-secure", + "address": "1.2.3.4", + "temperature_unit": "F", + "prefix": "guest_house", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "guest_house" + assert result2["data"] == { + "auto_configure": True, + "host": "elk://1.2.3.4", + "prefix": "guest_house", + "username": "", + "password": "", + "temperature_unit": "F", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_user_with_serial_elk(hass): + """Test we can setup a serial elk.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mocked_elk = mock_elk(invalid_auth=False) + + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + ), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "serial", + "address": "/dev/ttyS0:115200", + "temperature_unit": "F", + "prefix": "", + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "ElkM1" + assert result2["data"] == { + "auto_configure": True, + "host": "serial:///dev/ttyS0:115200", + "prefix": "", + "username": "", + "password": "", + "temperature_unit": "F", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mocked_elk = mock_elk(invalid_auth=False) + + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + ), patch( + "homeassistant.components.elkm1.config_flow.async_wait_for_elk_to_sync", + return_value=False, + ): # async_wait_for_elk_to_sync is being patched to avoid making the test wait 45s + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "temperature_unit": "F", + "prefix": "", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_invalid_auth(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mocked_elk = mock_elk(invalid_auth=True) + + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "protocol": "secure", + "address": "1.2.3.4", + "username": "test-username", + "password": "test-password", + "temperature_unit": "F", + "prefix": "", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_elk = mock_elk(invalid_auth=False) + with patch( + "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, + ), patch( + "homeassistant.components.elkm1.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.elkm1.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={ + "host": "elks://1.2.3.4", + "username": "friend", + "password": "love", + "temperature_unit": "C", + "auto_configure": False, + "keypad": { + "enabled": True, + "exclude": [], + "include": [[1, 1], [2, 2], [3, 3]], + }, + "output": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "zone": { + "enabled": True, + "exclude": [[15, 15], [28, 208]], + "include": [], + }, + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "ohana" + + assert result["data"] == { + "auto_configure": False, + "host": "elks://1.2.3.4", + "keypad": {"enabled": True, "exclude": [], "include": [[1, 1], [2, 2], [3, 3]]}, + "output": {"enabled": False, "exclude": [], "include": []}, + "password": "love", + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "temperature_unit": "C", + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "username": "friend", + "zone": {"enabled": True, "exclude": [[15, 15], [28, 208]], "include": []}, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 From 00c38c5f706dc04171f69b94793d47ded00367ca Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2020 15:40:54 -0500 Subject: [PATCH 280/431] Bump nexia to 0.7.3 (#33311) * Resolves not being able to see the compressor speed. --- homeassistant/components/nexia/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index d98dbcb2272..06130f605ef 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -2,7 +2,7 @@ "domain": "nexia", "name": "Nexia", "requirements": [ - "nexia==0.7.2" + "nexia==0.7.3" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 8d377e3602d..3432f8c8ffd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.7.2 +nexia==0.7.3 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7bded1c1d21..a2b29a3c0ba 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.7.2 +nexia==0.7.3 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From 5f72ad8da682ad33a3274eff1b39b8a1508791e6 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sat, 28 Mar 2020 00:08:09 +0000 Subject: [PATCH 281/431] [ci skip] Translation update --- .../components/deconz/.translations/fr.json | 3 +- .../components/demo/.translations/fr.json | 17 +++++++ .../components/doorbird/.translations/lb.json | 3 +- .../components/elkm1/.translations/en.json | 50 +++++++++---------- .../konnected/.translations/fr.json | 1 + .../components/melcloud/.translations/fr.json | 1 + .../monoprice/.translations/lb.json | 3 +- .../components/mqtt/.translations/fr.json | 15 ++++++ .../components/nuheat/.translations/lb.json | 1 + .../pvpc_hourly_pricing/.translations/lb.json | 10 +++- .../components/rachio/.translations/lb.json | 16 +++++- .../components/roku/.translations/lb.json | 4 ++ .../components/spotify/.translations/fr.json | 1 + .../components/tesla/.translations/lb.json | 1 + .../components/vilfo/.translations/fr.json | 5 ++ .../components/vizio/.translations/fr.json | 3 +- .../components/vizio/.translations/lb.json | 12 +++++ .../components/withings/.translations/fr.json | 1 + .../components/wwlln/.translations/lb.json | 3 ++ 19 files changed, 118 insertions(+), 32 deletions(-) diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 60c18217aef..2213dcf2d49 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -108,7 +108,8 @@ "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" }, - "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" + "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ", + "title": "Options deCONZ" } } } diff --git a/homeassistant/components/demo/.translations/fr.json b/homeassistant/components/demo/.translations/fr.json index bc093330c26..3621cd1c404 100644 --- a/homeassistant/components/demo/.translations/fr.json +++ b/homeassistant/components/demo/.translations/fr.json @@ -1,5 +1,22 @@ { "config": { "title": "D\u00e9mo" + }, + "options": { + "step": { + "options_1": { + "data": { + "bool": "Bool\u00e9en facultatif", + "int": "Entr\u00e9e num\u00e9rique" + } + }, + "options_2": { + "data": { + "multi": "S\u00e9lection multiple", + "select": "S\u00e9lectionnez une option", + "string": "Valeur de cha\u00eene" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/lb.json b/homeassistant/components/doorbird/.translations/lb.json index e5a7322a59e..936dfddf261 100644 --- a/homeassistant/components/doorbird/.translations/lb.json +++ b/homeassistant/components/doorbird/.translations/lb.json @@ -26,7 +26,8 @@ "init": { "data": { "events": "Komma getrennte L\u00ebscht vun Evenementer" - } + }, + "description": "Setzt ee mat Komma getrennten Evenement Numm fir all Evenement dob\u00e4i d\u00e9i sollt suiv\u00e9iert ginn. Wann's du se hei aginn hues, benotz d'DoorBird App fir se zu engem spezifeschen Evenement dob\u00e4i ze setzen. Kuckt d'Dokumentatioun op https://www.home-assistant.io/integrations/doorbird/#events. Beispill: somebody_pressed_the_button, motion" } } } diff --git a/homeassistant/components/elkm1/.translations/en.json b/homeassistant/components/elkm1/.translations/en.json index a5246a004c3..7671e250bf3 100644 --- a/homeassistant/components/elkm1/.translations/en.json +++ b/homeassistant/components/elkm1/.translations/en.json @@ -1,28 +1,28 @@ { - "config": { - "title": "Elk-M1 Control", - "step": { - "user": { - "title": "Connect to Elk-M1 Control", - "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", - "data": { - "protocol": "Protocol", - "address": "The IP address or domain or serial port if connecting via serial.", - "username": "Username (secure only).", - "password": "Password (secure only).", - "prefix": "A unique prefix (leave blank if you only have one ElkM1).", - "temperature_unit": "The temperature unit ElkM1 uses." - } - } - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "An ElkM1 with this prefix is already configured", - "address_already_configured": "An ElkM1 with this address is already configured" + "config": { + "abort": { + "address_already_configured": "An ElkM1 with this address is already configured", + "already_configured": "An ElkM1 with this prefix is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "address": "The IP address or domain or serial port if connecting via serial.", + "password": "Password (secure only).", + "prefix": "A unique prefix (leave blank if you only have one ElkM1).", + "protocol": "Protocol", + "temperature_unit": "The temperature unit ElkM1 uses.", + "username": "Username (secure only)." + }, + "description": "The address string must be in the form 'address[:port]' for 'secure' and 'non-secure'. Example: '192.168.1.1'. The port is optional and defaults to 2101 for 'non-secure' and 2601 for 'secure'. For the serial protocol, the address must be in the form 'tty[:baud]'. Example: '/dev/ttyS1'. The baud is optional and defaults to 115200.", + "title": "Connect to Elk-M1 Control" + } + }, + "title": "Elk-M1 Control" } - } } \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/fr.json b/homeassistant/components/konnected/.translations/fr.json index c41819369a0..fecdd35b808 100644 --- a/homeassistant/components/konnected/.translations/fr.json +++ b/homeassistant/components/konnected/.translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", + "already_in_progress": "Le flux de configuration de l'appareil est d\u00e9j\u00e0 en cours.", "not_konn_panel": "Non reconnu comme appareil Konnected.io", "unknown": "Une erreur inconnue s'est produite" }, diff --git a/homeassistant/components/melcloud/.translations/fr.json b/homeassistant/components/melcloud/.translations/fr.json index e442325d9dc..00661d3f0af 100644 --- a/homeassistant/components/melcloud/.translations/fr.json +++ b/homeassistant/components/melcloud/.translations/fr.json @@ -8,6 +8,7 @@ "step": { "user": { "data": { + "password": "Mot de passe MELCloud.", "username": "E-mail utilis\u00e9e pour vous connecter \u00e0 MELCloud." }, "description": "Se connecter en utilisant votre MELCloud compte.", diff --git a/homeassistant/components/monoprice/.translations/lb.json b/homeassistant/components/monoprice/.translations/lb.json index 6f530fa8a80..abb7caf4183 100644 --- a/homeassistant/components/monoprice/.translations/lb.json +++ b/homeassistant/components/monoprice/.translations/lb.json @@ -20,7 +20,8 @@ }, "title": "Mam Apparat verbannen" } - } + }, + "title": "Monoprice 6-Zone Verst\u00e4rker" }, "options": { "step": { diff --git a/homeassistant/components/mqtt/.translations/fr.json b/homeassistant/components/mqtt/.translations/fr.json index 648c2f972d7..eaf930b9a2d 100644 --- a/homeassistant/components/mqtt/.translations/fr.json +++ b/homeassistant/components/mqtt/.translations/fr.json @@ -27,5 +27,20 @@ } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Premier bouton", + "button_2": "Deuxi\u00e8me bouton", + "button_3": "Troisi\u00e8me bouton", + "button_4": "Quatri\u00e8me bouton", + "button_5": "Cinqui\u00e8me bouton", + "button_6": "Sixi\u00e8me bouton", + "turn_off": "\u00c9teindre", + "turn_on": "Allumer" + }, + "trigger_type": { + "button_short_press": "\" {subtype} \" press\u00e9" + } } } \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/lb.json b/homeassistant/components/nuheat/.translations/lb.json index cba8bb91597..fd2b3114d4d 100644 --- a/homeassistant/components/nuheat/.translations/lb.json +++ b/homeassistant/components/nuheat/.translations/lb.json @@ -16,6 +16,7 @@ "serial_number": "Seriennummer vum Thermostat", "username": "Benotzernumm" }, + "description": "Du brauchs d'Seriennummer oder ID vum Thermostat, andeems Du dech op https://MyNuHeat.com umells an den Thermostat auswielt.", "title": "Mat NuHeat verbannen" } }, diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json b/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json index 4fa14c495de..bed6af70e13 100644 --- a/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/lb.json @@ -1,12 +1,18 @@ { "config": { + "abort": { + "already_configured": "Integratioun ass scho konfigur\u00e9iert mat engem Sensor mat deem Tarif" + }, "step": { "user": { "data": { - "name": "Numm vum Sensor" + "name": "Numm vum Sensor", + "tariff": "Kontraktuellen Tarif (1, 2 oder 3 Perioden)" }, + "description": "D\u00ebse Sensor benotzt d\u00e9i offiziell API fir de [Stonne Pr\u00e4is fir Elektrizit\u00e9it a Spuenien (PVPC)](https://www.esios.ree.es/es/pvpc) ze kr\u00e9ien. Fir m\u00e9i pr\u00e4zise Erkl\u00e4runge kuck [Dokumentatioun vun der Integratioun](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nWiel den Taux bas\u00e9ierend op der Unzuel vun de Rechnungsz\u00e4ite pro Dag aus:\n- 1 Period: Normal\n- 2 perioden: Nuets Tarif\n- 3 Perioden: Elektreschen Auto (Nuets Tarif fir 3 Perioden)", "title": "Auswiel vum Tarif" } - } + }, + "title": "Stonne Pr\u00e4is fir Elektrizit\u00e9it a Spuenien (PVPC)" } } \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/lb.json b/homeassistant/components/rachio/.translations/lb.json index 24c1b8c382d..d43d4d9a044 100644 --- a/homeassistant/components/rachio/.translations/lb.json +++ b/homeassistant/components/rachio/.translations/lb.json @@ -5,13 +5,27 @@ }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", - "invalid_auth": "Ong\u00eblteg Authentifikatioun" + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" }, "step": { "user": { + "data": { + "api_key": "API Schl\u00ebssel fir den Racchio Kont." + }, + "description": "Du brauchs een API Schl\u00ebssel vun https://app.rach.io/. Wiel 'Account Settings', a klick dann op 'GET API KEY'.", "title": "Mam Rachio Apparat verbannen" } }, "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Fir w\u00e9i laang, a Minutten, soll eng Statioun ugeschalt gi wann de Schalter ageschalt ass." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/roku/.translations/lb.json b/homeassistant/components/roku/.translations/lb.json index 2e532bf6a93..789dac2eed7 100644 --- a/homeassistant/components/roku/.translations/lb.json +++ b/homeassistant/components/roku/.translations/lb.json @@ -11,6 +11,10 @@ "flow_title": "Roku: {name}", "step": { "ssdp_confirm": { + "data": { + "one": "Een", + "other": "Aaner" + }, "description": "Soll {name} konfigur\u00e9iert ginn?", "title": "Roku" }, diff --git a/homeassistant/components/spotify/.translations/fr.json b/homeassistant/components/spotify/.translations/fr.json index b6ec983df76..9c233b9b947 100644 --- a/homeassistant/components/spotify/.translations/fr.json +++ b/homeassistant/components/spotify/.translations/fr.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_setup": "Vous ne pouvez configurer qu'un seul compte Spotify.", + "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", "missing_configuration": "L'int\u00e9gration Spotify n'est pas configur\u00e9e. Veuillez suivre la documentation." }, "create_entry": { diff --git a/homeassistant/components/tesla/.translations/lb.json b/homeassistant/components/tesla/.translations/lb.json index fa63c5a289a..64bf528e95f 100644 --- a/homeassistant/components/tesla/.translations/lb.json +++ b/homeassistant/components/tesla/.translations/lb.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Forc\u00e9ier d'Erw\u00e4chen vun den Autoen beim starten", "scan_interval": "Sekonnen t\u00ebscht Scannen" } } diff --git a/homeassistant/components/vilfo/.translations/fr.json b/homeassistant/components/vilfo/.translations/fr.json index 6abeb789f23..64e48adc573 100644 --- a/homeassistant/components/vilfo/.translations/fr.json +++ b/homeassistant/components/vilfo/.translations/fr.json @@ -1,5 +1,10 @@ { "config": { + "step": { + "user": { + "title": "Connectez-vous au routeur Vilfo" + } + }, "title": "Routeur Vilfo" } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index 344fadd00f8..bf672e9dfb9 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -21,7 +21,8 @@ "pair_tv": { "data": { "pin": "PIN" - } + }, + "title": "Processus de couplage complet" }, "pairing_complete": { "description": "Votre appareil Vizio SmartCast est maintenant connect\u00e9 \u00e0 Home Assistant.", diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json index 5788776049d..034e2b3df00 100644 --- a/homeassistant/components/vizio/.translations/lb.json +++ b/homeassistant/components/vizio/.translations/lb.json @@ -33,6 +33,12 @@ "description": "D\u00e4in Visio SmartCast Apparat ass elo mam Home Assistant verbonnen.\n\nD\u00e4in Acc\u00e8s Jeton ass '**{access_token}**'.", "title": "Kopplung ofgeschloss" }, + "tv_apps": { + "data": { + "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", + "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" + } + }, "user": { "data": { "access_token": "Acc\u00e8ss Jeton", @@ -42,6 +48,12 @@ }, "description": "All Felder sinn noutwendeg ausser Acc\u00e8s Jeton. Wann keen Acc\u00e8s Jeton uginn ass, an den Typ vun Apparat ass 'TV', da g\u00ebtt e Kopplungs Prozess mam Apparat gestart fir een Acc\u00e8s Jeton z'erstellen.\n\nFir de Kopplung Prozess ofzesch\u00e9issen,ier op \"ofsch\u00e9cken\" klickt, pr\u00e9ift datt de Fernsee ugeschalt a mam Netzwierk verbonnen ass. Du muss och k\u00ebnnen op de Bildschierm gesinn.", "title": "Vizo Smartcast ariichten" + }, + "user_tv": { + "data": { + "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", + "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" + } } }, "title": "Vizio SmartCast" diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index 0ad55e7eaa7..a9a0db55005 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation.", "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." }, diff --git a/homeassistant/components/wwlln/.translations/lb.json b/homeassistant/components/wwlln/.translations/lb.json index c6d969894e7..a580b639d96 100644 --- a/homeassistant/components/wwlln/.translations/lb.json +++ b/homeassistant/components/wwlln/.translations/lb.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "D\u00ebse Standuert ass scho registr\u00e9iert" + }, "error": { "identifier_exists": "Standuert ass scho registr\u00e9iert" }, From 369ffe228847f05ac062bb6334406bde6dbfc7d2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 27 Mar 2020 23:02:51 -0500 Subject: [PATCH 282/431] Fix setting HomeKit temperatures with imperial units (#33315) In PR#24804 the step size was changed to match the step size of the device that HomeKit is controlling. Since HomeKit is always working in TEMP_CELSIUS and the step size is based on the Home Assistant units setting, we were mixing data with different units of measure when Home Assistant was using imperial units. This regression presented the symptom that setting the temperature to 73F would result in 74F. Other values are affected where the math doesn't happen to work out. HomeKit currently has a default of 0.1 in the spec and the override of this value has been removed. --- .../components/homekit/type_thermostats.py | 42 ++++++++----------- .../homekit/test_type_thermostats.py | 10 ++--- 2 files changed, 22 insertions(+), 30 deletions(-) diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 79a9d156f10..19f7899d79b 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -12,7 +12,6 @@ from homeassistant.components.climate.const import ( ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, - ATTR_TARGET_TEMP_STEP, CURRENT_HVAC_COOL, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, @@ -55,7 +54,6 @@ from .const import ( DEFAULT_MAX_TEMP_WATER_HEATER, DEFAULT_MIN_TEMP_WATER_HEATER, PROP_MAX_VALUE, - PROP_MIN_STEP, PROP_MIN_VALUE, SERV_THERMOSTAT, ) @@ -67,6 +65,7 @@ HC_HOMEKIT_VALID_MODES_WATER_HEATER = { "Heat": 1, } UNIT_HASS_TO_HOMEKIT = {TEMP_CELSIUS: 0, TEMP_FAHRENHEIT: 1} + UNIT_HOMEKIT_TO_HASS = {c: s for s, c in UNIT_HASS_TO_HOMEKIT.items()} HC_HASS_TO_HOMEKIT = { HVAC_MODE_OFF: 0, @@ -99,9 +98,6 @@ class Thermostat(HomeAccessory): self._flag_coolingthresh = False self._flag_heatingthresh = False min_temp, max_temp = self.get_temperature_range() - temp_step = self.hass.states.get(self.entity_id).attributes.get( - ATTR_TARGET_TEMP_STEP, 0.5 - ) # Add additional characteristics if auto mode is supported self.chars = [] @@ -162,11 +158,10 @@ class Thermostat(HomeAccessory): self.char_target_temp = serv_thermostat.configure_char( CHAR_TARGET_TEMPERATURE, value=21.0, - properties={ - PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: temp_step, - }, + # We do not set PROP_MIN_STEP here and instead use the HomeKit + # default of 0.1 in order to have enough precision to convert + # temperature units and avoid setting to 73F will result in 74F + properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, setter_callback=self.set_target_temperature, ) @@ -182,22 +177,20 @@ class Thermostat(HomeAccessory): self.char_cooling_thresh_temp = serv_thermostat.configure_char( CHAR_COOLING_THRESHOLD_TEMPERATURE, value=23.0, - properties={ - PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: temp_step, - }, + # We do not set PROP_MIN_STEP here and instead use the HomeKit + # default of 0.1 in order to have enough precision to convert + # temperature units and avoid setting to 73F will result in 74F + properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, setter_callback=self.set_cooling_threshold, ) if CHAR_HEATING_THRESHOLD_TEMPERATURE in self.chars: self.char_heating_thresh_temp = serv_thermostat.configure_char( CHAR_HEATING_THRESHOLD_TEMPERATURE, value=19.0, - properties={ - PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: temp_step, - }, + # We do not set PROP_MIN_STEP here and instead use the HomeKit + # default of 0.1 in order to have enough precision to convert + # temperature units and avoid setting to 73F will result in 74F + properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, setter_callback=self.set_heating_threshold, ) @@ -370,11 +363,10 @@ class WaterHeater(HomeAccessory): self.char_target_temp = serv_thermostat.configure_char( CHAR_TARGET_TEMPERATURE, value=50.0, - properties={ - PROP_MIN_VALUE: min_temp, - PROP_MAX_VALUE: max_temp, - PROP_MIN_STEP: 0.5, - }, + # We do not set PROP_MIN_STEP here and instead use the HomeKit + # default of 0.1 in order to have enough precision to convert + # temperature units and avoid setting to 73F will result in 74F + properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, setter_callback=self.set_target_temperature, ) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index c96cfdae602..df9a10fc409 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -102,7 +102,7 @@ async def test_thermostat(hass, hk_driver, cls, events): assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP - assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.5 + assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 hass.states.async_set( entity_id, @@ -276,10 +276,10 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert acc.char_cooling_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_cooling_thresh_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP - assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.5 + assert acc.char_cooling_thresh_temp.properties[PROP_MIN_STEP] == 0.1 assert acc.char_heating_thresh_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_heating_thresh_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP - assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.5 + assert acc.char_heating_thresh_temp.properties[PROP_MIN_STEP] == 0.1 hass.states.async_set( entity_id, @@ -517,7 +517,7 @@ async def test_thermostat_temperature_step_whole(hass, hk_driver, cls): await hass.async_add_job(acc.run) await hass.async_block_till_done() - assert acc.char_target_temp.properties[PROP_MIN_STEP] == 1.0 + assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 async def test_thermostat_restore(hass, hk_driver, cls, events): @@ -618,7 +618,7 @@ async def test_water_heater(hass, hk_driver, cls, events): assert ( acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP_WATER_HEATER ) - assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.5 + assert acc.char_target_temp.properties[PROP_MIN_STEP] == 0.1 hass.states.async_set( entity_id, From 28a2c9c653961439185ea71018a90440df535f85 Mon Sep 17 00:00:00 2001 From: ochlocracy <5885236+ochlocracy@users.noreply.github.com> Date: Sat, 28 Mar 2020 00:19:11 -0400 Subject: [PATCH 283/431] Implement Alexa.CameraStreamController in Alexa (#32772) * Implement Alexa.CameraStreamController. * Add dependencies. * Add camera helpers for image url, and mjpeg url. * Remove unsupported AlexaPowerController from cameras. * Refactor camera_stream_url to hass_url * Declare HLS instead of RTSP. * Add test for async_get_image_url() and async_get_mpeg_stream_url(). * Sort imports. * Fix camera.options to camera.stream_options. (#32767) (cherry picked from commit 9af95e85779c1cfabc6cb779df10cab72c3e7a69) * Remove URL configuration option for AlexaCameraStreamController. * Update test_initialize_camera_stream. * Optimize camera stream configuration. * Update Tests for optimized camera stream configuration. * Sort imports. * Add check for Stream integration. * Checks and Balances. * Remove unnecessary camera helpers. * Return None instead of empty list for camera_stream_configurations(). --- homeassistant/components/alexa/__init__.py | 6 +- .../components/alexa/capabilities.py | 46 ++++++ homeassistant/components/alexa/entities.py | 45 ++++++ homeassistant/components/alexa/handlers.py | 27 ++++ homeassistant/components/alexa/manifest.json | 2 +- tests/components/alexa/__init__.py | 1 + tests/components/alexa/test_smart_home.py | 143 ++++++++++++++++-- 7 files changed, 254 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/alexa/__init__.py b/homeassistant/components/alexa/__init__.py index 1355b0123b8..de5a67087ca 100644 --- a/homeassistant/components/alexa/__init__.py +++ b/homeassistant/components/alexa/__init__.py @@ -98,11 +98,7 @@ async def async_setup(hass, config): f"send command {data['request']['namespace']}/{data['request']['name']}" ) - return { - "name": "Amazon Alexa", - "message": message, - "entity_id": entity_id, - } + return {"name": "Amazon Alexa", "message": message, "entity_id": entity_id} hass.components.logbook.async_describe_event( DOMAIN, EVENT_ALEXA_SMART_HOME, async_describe_logbook_event diff --git a/homeassistant/components/alexa/capabilities.py b/homeassistant/components/alexa/capabilities.py index 6ab086ddda3..63be7df2ead 100644 --- a/homeassistant/components/alexa/capabilities.py +++ b/homeassistant/components/alexa/capabilities.py @@ -169,6 +169,11 @@ class AlexaCapability: """Return the supportedOperations object.""" return [] + @staticmethod + def camera_stream_configurations(): + """Applicable only to CameraStreamController.""" + return None + def serialize_discovery(self): """Serialize according to the Discovery API.""" result = {"type": "AlexaInterface", "interface": self.name(), "version": "3"} @@ -222,6 +227,10 @@ class AlexaCapability: if inputs: result["inputs"] = inputs + camera_stream_configurations = self.camera_stream_configurations() + if camera_stream_configurations: + result["cameraStreamConfigurations"] = camera_stream_configurations + return result def serialize_properties(self): @@ -1854,3 +1863,40 @@ class AlexaTimeHoldController(AlexaCapability): When false, Alexa does not send the Resume directive. """ return {"allowRemoteResume": self._allow_remote_resume} + + +class AlexaCameraStreamController(AlexaCapability): + """Implements Alexa.CameraStreamController. + + https://developer.amazon.com/docs/device-apis/alexa-camerastreamcontroller.html + """ + + supported_locales = { + "de-DE", + "en-AU", + "en-CA", + "en-GB", + "en-IN", + "en-US", + "es-ES", + "fr-FR", + "it-IT", + "ja-JP", + } + + def name(self): + """Return the Alexa API name of this interface.""" + return "Alexa.CameraStreamController" + + def camera_stream_configurations(self): + """Return cameraStreamConfigurations object.""" + camera_stream_configurations = [ + { + "protocols": ["HLS"], + "resolutions": [{"width": 1280, "height": 720}], + "authorizationTypes": ["NONE"], + "videoCodecs": ["H264"], + "audioCodecs": ["AAC"], + } + ] + return camera_stream_configurations diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index aa9fe40164c..fce05c8dc86 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -1,11 +1,14 @@ """Alexa entity adapters.""" +import logging from typing import List +from urllib.parse import urlparse from homeassistant.components import ( alarm_control_panel, alert, automation, binary_sensor, + camera, cover, fan, group, @@ -33,11 +36,13 @@ from homeassistant.const import ( TEMP_FAHRENHEIT, ) from homeassistant.core import callback +from homeassistant.helpers import network from homeassistant.util.decorator import Registry from .capabilities import ( Alexa, AlexaBrightnessController, + AlexaCameraStreamController, AlexaChannelController, AlexaColorController, AlexaColorTemperatureController, @@ -68,6 +73,8 @@ from .capabilities import ( ) from .const import CONF_DESCRIPTION, CONF_DISPLAY_CATEGORIES +_LOGGER = logging.getLogger(__name__) + ENTITY_ADAPTERS = Registry() TRANSLATION_TABLE = dict.fromkeys(map(ord, r"}{\/|\"()[]+~!><*%"), None) @@ -763,3 +770,41 @@ class VacuumCapabilities(AlexaEntity): yield AlexaEndpointHealth(self.hass, self.entity) yield Alexa(self.hass) + + +@ENTITY_ADAPTERS.register(camera.DOMAIN) +class CameraCapabilities(AlexaEntity): + """Class to represent Camera capabilities.""" + + def default_display_categories(self): + """Return the display categories for this entity.""" + return [DisplayCategory.CAMERA] + + def interfaces(self): + """Yield the supported interfaces.""" + if self._check_requirements(): + supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) + if supported & camera.SUPPORT_STREAM: + yield AlexaCameraStreamController(self.entity) + + yield AlexaEndpointHealth(self.hass, self.entity) + yield Alexa(self.hass) + + def _check_requirements(self): + """Check the hass URL for HTTPS scheme and port 443.""" + if "stream" not in self.hass.config.components: + _LOGGER.error( + "%s requires stream component for AlexaCameraStreamController", + self.entity_id, + ) + return False + + url = urlparse(network.async_get_external_url(self.hass)) + if url.scheme != "https" or (url.port is not None and url.port != 443): + _LOGGER.error( + "%s requires HTTPS support on port 443 for AlexaCameraStreamController", + self.entity_id, + ) + return False + + return True diff --git a/homeassistant/components/alexa/handlers.py b/homeassistant/components/alexa/handlers.py index 67083607769..b3885588b0f 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 ( + camera, cover, fan, group, @@ -41,6 +42,7 @@ from homeassistant.const import ( TEMP_CELSIUS, TEMP_FAHRENHEIT, ) +from homeassistant.helpers import network import homeassistant.util.color as color_util from homeassistant.util.decorator import Registry import homeassistant.util.dt as dt_util @@ -1523,3 +1525,28 @@ async def async_api_resume(hass, config, directive, context): ) return directive.response() + + +@HANDLERS.register(("Alexa.CameraStreamController", "InitializeCameraStreams")) +async def async_api_initialize_camera_stream(hass, config, directive, context): + """Process a InitializeCameraStreams request.""" + entity = directive.entity + stream_source = await camera.async_request_stream(hass, entity.entity_id, fmt="hls") + camera_image = hass.states.get(entity.entity_id).attributes["entity_picture"] + external_url = network.async_get_external_url(hass) + payload = { + "cameraStreams": [ + { + "uri": f"{external_url}{stream_source}", + "protocol": "HLS", + "resolution": {"width": 1280, "height": 720}, + "authorizationType": "NONE", + "videoCodec": "H264", + "audioCodec": "AAC", + } + ], + "imageUri": f"{external_url}{camera_image}", + } + return directive.response( + name="Response", namespace="Alexa.CameraStreamController", payload=payload + ) diff --git a/homeassistant/components/alexa/manifest.json b/homeassistant/components/alexa/manifest.json index bf8d4b08ba4..d47e5dea96a 100644 --- a/homeassistant/components/alexa/manifest.json +++ b/homeassistant/components/alexa/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/alexa", "requirements": [], "dependencies": ["http"], - "after_dependencies": ["logbook"], + "after_dependencies": ["logbook", "camera"], "codeowners": ["@home-assistant/cloud", "@ochlocracy"] } diff --git a/tests/components/alexa/__init__.py b/tests/components/alexa/__init__.py index fa2917edcad..04f90476c57 100644 --- a/tests/components/alexa/__init__.py +++ b/tests/components/alexa/__init__.py @@ -19,6 +19,7 @@ class MockConfig(config.AbstractConfig): "binary_sensor.test_contact_forced": {"display_categories": "CONTACT_SENSOR"}, "binary_sensor.test_motion_forced": {"display_categories": "MOTION_SENSOR"}, "binary_sensor.test_motion_camera_event": {"display_categories": "CAMERA"}, + "camera.test": {"display_categories": "CAMERA"}, } @property diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index fa8f7fbdc9a..a0d40460373 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -1,7 +1,10 @@ """Test for smart home alexa support.""" +from unittest.mock import patch + import pytest from homeassistant.components.alexa import messages, smart_home +import homeassistant.components.camera as camera from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -22,6 +25,7 @@ import homeassistant.components.vacuum as vacuum from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import Context, callback from homeassistant.helpers import entityfilter +from homeassistant.setup import async_setup_component from . import ( DEFAULT_CONFIG, @@ -35,7 +39,7 @@ from . import ( reported_properties, ) -from tests.common import async_mock_service +from tests.common import async_mock_service, mock_coro @pytest.fixture @@ -48,6 +52,22 @@ def events(hass): yield events +@pytest.fixture +def mock_camera(hass): + """Initialize a demo camera platform.""" + assert hass.loop.run_until_complete( + async_setup_component(hass, "camera", {camera.DOMAIN: {"platform": "demo"}}) + ) + + +@pytest.fixture +def mock_stream(hass): + """Initialize a demo camera platform with streaming.""" + assert hass.loop.run_until_complete( + async_setup_component(hass, "stream", {"stream": {}}) + ) + + def test_create_api_message_defaults(hass): """Create a API message response of a request with defaults.""" request = get_new_request("Alexa.PowerController", "TurnOn", "switch#xy") @@ -3445,11 +3465,11 @@ async def test_vacuum_discovery(hass): properties.assert_equal("Alexa.PowerController", "powerState", "OFF") await assert_request_calls_service( - "Alexa.PowerController", "TurnOn", "vacuum#test_1", "vacuum.turn_on", hass, + "Alexa.PowerController", "TurnOn", "vacuum#test_1", "vacuum.turn_on", hass ) await assert_request_calls_service( - "Alexa.PowerController", "TurnOff", "vacuum#test_1", "vacuum.turn_off", hass, + "Alexa.PowerController", "TurnOff", "vacuum#test_1", "vacuum.turn_off", hass ) @@ -3663,18 +3683,18 @@ async def test_vacuum_discovery_no_turn_on(hass): appliance = await discovery_test(device, hass) assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa", + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) properties = await reported_properties(hass, "vacuum#test_5") properties.assert_equal("Alexa.PowerController", "powerState", "ON") await assert_request_calls_service( - "Alexa.PowerController", "TurnOn", "vacuum#test_5", "vacuum.start", hass, + "Alexa.PowerController", "TurnOn", "vacuum#test_5", "vacuum.start", hass ) await assert_request_calls_service( - "Alexa.PowerController", "TurnOff", "vacuum#test_5", "vacuum.turn_off", hass, + "Alexa.PowerController", "TurnOff", "vacuum#test_5", "vacuum.turn_off", hass ) @@ -3693,11 +3713,11 @@ async def test_vacuum_discovery_no_turn_off(hass): appliance = await discovery_test(device, hass) assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa", + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_request_calls_service( - "Alexa.PowerController", "TurnOn", "vacuum#test_6", "vacuum.turn_on", hass, + "Alexa.PowerController", "TurnOn", "vacuum#test_6", "vacuum.turn_on", hass ) await assert_request_calls_service( @@ -3722,11 +3742,11 @@ async def test_vacuum_discovery_no_turn_on_or_off(hass): appliance = await discovery_test(device, hass) assert_endpoint_capabilities( - appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa", + appliance, "Alexa.PowerController", "Alexa.EndpointHealth", "Alexa" ) await assert_request_calls_service( - "Alexa.PowerController", "TurnOn", "vacuum#test_7", "vacuum.start", hass, + "Alexa.PowerController", "TurnOn", "vacuum#test_7", "vacuum.start", hass ) await assert_request_calls_service( @@ -3736,3 +3756,106 @@ async def test_vacuum_discovery_no_turn_on_or_off(hass): "vacuum.return_to_base", hass, ) + + +async def test_camera_discovery(hass, mock_stream): + """Test camera discovery.""" + device = ( + "camera.test", + "idle", + {"friendly_name": "Test camera", "supported_features": 3}, + ) + with patch( + "homeassistant.helpers.network.async_get_external_url", + return_value="https://example.nabu.casa", + ): + appliance = await discovery_test(device, hass) + + capabilities = assert_endpoint_capabilities( + appliance, "Alexa.CameraStreamController", "Alexa.EndpointHealth", "Alexa" + ) + + camera_stream_capability = get_capability( + capabilities, "Alexa.CameraStreamController" + ) + configuration = camera_stream_capability["cameraStreamConfigurations"][0] + assert "HLS" in configuration["protocols"] + assert {"width": 1280, "height": 720} in configuration["resolutions"] + assert "NONE" in configuration["authorizationTypes"] + assert "H264" in configuration["videoCodecs"] + assert "AAC" in configuration["audioCodecs"] + + +async def test_camera_discovery_without_stream(hass): + """Test camera discovery without stream integration.""" + device = ( + "camera.test", + "idle", + {"friendly_name": "Test camera", "supported_features": 3}, + ) + with patch( + "homeassistant.helpers.network.async_get_external_url", + return_value="https://example.nabu.casa", + ): + appliance = await discovery_test(device, hass) + # assert Alexa.CameraStreamController is not yielded. + assert_endpoint_capabilities(appliance, "Alexa.EndpointHealth", "Alexa") + + +@pytest.mark.parametrize( + "url,result", + [ + ("http://nohttpswrongport.org:8123", 2), + ("https://httpswrongport.org:8123", 2), + ("http://nohttpsport443.org:443", 2), + ("tls://nohttpsport443.org:443", 2), + ("https://correctschemaandport.org:443", 3), + ("https://correctschemaandport.org", 3), + ], +) +async def test_camera_hass_urls(hass, mock_stream, url, result): + """Test camera discovery with unsupported urls.""" + device = ( + "camera.test", + "idle", + {"friendly_name": "Test camera", "supported_features": 3}, + ) + with patch( + "homeassistant.helpers.network.async_get_external_url", return_value=url + ): + appliance = await discovery_test(device, hass) + assert len(appliance["capabilities"]) == result + + +async def test_initialize_camera_stream(hass, mock_camera, mock_stream): + """Test InitializeCameraStreams handler.""" + request = get_new_request( + "Alexa.CameraStreamController", "InitializeCameraStreams", "camera#demo_camera" + ) + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=mock_coro("rtsp://example.local"), + ), patch( + "homeassistant.helpers.network.async_get_external_url", + return_value="https://mycamerastream.test", + ): + msg = await smart_home.async_handle_message(hass, DEFAULT_CONFIG, request) + await hass.async_block_till_done() + + assert "event" in msg + response = msg["event"] + assert response["header"]["namespace"] == "Alexa.CameraStreamController" + assert response["header"]["name"] == "Response" + camera_streams = response["payload"]["cameraStreams"] + assert "https://mycamerastream.test/api/hls/" in camera_streams[0]["uri"] + assert camera_streams[0]["protocol"] == "HLS" + assert camera_streams[0]["resolution"]["width"] == 1280 + assert camera_streams[0]["resolution"]["height"] == 720 + assert camera_streams[0]["authorizationType"] == "NONE" + assert camera_streams[0]["videoCodec"] == "H264" + assert camera_streams[0]["audioCodec"] == "AAC" + assert ( + "https://mycamerastream.test/api/camera_proxy/camera.demo_camera?token=" + in response["payload"]["imageUri"] + ) From ff391e538a8ddc1ad63a2640c1dda63aa4d5608e Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Sat, 28 Mar 2020 07:36:06 +0300 Subject: [PATCH 284/431] Fail tests with uncaught exceptions (#33312) * initial implementation and ignore file * updated ignore list for the tests in the most recent dev branch --- tests/conftest.py | 23 ++- tests/ignore_uncaught_exceptions.py | 235 ++++++++++++++++++++++++++++ 2 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 tests/ignore_uncaught_exceptions.py diff --git a/tests/conftest.py b/tests/conftest.py index 04e584cb158..0963d151490 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -15,9 +15,12 @@ from homeassistant.components.websocket_api.auth import ( TYPE_AUTH_REQUIRED, ) from homeassistant.components.websocket_api.http import URL +from homeassistant.exceptions import ServiceNotFound from homeassistant.setup import async_setup_component from homeassistant.util import location +from tests.ignore_uncaught_exceptions import IGNORE_UNCAUGHT_EXCEPTIONS + pytest.register_assert_rewrite("tests.common") from tests.common import ( # noqa: E402, isort:skip @@ -77,13 +80,31 @@ def hass_storage(): @pytest.fixture -def hass(loop, hass_storage): +def hass(loop, hass_storage, request): """Fixture to provide a test instance of Home Assistant.""" + + def exc_handle(loop, context): + """Handle exceptions by rethrowing them, which will fail the test.""" + exceptions.append(context["exception"]) + orig_exception_handler(loop, context) + + exceptions = [] hass = loop.run_until_complete(async_test_home_assistant(loop)) + orig_exception_handler = loop.get_exception_handler() + loop.set_exception_handler(exc_handle) yield hass loop.run_until_complete(hass.async_stop(force=True)) + for ex in exceptions: + if ( + request.module.__name__, + request.function.__name__, + ) in IGNORE_UNCAUGHT_EXCEPTIONS: + continue + if isinstance(ex, ServiceNotFound): + continue + raise ex @pytest.fixture diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py new file mode 100644 index 00000000000..8c961962ad5 --- /dev/null +++ b/tests/ignore_uncaught_exceptions.py @@ -0,0 +1,235 @@ +"""List of modules that have uncaught exceptions today. Will be shrunk over time.""" +IGNORE_UNCAUGHT_EXCEPTIONS = [ + ("tests.components.abode.test_alarm_control_panel", "test_entity_registry"), + ("tests.components.abode.test_alarm_control_panel", "test_attributes"), + ("tests.components.abode.test_alarm_control_panel", "test_set_alarm_away"), + ("tests.components.abode.test_alarm_control_panel", "test_set_alarm_home"), + ("tests.components.abode.test_alarm_control_panel", "test_set_alarm_standby"), + ("tests.components.abode.test_alarm_control_panel", "test_state_unknown"), + ("tests.components.abode.test_binary_sensor", "test_entity_registry"), + ("tests.components.abode.test_binary_sensor", "test_attributes"), + ("tests.components.abode.test_camera", "test_entity_registry"), + ("tests.components.abode.test_camera", "test_attributes"), + ("tests.components.abode.test_camera", "test_capture_image"), + ("tests.components.abode.test_cover", "test_entity_registry"), + ("tests.components.abode.test_cover", "test_attributes"), + ("tests.components.abode.test_cover", "test_open"), + ("tests.components.abode.test_cover", "test_close"), + ("tests.components.abode.test_init", "test_change_settings"), + ("tests.components.abode.test_light", "test_entity_registry"), + ("tests.components.abode.test_light", "test_attributes"), + ("tests.components.abode.test_light", "test_switch_off"), + ("tests.components.abode.test_light", "test_switch_on"), + ("tests.components.abode.test_light", "test_set_brightness"), + ("tests.components.abode.test_light", "test_set_color"), + ("tests.components.abode.test_light", "test_set_color_temp"), + ("tests.components.abode.test_lock", "test_entity_registry"), + ("tests.components.abode.test_lock", "test_attributes"), + ("tests.components.abode.test_lock", "test_lock"), + ("tests.components.abode.test_lock", "test_unlock"), + ("tests.components.abode.test_sensor", "test_entity_registry"), + ("tests.components.abode.test_sensor", "test_attributes"), + ("tests.components.abode.test_switch", "test_entity_registry"), + ("tests.components.abode.test_switch", "test_attributes"), + ("tests.components.abode.test_switch", "test_switch_on"), + ("tests.components.abode.test_switch", "test_switch_off"), + ("tests.components.abode.test_switch", "test_automation_attributes"), + ("tests.components.abode.test_switch", "test_turn_automation_off"), + ("tests.components.abode.test_switch", "test_turn_automation_on"), + ("tests.components.abode.test_switch", "test_trigger_automation"), + ("tests.components.cast.test_media_player", "test_start_discovery_called_once"), + ("tests.components.cast.test_media_player", "test_entry_setup_single_config"), + ("tests.components.cast.test_media_player", "test_entry_setup_list_config"), + ("tests.components.cast.test_media_player", "test_entry_setup_platform_not_ready"), + ("tests.components.config.test_automation", "test_delete_automation"), + ("tests.components.config.test_group", "test_update_device_config"), + ("tests.components.deconz.test_binary_sensor", "test_allow_clip_sensor"), + ("tests.components.deconz.test_climate", "test_clip_climate_device"), + ("tests.components.deconz.test_init", "test_unload_entry_multiple_gateways"), + ("tests.components.deconz.test_light", "test_disable_light_groups"), + ("tests.components.deconz.test_sensor", "test_allow_clip_sensors"), + ("tests.components.default_config.test_init", "test_setup"), + ("tests.components.demo.test_init", "test_setting_up_demo"), + ("tests.components.discovery.test_init", "test_discover_config_flow"), + ("tests.components.dsmr.test_sensor", "test_default_setup"), + ("tests.components.dsmr.test_sensor", "test_v4_meter"), + ("tests.components.dsmr.test_sensor", "test_v5_meter"), + ("tests.components.dsmr.test_sensor", "test_belgian_meter"), + ("tests.components.dsmr.test_sensor", "test_belgian_meter_low"), + ("tests.components.dsmr.test_sensor", "test_tcp"), + ("tests.components.dsmr.test_sensor", "test_connection_errors_retry"), + ("tests.components.dynalite.test_bridge", "test_add_devices_then_register"), + ("tests.components.dynalite.test_bridge", "test_register_then_add_devices"), + ("tests.components.dynalite.test_config_flow", "test_existing_update"), + ("tests.components.dyson.test_air_quality", "test_purecool_aiq_attributes"), + ("tests.components.dyson.test_air_quality", "test_purecool_aiq_update_state"), + ( + "tests.components.dyson.test_air_quality", + "test_purecool_component_setup_only_once", + ), + ("tests.components.dyson.test_air_quality", "test_purecool_aiq_without_discovery"), + ( + "tests.components.dyson.test_air_quality", + "test_purecool_aiq_empty_environment_state", + ), + ( + "tests.components.dyson.test_climate", + "test_setup_component_with_parent_discovery", + ), + ("tests.components.dyson.test_fan", "test_purecoollink_attributes"), + ("tests.components.dyson.test_fan", "test_purecool_turn_on"), + ("tests.components.dyson.test_fan", "test_purecool_set_speed"), + ("tests.components.dyson.test_fan", "test_purecool_turn_off"), + ("tests.components.dyson.test_fan", "test_purecool_set_dyson_speed"), + ("tests.components.dyson.test_fan", "test_purecool_oscillate"), + ("tests.components.dyson.test_fan", "test_purecool_set_night_mode"), + ("tests.components.dyson.test_fan", "test_purecool_set_auto_mode"), + ("tests.components.dyson.test_fan", "test_purecool_set_angle"), + ("tests.components.dyson.test_fan", "test_purecool_set_flow_direction_front"), + ("tests.components.dyson.test_fan", "test_purecool_set_timer"), + ("tests.components.dyson.test_fan", "test_purecool_update_state"), + ("tests.components.dyson.test_fan", "test_purecool_update_state_filter_inv"), + ("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"), + ("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"), + ("tests.components.gdacs.test_geo_location", "test_setup"), + ("tests.components.gdacs.test_sensor", "test_setup"), + ("tests.components.geonetnz_quakes.test_geo_location", "test_setup"), + ("tests.components.geonetnz_quakes.test_sensor", "test_setup"), + ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), + ("tests.components.homematicip_cloud.test_config_flow", "test_flow_works"), + ("tests.components.homematicip_cloud.test_config_flow", "test_import_config"), + ("tests.components.homematicip_cloud.test_device", "test_hmip_remove_group"), + ( + "tests.components.homematicip_cloud.test_init", + "test_config_with_accesspoint_passed_to_config_entry", + ), + ( + "tests.components.homematicip_cloud.test_init", + "test_config_already_registered_not_passed_to_config_entry", + ), + ( + "tests.components.homematicip_cloud.test_init", + "test_load_entry_fails_due_to_generic_exception", + ), + ("tests.components.hue.test_bridge", "test_handle_unauthorized"), + ("tests.components.hue.test_init", "test_security_vuln_check"), + ("tests.components.hue.test_light", "test_group_features"), + ("tests.components.ios.test_init", "test_creating_entry_sets_up_sensor"), + ("tests.components.ios.test_init", "test_not_configuring_ios_not_creates_entry"), + ("tests.components.local_file.test_camera", "test_file_not_readable"), + ("tests.components.meteo_france.test_config_flow", "test_user"), + ("tests.components.meteo_france.test_config_flow", "test_import"), + ("tests.components.mikrotik.test_device_tracker", "test_restoring_devices"), + ("tests.components.mikrotik.test_hub", "test_arp_ping"), + ("tests.components.mqtt.test_alarm_control_panel", "test_unique_id"), + ("tests.components.mqtt.test_binary_sensor", "test_unique_id"), + ("tests.components.mqtt.test_camera", "test_unique_id"), + ("tests.components.mqtt.test_climate", "test_unique_id"), + ("tests.components.mqtt.test_cover", "test_unique_id"), + ("tests.components.mqtt.test_fan", "test_unique_id"), + ( + "tests.components.mqtt.test_init", + "test_setup_uses_certificate_on_certificate_set_to_auto", + ), + ( + "tests.components.mqtt.test_init", + "test_setup_does_not_use_certificate_on_mqtts_port", + ), + ( + "tests.components.mqtt.test_init", + "test_setup_without_tls_config_uses_tlsv1_under_python36", + ), + ( + "tests.components.mqtt.test_init", + "test_setup_with_tls_config_uses_tls_version1_2", + ), + ( + "tests.components.mqtt.test_init", + "test_setup_with_tls_config_of_v1_under_python36_only_uses_v1", + ), + ("tests.components.mqtt.test_legacy_vacuum", "test_unique_id"), + ("tests.components.mqtt.test_light", "test_unique_id"), + ("tests.components.mqtt.test_light", "test_entity_device_info_remove"), + ("tests.components.mqtt.test_light_json", "test_unique_id"), + ("tests.components.mqtt.test_light_json", "test_entity_device_info_remove"), + ("tests.components.mqtt.test_light_template", "test_entity_device_info_remove"), + ("tests.components.mqtt.test_lock", "test_unique_id"), + ("tests.components.mqtt.test_sensor", "test_unique_id"), + ("tests.components.mqtt.test_state_vacuum", "test_unique_id"), + ("tests.components.mqtt.test_switch", "test_unique_id"), + ("tests.components.mqtt.test_switch", "test_entity_device_info_remove"), + ("tests.components.plex.test_config_flow", "test_import_success"), + ("tests.components.plex.test_config_flow", "test_single_available_server"), + ("tests.components.plex.test_config_flow", "test_multiple_servers_with_selection"), + ("tests.components.plex.test_config_flow", "test_adding_last_unconfigured_server"), + ("tests.components.plex.test_config_flow", "test_option_flow"), + ("tests.components.plex.test_config_flow", "test_option_flow_new_users_available"), + ("tests.components.plex.test_init", "test_setup_with_config"), + ("tests.components.plex.test_init", "test_setup_with_config_entry"), + ("tests.components.plex.test_init", "test_set_config_entry_unique_id"), + ("tests.components.plex.test_init", "test_setup_with_insecure_config_entry"), + ("tests.components.plex.test_init", "test_setup_with_photo_session"), + ("tests.components.plex.test_server", "test_new_users_available"), + ("tests.components.plex.test_server", "test_new_ignored_users_available"), + ("tests.components.plex.test_server", "test_mark_sessions_idle"), + ("tests.components.qwikswitch.test_init", "test_binary_sensor_device"), + ("tests.components.qwikswitch.test_init", "test_sensor_device"), + ("tests.components.rflink.test_init", "test_send_command_invalid_arguments"), + ("tests.components.samsungtv.test_media_player", "test_update_connection_failure"), + ("tests.components.tplink.test_init", "test_configuring_device_types"), + ( + "tests.components.tplink.test_init", + "test_configuring_devices_from_multiple_sources", + ), + ("tests.components.tradfri.test_light", "test_light"), + ("tests.components.tradfri.test_light", "test_light_observed"), + ("tests.components.tradfri.test_light", "test_light_available"), + ("tests.components.tradfri.test_light", "test_turn_on"), + ("tests.components.tradfri.test_light", "test_turn_off"), + ("tests.components.unifi_direct.test_device_tracker", "test_get_scanner"), + ("tests.components.upnp.test_init", "test_async_setup_entry_default"), + ("tests.components.upnp.test_init", "test_async_setup_entry_port_mapping"), + ("tests.components.vera.test_init", "test_init"), + ("tests.components.wunderground.test_sensor", "test_fails_because_of_unique_id"), + ("tests.components.yr.test_sensor", "test_default_setup"), + ("tests.components.yr.test_sensor", "test_custom_setup"), + ("tests.components.yr.test_sensor", "test_forecast_setup"), + ("tests.components.zha.test_api", "test_device_clusters"), + ("tests.components.zha.test_api", "test_device_cluster_attributes"), + ("tests.components.zha.test_api", "test_device_cluster_commands"), + ("tests.components.zha.test_api", "test_list_devices"), + ("tests.components.zha.test_api", "test_device_not_found"), + ("tests.components.zha.test_api", "test_list_groups"), + ("tests.components.zha.test_api", "test_get_group"), + ("tests.components.zha.test_api", "test_get_group_not_found"), + ("tests.components.zha.test_api", "test_list_groupable_devices"), + ("tests.components.zha.test_api", "test_add_group"), + ("tests.components.zha.test_api", "test_remove_group"), + ("tests.components.zha.test_binary_sensor", "test_binary_sensor"), + ("tests.components.zha.test_cover", "test_cover"), + ("tests.components.zha.test_device_action", "test_get_actions"), + ("tests.components.zha.test_device_action", "test_action"), + ("tests.components.zha.test_device_tracker", "test_device_tracker"), + ("tests.components.zha.test_device_trigger", "test_triggers"), + ("tests.components.zha.test_device_trigger", "test_no_triggers"), + ("tests.components.zha.test_device_trigger", "test_if_fires_on_event"), + ("tests.components.zha.test_device_trigger", "test_exception_no_triggers"), + ("tests.components.zha.test_device_trigger", "test_exception_bad_trigger"), + ("tests.components.zha.test_discover", "test_devices"), + ("tests.components.zha.test_discover", "test_device_override"), + ("tests.components.zha.test_fan", "test_fan"), + ("tests.components.zha.test_gateway", "test_gateway_group_methods"), + ("tests.components.zha.test_light", "test_light"), + ("tests.components.zha.test_lock", "test_lock"), + ("tests.components.zha.test_sensor", "test_sensor"), + ("tests.components.zha.test_switch", "test_switch"), + ("tests.components.zwave.test_init", "test_power_schemes"), + ( + "tests.helpers.test_entity_platform", + "test_adding_entities_with_generator_and_thread_callback", + ), + ( + "tests.helpers.test_entity_platform", + "test_not_adding_duplicate_entities_with_unique_id", + ), +] From 5ce31cb3830d543487c8cf46e678ebb454a1f399 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 28 Mar 2020 02:01:17 -0400 Subject: [PATCH 285/431] Fix ZHA light group on state (#33308) --- homeassistant/components/zha/light.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index c65d3c47de6..07cbc6af78c 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -106,7 +106,6 @@ class BaseLight(BaseZhaEntity, light.Light): def __init__(self, *args, **kwargs): """Initialize the light.""" super().__init__(*args, **kwargs) - self._is_on: bool = False self._available: bool = False self._brightness: Optional[int] = None self._off_brightness: Optional[int] = None @@ -525,7 +524,7 @@ class LightGroup(BaseLight): states: List[State] = list(filter(None, all_states)) on_states = [state for state in states if state.state == STATE_ON] - self._is_on = len(on_states) > 0 + self._state = len(on_states) > 0 self._available = any(state.state != STATE_UNAVAILABLE for state in states) self._brightness = helpers.reduce_attribute(on_states, ATTR_BRIGHTNESS) From 95cefd1acc4d79c27a1ee79efa0ce5b18229064a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Mar 2020 01:01:59 -0500 Subject: [PATCH 286/431] Fix nuheat unexpectedly switching to a permanent hold (#33316) When in run schedule mode, setting the temperature will switch the device to a temporary hold. If you set the temperature again, we should stay in temporary hold mode and not switch to permanent hold. The underlying nuheat package contains a default to a permanent hold if no setting was passed. We now always pass the setting. --- homeassistant/components/nuheat/climate.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index dbd4eed8efb..f675b6a90f4 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -3,6 +3,7 @@ from datetime import timedelta import logging from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD +from nuheat.util import celsius_to_nuheat, fahrenheit_to_nuheat from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( @@ -186,20 +187,26 @@ class NuHeatThermostat(ClimateDevice): def _set_temperature(self, temperature): if self._temperature_unit == "C": - self._thermostat.target_celsius = temperature + target_temp = celsius_to_nuheat(temperature) else: - self._thermostat.target_fahrenheit = temperature + target_temp = fahrenheit_to_nuheat(temperature) + # If they set a temperature without changing the mode # to heat, we behave like the device does locally # and set a temp hold. - if self._thermostat.schedule_mode == SCHEDULE_RUN: - self._thermostat.schedule_mode = SCHEDULE_TEMPORARY_HOLD + target_schedule_mode = SCHEDULE_HOLD + if self._thermostat.schedule_mode in (SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD): + target_schedule_mode = SCHEDULE_TEMPORARY_HOLD _LOGGER.debug( - "Setting NuHeat thermostat temperature to %s %s", + "Setting NuHeat thermostat temperature to %s %s and schedule mode: %s", temperature, self.temperature_unit, + target_schedule_mode, ) + # If we do not send schedule_mode we always get + # SCHEDULE_HOLD + self._thermostat.set_target_temperature(target_temp, target_schedule_mode) self._schedule_update() def _schedule_update(self): From 4a2236fe8543af22ffc05d5b5b75d1ba73516c91 Mon Sep 17 00:00:00 2001 From: Hans Oischinger Date: Sat, 28 Mar 2020 12:12:27 +0100 Subject: [PATCH 287/431] :vicare: Improve scan_interval (#33294) This upgrades to PyVicare 0.1.10 which allows to combine multiple requests into a single HTTP GET. This in turn allows us to increase the scan_interval to 60 seconds by default. Additionally scan_interval has been introduced as a config parameter. --- homeassistant/components/vicare/__init__.py | 12 +++++++++++- homeassistant/components/vicare/climate.py | 4 ---- homeassistant/components/vicare/manifest.json | 2 +- homeassistant/components/vicare/water_heater.py | 4 ---- requirements_all.txt | 2 +- 5 files changed, 13 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 7632a101769..335e89eb873 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -7,7 +7,12 @@ from PyViCare.PyViCareGazBoiler import GazBoiler from PyViCare.PyViCareHeatPump import HeatPump import voluptuous as vol -from homeassistant.const import CONF_NAME, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ( + CONF_NAME, + CONF_PASSWORD, + CONF_SCAN_INTERVAL, + CONF_USERNAME, +) from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR @@ -40,6 +45,9 @@ CONFIG_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, + vol.Optional(CONF_SCAN_INTERVAL, default=60): vol.All( + cv.time_period, lambda value: value.total_seconds() + ), vol.Optional(CONF_CIRCUIT): int, vol.Optional(CONF_NAME, default="ViCare"): cv.string, vol.Optional(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE): cv.enum( @@ -59,6 +67,8 @@ def setup(hass, config): if conf.get(CONF_CIRCUIT) is not None: params["circuit"] = conf[CONF_CIRCUIT] + params["cacheDuration"] = conf.get(CONF_SCAN_INTERVAL) + heating_type = conf[CONF_HEATING_TYPE] try: diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index ef5533523f8..1b101cc7612 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -1,5 +1,4 @@ """Viessmann ViCare climate device.""" -from datetime import timedelta import logging import requests @@ -80,9 +79,6 @@ HA_TO_VICARE_PRESET_HEATING = { PYVICARE_ERROR = "error" -# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit -SCAN_INTERVAL = timedelta(seconds=900) - def setup_platform(hass, config, add_entities, discovery_info=None): """Create the ViCare climate devices.""" diff --git a/homeassistant/components/vicare/manifest.json b/homeassistant/components/vicare/manifest.json index 66fd15d3a90..a03c927c2ac 100644 --- a/homeassistant/components/vicare/manifest.json +++ b/homeassistant/components/vicare/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/vicare", "dependencies": [], "codeowners": ["@oischinger"], - "requirements": ["PyViCare==0.1.7"] + "requirements": ["PyViCare==0.1.10"] } diff --git a/homeassistant/components/vicare/water_heater.py b/homeassistant/components/vicare/water_heater.py index fdac2962739..eea3d81faf6 100644 --- a/homeassistant/components/vicare/water_heater.py +++ b/homeassistant/components/vicare/water_heater.py @@ -1,5 +1,4 @@ """Viessmann ViCare water_heater device.""" -from datetime import timedelta import logging import requests @@ -43,9 +42,6 @@ HA_TO_VICARE_HVAC_DHW = { PYVICARE_ERROR = "error" -# Scan interval of 15 minutes seems to be safe to not hit the ViCare server rate limit -SCAN_INTERVAL = timedelta(seconds=900) - def setup_platform(hass, config, add_entities, discovery_info=None): """Create the ViCare water_heater devices.""" diff --git a/requirements_all.txt b/requirements_all.txt index 3432f8c8ffd..99be3ba875e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -78,7 +78,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.vicare -PyViCare==0.1.7 +PyViCare==0.1.10 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.12.4 From 1477087c71a027e59b6bd9dcc0ba51cd3a314eda Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Sat, 28 Mar 2020 13:57:36 +0100 Subject: [PATCH 288/431] Fix Lovelace resources health info (#33309) * Fix Lovelace resources health info * Fix tests --- homeassistant/components/lovelace/__init__.py | 5 ++++- homeassistant/components/lovelace/dashboard.py | 3 +-- homeassistant/components/lovelace/resources.py | 12 ++++++++++++ tests/components/lovelace/test_dashboard.py | 8 +++++--- 4 files changed, 22 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/lovelace/__init__.py b/homeassistant/components/lovelace/__init__.py index 95508c2f8f3..9b944be556b 100644 --- a/homeassistant/components/lovelace/__init__.py +++ b/homeassistant/components/lovelace/__init__.py @@ -234,7 +234,10 @@ async def create_yaml_resource_col(hass, yaml_resources): async def system_health_info(hass): """Get info for the info page.""" - return await hass.data[DOMAIN]["dashboards"][None].async_get_info() + health_info = {"dashboards": len(hass.data[DOMAIN]["dashboards"])} + health_info.update(await hass.data[DOMAIN]["dashboards"][None].async_get_info()) + health_info.update(await hass.data[DOMAIN]["resources"].async_get_info()) + return health_info @callback diff --git a/homeassistant/components/lovelace/dashboard.py b/homeassistant/components/lovelace/dashboard.py index cdb104a150b..2d3196054e3 100644 --- a/homeassistant/components/lovelace/dashboard.py +++ b/homeassistant/components/lovelace/dashboard.py @@ -101,7 +101,7 @@ class LovelaceStorage(LovelaceConfig): return MODE_STORAGE async def async_get_info(self): - """Return the YAML storage mode.""" + """Return the Lovelace storage info.""" if self._data is None: await self._load() @@ -213,7 +213,6 @@ def _config_info(mode, config): """Generate info about the config.""" return { "mode": mode, - "resources": len(config.get("resources", [])), "views": len(config.get("views", [])), } diff --git a/homeassistant/components/lovelace/resources.py b/homeassistant/components/lovelace/resources.py index 57acaa487bd..78a23540ed4 100644 --- a/homeassistant/components/lovelace/resources.py +++ b/homeassistant/components/lovelace/resources.py @@ -34,6 +34,10 @@ class ResourceYAMLCollection: """Initialize a resource YAML collection.""" self.data = data + async def async_get_info(self): + """Return the resources info for YAML mode.""" + return {"resources": len(self.async_items() or [])} + @callback def async_items(self) -> List[dict]: """Return list of items in collection.""" @@ -55,6 +59,14 @@ class ResourceStorageCollection(collection.StorageCollection): ) self.ll_config = ll_config + async def async_get_info(self): + """Return the resources info for YAML mode.""" + if not self.loaded: + await self.async_load() + self.loaded = True + + return {"resources": len(self.async_items() or [])} + async def _async_load_data(self) -> Optional[dict]: """Load the data.""" data = await self.store.async_load() diff --git a/tests/components/lovelace/test_dashboard.py b/tests/components/lovelace/test_dashboard.py index 775b2760c96..8509ad37fcd 100644 --- a/tests/components/lovelace/test_dashboard.py +++ b/tests/components/lovelace/test_dashboard.py @@ -165,7 +165,7 @@ async def test_system_health_info_autogen(hass): """Test system health info endpoint.""" assert await async_setup_component(hass, "lovelace", {}) info = await get_system_health_info(hass, "lovelace") - assert info == {"mode": "auto-gen"} + assert info == {"dashboards": 1, "mode": "auto-gen", "resources": 0} async def test_system_health_info_storage(hass, hass_storage): @@ -177,7 +177,7 @@ async def test_system_health_info_storage(hass, hass_storage): } assert await async_setup_component(hass, "lovelace", {}) info = await get_system_health_info(hass, "lovelace") - assert info == {"mode": "storage", "resources": 0, "views": 0} + assert info == {"dashboards": 1, "mode": "storage", "resources": 0, "views": 0} async def test_system_health_info_yaml(hass): @@ -188,7 +188,7 @@ async def test_system_health_info_yaml(hass): return_value={"views": [{"cards": []}]}, ): info = await get_system_health_info(hass, "lovelace") - assert info == {"mode": "yaml", "resources": 0, "views": 1} + assert info == {"dashboards": 1, "mode": "yaml", "resources": 0, "views": 1} async def test_system_health_info_yaml_not_found(hass): @@ -196,8 +196,10 @@ async def test_system_health_info_yaml_not_found(hass): assert await async_setup_component(hass, "lovelace", {"lovelace": {"mode": "YAML"}}) info = await get_system_health_info(hass, "lovelace") assert info == { + "dashboards": 1, "mode": "yaml", "error": "{} not found".format(hass.config.path("ui-lovelace.yaml")), + "resources": 0, } From 5a1b0edd9655d9e1700c0ed51d8ddac6cde6111e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 28 Mar 2020 11:23:39 -0500 Subject: [PATCH 289/431] =?UTF-8?q?Add=20inline=20comments=20to=20elkm1=20?= =?UTF-8?q?about=20how=20config=20is=20updated=20from=E2=80=A6=20(#33361)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/elkm1/__init__.py | 5 +++++ tests/components/elkm1/test_config_flow.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 2f08b046d9c..183897d306e 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -149,6 +149,11 @@ async def async_setup(hass: HomeAssistant, hass_config: ConfigType) -> bool: for index, conf in enumerate(hass_config[DOMAIN]): _LOGGER.debug("Importing elkm1 #%d - %s", index, conf[CONF_HOST]) + + # The update of the config entry is done in async_setup + # to ensure the entry if updated before async_setup_entry + # is called to avoid a situation where the user has to restart + # twice for the changes to take effect current_config_entry = _async_find_matching_config_entry( hass, conf[CONF_PREFIX] ) diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index 466005f3d43..02e3fd7fce9 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -176,7 +176,7 @@ async def test_form_cannot_connect(hass): async def test_form_invalid_auth(hass): - """Test we handle cannot connect error.""" + """Test we handle invalid auth error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) From bf16b50679c25d91308163621c2be414af78e406 Mon Sep 17 00:00:00 2001 From: shred86 <32663154+shred86@users.noreply.github.com> Date: Sat, 28 Mar 2020 10:54:07 -0700 Subject: [PATCH 290/431] Fix Abode tests uncaught exceptions (#33365) --- tests/components/abode/conftest.py | 2 ++ tests/fixtures/abode_logout.json | 4 ++++ tests/ignore_uncaught_exceptions.py | 37 ----------------------------- 3 files changed, 6 insertions(+), 37 deletions(-) create mode 100644 tests/fixtures/abode_logout.json diff --git a/tests/components/abode/conftest.py b/tests/components/abode/conftest.py index 92f9bb09681..681f65ddf93 100644 --- a/tests/components/abode/conftest.py +++ b/tests/components/abode/conftest.py @@ -10,6 +10,8 @@ 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")) + # Mocks the logout response for abodepy. + requests_mock.post(CONST.LOGOUT_URL, text=load_fixture("abode_logout.json")) # Mocks the oauth claims response for abodepy. requests_mock.get( CONST.OAUTH_TOKEN_URL, text=load_fixture("abode_oauth_claims.json") diff --git a/tests/fixtures/abode_logout.json b/tests/fixtures/abode_logout.json new file mode 100644 index 00000000000..53e242fced3 --- /dev/null +++ b/tests/fixtures/abode_logout.json @@ -0,0 +1,4 @@ +{ + "code": 200, + "message": "Logout successful." +} \ No newline at end of file diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 8c961962ad5..34323f5b6d4 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -1,42 +1,5 @@ """List of modules that have uncaught exceptions today. Will be shrunk over time.""" IGNORE_UNCAUGHT_EXCEPTIONS = [ - ("tests.components.abode.test_alarm_control_panel", "test_entity_registry"), - ("tests.components.abode.test_alarm_control_panel", "test_attributes"), - ("tests.components.abode.test_alarm_control_panel", "test_set_alarm_away"), - ("tests.components.abode.test_alarm_control_panel", "test_set_alarm_home"), - ("tests.components.abode.test_alarm_control_panel", "test_set_alarm_standby"), - ("tests.components.abode.test_alarm_control_panel", "test_state_unknown"), - ("tests.components.abode.test_binary_sensor", "test_entity_registry"), - ("tests.components.abode.test_binary_sensor", "test_attributes"), - ("tests.components.abode.test_camera", "test_entity_registry"), - ("tests.components.abode.test_camera", "test_attributes"), - ("tests.components.abode.test_camera", "test_capture_image"), - ("tests.components.abode.test_cover", "test_entity_registry"), - ("tests.components.abode.test_cover", "test_attributes"), - ("tests.components.abode.test_cover", "test_open"), - ("tests.components.abode.test_cover", "test_close"), - ("tests.components.abode.test_init", "test_change_settings"), - ("tests.components.abode.test_light", "test_entity_registry"), - ("tests.components.abode.test_light", "test_attributes"), - ("tests.components.abode.test_light", "test_switch_off"), - ("tests.components.abode.test_light", "test_switch_on"), - ("tests.components.abode.test_light", "test_set_brightness"), - ("tests.components.abode.test_light", "test_set_color"), - ("tests.components.abode.test_light", "test_set_color_temp"), - ("tests.components.abode.test_lock", "test_entity_registry"), - ("tests.components.abode.test_lock", "test_attributes"), - ("tests.components.abode.test_lock", "test_lock"), - ("tests.components.abode.test_lock", "test_unlock"), - ("tests.components.abode.test_sensor", "test_entity_registry"), - ("tests.components.abode.test_sensor", "test_attributes"), - ("tests.components.abode.test_switch", "test_entity_registry"), - ("tests.components.abode.test_switch", "test_attributes"), - ("tests.components.abode.test_switch", "test_switch_on"), - ("tests.components.abode.test_switch", "test_switch_off"), - ("tests.components.abode.test_switch", "test_automation_attributes"), - ("tests.components.abode.test_switch", "test_turn_automation_off"), - ("tests.components.abode.test_switch", "test_turn_automation_on"), - ("tests.components.abode.test_switch", "test_trigger_automation"), ("tests.components.cast.test_media_player", "test_start_discovery_called_once"), ("tests.components.cast.test_media_player", "test_entry_setup_single_config"), ("tests.components.cast.test_media_player", "test_entry_setup_list_config"), From c08ca8a439ad9a617e619db6c2d7703bad1ead95 Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Sun, 29 Mar 2020 07:18:15 +1100 Subject: [PATCH 291/431] Fix GDACS tests (#33357) * enable tests * only remove entity if exists --- homeassistant/components/gdacs/geo_location.py | 3 ++- tests/ignore_uncaught_exceptions.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/gdacs/geo_location.py b/homeassistant/components/gdacs/geo_location.py index f434645ca20..c45d6e56425 100644 --- a/homeassistant/components/gdacs/geo_location.py +++ b/homeassistant/components/gdacs/geo_location.py @@ -110,7 +110,8 @@ class GdacsEvent(GeolocationEvent): self._remove_signal_update() # Remove from entity registry. entity_registry = await async_get_registry(self.hass) - entity_registry.async_remove(self.entity_id) + if self.entity_id in entity_registry.entities: + entity_registry.async_remove(self.entity_id) @callback def _delete_callback(self): diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 34323f5b6d4..b6580b63d0e 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -54,8 +54,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.dyson.test_fan", "test_purecool_update_state_filter_inv"), ("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"), ("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"), - ("tests.components.gdacs.test_geo_location", "test_setup"), - ("tests.components.gdacs.test_sensor", "test_setup"), ("tests.components.geonetnz_quakes.test_geo_location", "test_setup"), ("tests.components.geonetnz_quakes.test_sensor", "test_setup"), ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), From 03a9e3284c5cb1e60a96ba54d57e7e5a7ab1edb0 Mon Sep 17 00:00:00 2001 From: Colin Harrington Date: Sat, 28 Mar 2020 15:21:28 -0500 Subject: [PATCH 292/431] Fix broken supported_features in Plum Lightpad (#33319) --- homeassistant/components/plum_lightpad/light.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index 63fa67f4da5..e19035789b8 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -74,7 +74,7 @@ class PlumLight(Light): """Flag supported features.""" if self._load.dimmable: return SUPPORT_BRIGHTNESS - return None + return 0 async def async_turn_on(self, **kwargs): """Turn the light on.""" From dd232a35072a68ec77d484f05872897ea82eb9f0 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Sun, 29 Mar 2020 00:19:43 +0300 Subject: [PATCH 293/431] Fix uncaught exceptions in dynalite (#33374) --- homeassistant/components/dynalite/__init__.py | 2 +- tests/components/dynalite/test_bridge.py | 6 ++++++ tests/ignore_uncaught_exceptions.py | 3 --- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/dynalite/__init__.py b/homeassistant/components/dynalite/__init__.py index e27bdfbb142..973d09a384f 100755 --- a/homeassistant/components/dynalite/__init__.py +++ b/homeassistant/components/dynalite/__init__.py @@ -141,7 +141,7 @@ async def async_entry_changed(hass, entry): """Reload entry since the data has changed.""" LOGGER.debug("Reconfiguring entry %s", entry.data) bridge = hass.data[DOMAIN][entry.entry_id] - await bridge.reload_config(entry.data) + bridge.reload_config(entry.data) LOGGER.debug("Reconfiguring entry finished %s", entry.data) diff --git a/tests/components/dynalite/test_bridge.py b/tests/components/dynalite/test_bridge.py index ee6baaa7561..97759e96b69 100755 --- a/tests/components/dynalite/test_bridge.py +++ b/tests/components/dynalite/test_bridge.py @@ -55,8 +55,11 @@ async def test_add_devices_then_register(hass): device1 = Mock() device1.category = "light" device1.name = "NAME" + device1.unique_id = "unique1" device2 = Mock() device2.category = "switch" + device2.name = "NAME2" + device2.unique_id = "unique2" new_device_func([device1, device2]) await hass.async_block_till_done() assert hass.states.get("light.name") @@ -78,8 +81,11 @@ async def test_register_then_add_devices(hass): device1 = Mock() device1.category = "light" device1.name = "NAME" + device1.unique_id = "unique1" device2 = Mock() device2.category = "switch" + device2.name = "NAME2" + device2.unique_id = "unique2" new_device_func([device1, device2]) await hass.async_block_till_done() assert hass.states.get("light.name") diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index b6580b63d0e..16bd736cef6 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -21,9 +21,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.dsmr.test_sensor", "test_belgian_meter_low"), ("tests.components.dsmr.test_sensor", "test_tcp"), ("tests.components.dsmr.test_sensor", "test_connection_errors_retry"), - ("tests.components.dynalite.test_bridge", "test_add_devices_then_register"), - ("tests.components.dynalite.test_bridge", "test_register_then_add_devices"), - ("tests.components.dynalite.test_config_flow", "test_existing_update"), ("tests.components.dyson.test_air_quality", "test_purecool_aiq_attributes"), ("tests.components.dyson.test_air_quality", "test_purecool_aiq_update_state"), ( From 03a090e384ccf49c45aab376f694f23c0c73bd9d Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sun, 29 Mar 2020 00:15:32 +0100 Subject: [PATCH 294/431] Fixes for Sonos media position (#33379) --- .../components/sonos/media_player.py | 54 +++++++++++-------- 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 46e3765ad44..bb1179bb1e7 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -490,6 +490,11 @@ class SonosEntity(MediaPlayerDevice): """Return True if entity is available.""" return self._seen_timer is not None + def _clear_media_position(self): + """Clear the media_position.""" + self._media_position = None + self._media_position_updated_at = None + def _set_favorites(self): """Set available favorites.""" self._favorites = [] @@ -553,8 +558,6 @@ class SonosEntity(MediaPlayerDevice): self._shuffle = self.soco.shuffle self._uri = None self._media_duration = None - self._media_position = None - self._media_position_updated_at = None self._media_image_url = None self._media_artist = None self._media_album_name = None @@ -570,16 +573,20 @@ class SonosEntity(MediaPlayerDevice): self.update_media_linein(SOURCE_LINEIN) else: track_info = self.soco.get_current_track_info() - self._uri = track_info["uri"] - self._media_artist = track_info.get("artist") - self._media_album_name = track_info.get("album") - self._media_title = track_info.get("title") - - if self.soco.is_radio_uri(track_info["uri"]): - variables = event and event.variables - self.update_media_radio(variables, track_info) + if not track_info["uri"]: + self._clear_media_position() else: - self.update_media_music(update_position, track_info) + self._uri = track_info["uri"] + self._media_artist = track_info.get("artist") + self._media_album_name = track_info.get("album") + self._media_title = track_info.get("title") + + if self.soco.is_radio_uri(track_info["uri"]): + variables = event and event.variables + self.update_media_radio(variables, track_info) + else: + variables = event and event.variables + self.update_media_music(update_position, track_info) self.schedule_update_ha_state() @@ -591,11 +598,15 @@ class SonosEntity(MediaPlayerDevice): def update_media_linein(self, source): """Update state when playing from line-in/tv.""" + self._clear_media_position() + self._media_title = source self._source_name = source def update_media_radio(self, variables, track_info): """Update state when streaming radio.""" + self._clear_media_position() + try: library = pysonos.music_library.MusicLibrary(self.soco) album_art_uri = variables["current_track_meta_data"].album_art_uri @@ -627,26 +638,24 @@ class SonosEntity(MediaPlayerDevice): ) rel_time = _timespan_secs(position_info.get("RelTime")) - # player no longer reports position? - update_media_position |= rel_time is None and self._media_position is not None - # player started reporting position? update_media_position |= rel_time is not None and self._media_position is None # position jumped? - if ( - self.state == STATE_PLAYING - and rel_time is not None - and self._media_position is not None - ): - time_diff = utcnow() - self._media_position_updated_at - time_diff = time_diff.total_seconds() + if rel_time is not None and self._media_position is not None: + if self.state == STATE_PLAYING: + time_diff = utcnow() - self._media_position_updated_at + time_diff = time_diff.total_seconds() + else: + time_diff = 0 calculated_position = self._media_position + time_diff update_media_position |= abs(calculated_position - rel_time) > 1.5 - if update_media_position: + if rel_time is None: + self._clear_media_position() + elif update_media_position: self._media_position = rel_time self._media_position_updated_at = utcnow() @@ -770,6 +779,7 @@ class SonosEntity(MediaPlayerDevice): return self._shuffle @property + @soco_coordinator def media_content_id(self): """Content id of current playing media.""" return self._uri From 5bedc4ede2389d728f656eff7e2adffc823309a3 Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Sun, 29 Mar 2020 00:04:45 +0000 Subject: [PATCH 295/431] [ci skip] Translation update --- .../components/abode/.translations/no.json | 2 +- .../components/adguard/.translations/no.json | 2 +- .../components/airly/.translations/no.json | 4 +-- .../airvisual/.translations/no.json | 2 +- .../components/almond/.translations/no.json | 2 +- .../ambiclimate/.translations/no.json | 2 +- .../ambient_station/.translations/no.json | 2 +- .../components/august/.translations/no.json | 4 +-- .../components/axis/.translations/no.json | 2 +- .../components/cast/.translations/no.json | 2 +- .../coolmaster/.translations/no.json | 2 +- .../coronavirus/.translations/no.json | 2 +- .../components/daikin/.translations/no.json | 2 +- .../components/deconz/.translations/no.json | 2 +- .../components/directv/.translations/no.json | 4 +-- .../components/ecobee/.translations/no.json | 2 +- .../components/elkm1/.translations/ca.json | 28 +++++++++++++++++++ .../components/elkm1/.translations/de.json | 19 +++++++++++++ .../components/elkm1/.translations/fr.json | 21 ++++++++++++++ .../components/elkm1/.translations/ko.json | 28 +++++++++++++++++++ .../elkm1/.translations/zh-Hant.json | 28 +++++++++++++++++++ .../emulated_roku/.translations/no.json | 2 +- .../components/esphome/.translations/no.json | 6 ++-- .../components/freebox/.translations/no.json | 6 ++-- .../garmin_connect/.translations/no.json | 4 +-- .../components/gdacs/.translations/no.json | 2 +- .../components/geofency/.translations/no.json | 2 +- .../components/gios/.translations/no.json | 2 +- .../components/glances/.translations/no.json | 2 +- .../components/griddy/.translations/no.json | 2 +- .../components/harmony/.translations/no.json | 2 +- .../components/heos/.translations/no.json | 2 +- .../huawei_lte/.translations/no.json | 4 +-- .../components/hue/.translations/no.json | 4 +-- .../iaqualink/.translations/no.json | 2 +- .../components/iqvia/.translations/no.json | 2 +- .../components/izone/.translations/no.json | 4 +-- .../konnected/.translations/no.json | 2 +- .../components/locative/.translations/no.json | 2 +- .../logi_circle/.translations/no.json | 2 +- .../components/melcloud/.translations/no.json | 2 +- .../meteo_france/.translations/no.json | 4 +-- .../components/mikrotik/.translations/no.json | 4 +-- .../minecraft_server/.translations/no.json | 4 +-- .../monoprice/.translations/no.json | 6 ++-- .../components/mqtt/.translations/no.json | 4 +-- .../components/neato/.translations/no.json | 2 +- .../components/nest/.translations/no.json | 2 +- .../components/netatmo/.translations/no.json | 2 +- .../opentherm_gw/.translations/no.json | 2 +- .../owntracks/.translations/no.json | 2 +- .../components/plex/.translations/no.json | 6 ++-- .../components/point/.translations/no.json | 2 +- .../components/ps4/.translations/no.json | 10 +++---- .../components/rachio/.translations/no.json | 2 +- .../rainmachine/.translations/no.json | 2 +- .../components/ring/.translations/no.json | 2 +- .../samsungtv/.translations/no.json | 8 +++--- .../components/sense/.translations/no.json | 2 +- .../components/sentry/.translations/no.json | 4 +-- .../simplisafe/.translations/pl.json | 10 +++++++ .../smartthings/.translations/no.json | 2 +- .../solaredge/.translations/no.json | 2 +- .../components/solarlog/.translations/no.json | 2 +- .../components/soma/.translations/no.json | 6 ++-- .../components/spotify/.translations/no.json | 2 +- .../tellduslive/.translations/no.json | 2 +- .../components/tesla/.translations/no.json | 2 +- .../components/toon/.translations/no.json | 2 +- .../components/tplink/.translations/no.json | 4 +-- .../transmission/.translations/no.json | 4 +-- .../components/unifi/.translations/no.json | 2 +- .../components/vilfo/.translations/no.json | 2 +- .../components/vizio/.translations/no.json | 2 +- .../components/vizio/.translations/pl.json | 3 +- .../components/zha/.translations/no.json | 4 +-- .../components/zone/.translations/no.json | 2 +- 77 files changed, 238 insertions(+), 103 deletions(-) create mode 100644 homeassistant/components/elkm1/.translations/ca.json create mode 100644 homeassistant/components/elkm1/.translations/de.json create mode 100644 homeassistant/components/elkm1/.translations/fr.json create mode 100644 homeassistant/components/elkm1/.translations/ko.json create mode 100644 homeassistant/components/elkm1/.translations/zh-Hant.json diff --git a/homeassistant/components/abode/.translations/no.json b/homeassistant/components/abode/.translations/no.json index 542381cbb64..eefd4526d7f 100644 --- a/homeassistant/components/abode/.translations/no.json +++ b/homeassistant/components/abode/.translations/no.json @@ -17,6 +17,6 @@ "title": "Fyll ut innloggingsinformasjonen for Abode" } }, - "title": "Abode" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/adguard/.translations/no.json b/homeassistant/components/adguard/.translations/no.json index 22a8c23644f..d91f226a7eb 100644 --- a/homeassistant/components/adguard/.translations/no.json +++ b/homeassistant/components/adguard/.translations/no.json @@ -18,7 +18,7 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port", + "port": "", "ssl": "AdGuard Hjem bruker et SSL-sertifikat", "username": "Brukernavn", "verify_ssl": "AdGuard Home bruker et riktig sertifikat" diff --git a/homeassistant/components/airly/.translations/no.json b/homeassistant/components/airly/.translations/no.json index ada9955f9c5..79dfcd7307e 100644 --- a/homeassistant/components/airly/.translations/no.json +++ b/homeassistant/components/airly/.translations/no.json @@ -17,9 +17,9 @@ "name": "Navn p\u00e5 integrasjonen" }, "description": "Sett opp Airly luftkvalitet integrering. For \u00e5 generere API-n\u00f8kkel g\u00e5 til https://developer.airly.eu/register", - "title": "Airly" + "title": "" } }, - "title": "Airly" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/airvisual/.translations/no.json b/homeassistant/components/airvisual/.translations/no.json index 7c0b540f16a..82533db387f 100644 --- a/homeassistant/components/airvisual/.translations/no.json +++ b/homeassistant/components/airvisual/.translations/no.json @@ -18,7 +18,7 @@ "title": "Konfigurer AirVisual" } }, - "title": "AirVisual" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/almond/.translations/no.json b/homeassistant/components/almond/.translations/no.json index 47e32db0abe..63e1d99f7a9 100644 --- a/homeassistant/components/almond/.translations/no.json +++ b/homeassistant/components/almond/.translations/no.json @@ -14,6 +14,6 @@ "title": "Velg autentiseringsmetode" } }, - "title": "Almond" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/ambiclimate/.translations/no.json b/homeassistant/components/ambiclimate/.translations/no.json index e84de4ffc22..3f8e8444cf0 100644 --- a/homeassistant/components/ambiclimate/.translations/no.json +++ b/homeassistant/components/ambiclimate/.translations/no.json @@ -18,6 +18,6 @@ "title": "Autensiere Ambiclimate" } }, - "title": "Ambiclimate" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/ambient_station/.translations/no.json b/homeassistant/components/ambient_station/.translations/no.json index 4a089eba4c0..2b915aafce1 100644 --- a/homeassistant/components/ambient_station/.translations/no.json +++ b/homeassistant/components/ambient_station/.translations/no.json @@ -17,6 +17,6 @@ "title": "Fyll ut informasjonen din" } }, - "title": "Ambient PWS" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/august/.translations/no.json b/homeassistant/components/august/.translations/no.json index 61193656b51..449989bade1 100644 --- a/homeassistant/components/august/.translations/no.json +++ b/homeassistant/components/august/.translations/no.json @@ -23,10 +23,10 @@ "data": { "code": "Bekreftelseskode" }, - "description": "Kontroller {login_method} ( {username} ) og skriv inn bekreftelseskoden nedenfor", + "description": "Kontroller {login_method} ({username}) og skriv inn bekreftelseskoden nedenfor", "title": "To-faktor autentisering" } }, - "title": "August" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 32e1cc2fd40..010472d2cce 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -19,7 +19,7 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port", + "port": "", "username": "Brukernavn" }, "title": "Sett opp Axis enhet" diff --git a/homeassistant/components/cast/.translations/no.json b/homeassistant/components/cast/.translations/no.json index 6b8166f23c0..6c733896d27 100644 --- a/homeassistant/components/cast/.translations/no.json +++ b/homeassistant/components/cast/.translations/no.json @@ -7,7 +7,7 @@ "step": { "confirm": { "description": "\u00d8nsker du \u00e5 sette opp Google Cast?", - "title": "Google Cast" + "title": "" } }, "title": "Google Cast" diff --git a/homeassistant/components/coolmaster/.translations/no.json b/homeassistant/components/coolmaster/.translations/no.json index 90c40aaa617..e9859d23989 100644 --- a/homeassistant/components/coolmaster/.translations/no.json +++ b/homeassistant/components/coolmaster/.translations/no.json @@ -18,6 +18,6 @@ "title": "Konfigurer informasjonen om CoolMasterNet-tilkoblingen." } }, - "title": "CoolMasterNet" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/coronavirus/.translations/no.json b/homeassistant/components/coronavirus/.translations/no.json index ef5d75ac2a9..03a3ff49916 100644 --- a/homeassistant/components/coronavirus/.translations/no.json +++ b/homeassistant/components/coronavirus/.translations/no.json @@ -11,6 +11,6 @@ "title": "Velg et land du vil overv\u00e5ke" } }, - "title": "Coronavirus" + "title": "Koronavirus" } } \ No newline at end of file diff --git a/homeassistant/components/daikin/.translations/no.json b/homeassistant/components/daikin/.translations/no.json index 806106c5e52..30feb3b5acc 100644 --- a/homeassistant/components/daikin/.translations/no.json +++ b/homeassistant/components/daikin/.translations/no.json @@ -14,6 +14,6 @@ "title": "Konfigurer Daikin AC" } }, - "title": "Daikin AC" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index 3387c993ae0..e7c5893b4f1 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -24,7 +24,7 @@ "init": { "data": { "host": "Vert", - "port": "Port" + "port": "" }, "title": "Definer deCONZ-gatewayen" }, diff --git a/homeassistant/components/directv/.translations/no.json b/homeassistant/components/directv/.translations/no.json index 50f46fbb7bd..b010b1aac01 100644 --- a/homeassistant/components/directv/.translations/no.json +++ b/homeassistant/components/directv/.translations/no.json @@ -8,7 +8,7 @@ "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", "unknown": "Uventet feil" }, - "flow_title": "DirecTV: {name}", + "flow_title": "", "step": { "ssdp_confirm": { "description": "Vil du sette opp {name} ?", @@ -21,6 +21,6 @@ "title": "Koble til DirecTV-mottakeren" } }, - "title": "DirecTV" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/ecobee/.translations/no.json b/homeassistant/components/ecobee/.translations/no.json index efaa566c424..6658c3ac2e9 100644 --- a/homeassistant/components/ecobee/.translations/no.json +++ b/homeassistant/components/ecobee/.translations/no.json @@ -20,6 +20,6 @@ "title": "ecobee API-n\u00f8kkel" } }, - "title": "ecobee" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/ca.json b/homeassistant/components/elkm1/.translations/ca.json new file mode 100644 index 00000000000..a426b7a3433 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/ca.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "address_already_configured": "Ja hi ha un Elk-M1 configurat amb aquesta adre\u00e7a", + "already_configured": "Ja hi ha un Elk-M1 configurat amb aquest prefix" + }, + "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", + "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "address": "Adre\u00e7a IP, domini o port s\u00e8rie (si es est\u00e0 connectat amb una connexi\u00f3 s\u00e8rie).", + "password": "Contrasenya (nom\u00e9s segur).", + "prefix": "Prefix \u00fanic (deixa-ho en blanc si nom\u00e9s tens un \u00fanic controlador Elk-M1).", + "protocol": "Protocol", + "temperature_unit": "Unitats de temperatura que utilitza l'Elk-M1.", + "username": "Nom d'usuari (nom\u00e9s segur)." + }, + "description": "La cadena de car\u00e0cters (string) de l'adre\u00e7a ha de tenir el format: 'adre\u00e7a[:port]' tant per al mode 'segur' com el 'no segur'. Exemple: '192.168.1.1'. El port \u00e9s opcional, per defecte \u00e9s el 2101 pel mode 'no segur' i el 2601 pel 'segur'. Per al protocol s\u00e8rie, l'adre\u00e7a ha de tenir el format 'tty[:baud]'. Exemple: '/dev/ttyS1'. La velocitat en bauds \u00e9s opcional (115200 per defecte).", + "title": "Connexi\u00f3 amb el controlador Elk-M1" + } + }, + "title": "Controlador Elk-M1" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/de.json b/homeassistant/components/elkm1/.translations/de.json new file mode 100644 index 00000000000..3afcef8c464 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/de.json @@ -0,0 +1,19 @@ +{ + "config": { + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "invalid_auth": "Ung\u00fcltige Authentifizierung", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "protocol": "Protokoll", + "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit." + }, + "title": "Stellen Sie eine Verbindung zur Elk-M1-Steuerung her" + } + }, + "title": "Elk-M1-Steuerung" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/fr.json b/homeassistant/components/elkm1/.translations/fr.json new file mode 100644 index 00000000000..20ad7b8b007 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/fr.json @@ -0,0 +1,21 @@ +{ + "config": { + "error": { + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", + "invalid_auth": "Authentification non valide", + "unknown": "Erreur inattendue" + }, + "step": { + "user": { + "data": { + "address": "L'adresse IP ou le domaine ou le port s\u00e9rie si vous vous connectez via s\u00e9rie.", + "password": "Mot de passe (s\u00e9curis\u00e9 uniquement).", + "protocol": "Protocole", + "username": "Nom d'utilisateur (s\u00e9curis\u00e9 uniquement)." + }, + "title": "Se connecter a Elk-M1 Control" + } + }, + "title": "Elk-M1 Control" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/ko.json b/homeassistant/components/elkm1/.translations/ko.json new file mode 100644 index 00000000000..4ef1e528c22 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/ko.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "address_already_configured": "\uc774 \uc8fc\uc18c\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "already_configured": "\uc774 \uc811\ub450\uc0ac\ub85c ElkM1 \uc774 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "address": "\uc2dc\ub9ac\uc5bc\uc744 \ud1b5\ud574 \uc5f0\uacb0\ud558\ub294 \uacbd\uc6b0\uc758 IP \uc8fc\uc18c \ub098 \ub3c4\uba54\uc778 \ub610\ub294 \uc2dc\ub9ac\uc5bc \ud3ec\ud2b8", + "password": "\ube44\ubc00\ubc88\ud638 (\ubcf4\uc548 \uc804\uc6a9).", + "prefix": "\uace0\uc720\ud55c \uc811\ub450\uc0ac (ElkM1 \uc774 \ud558\ub098\ub9cc \uc788\uc73c\uba74 \ube44\uc6cc\ub450\uc138\uc694).", + "protocol": "\ud504\ub85c\ud1a0\ucf5c", + "temperature_unit": "ElkM1 \uc774 \uc0ac\uc6a9\ud558\ub294 \uc628\ub3c4 \ub2e8\uc704", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984 (\ubcf4\uc548 \uc804\uc6a9)." + }, + "description": "\uc8fc\uc18c \ubb38\uc790\uc5f4\uc740 '\ubcf4\uc548' \ubc0f '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 'address[:port]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '192.168.1.1'. \ud3ec\ud2b8\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 '\ube44\ubcf4\uc548' \uc758 \uacbd\uc6b0 2101 \uc774\uace0 '\ubcf4\uc548' \uc758 \uacbd\uc6b0 2601 \uc785\ub2c8\ub2e4. \uc2dc\ub9ac\uc5bc \ud504\ub85c\ud1a0\ucf5c\uc758 \uacbd\uc6b0 \uc8fc\uc18c\ub294 'tty[:baud]' \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4. \uc608: '/dev/ttyS1'. \ud1b5\uc2e0\uc18d\ub3c4 \ubc14\uc6b0\ub4dc\ub294 \uc120\ud0dd \uc0ac\ud56d\uc774\uba70 \uae30\ubcf8\uac12\uc740 115200 \uc785\ub2c8\ub2e4.", + "title": "Elk-M1 \uc81c\uc5b4\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "Elk-M1 \uc81c\uc5b4" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/zh-Hant.json b/homeassistant/components/elkm1/.translations/zh-Hant.json new file mode 100644 index 00000000000..d40d927ae8f --- /dev/null +++ b/homeassistant/components/elkm1/.translations/zh-Hant.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "address_already_configured": "\u4f7f\u7528\u6b64\u4f4d\u5740\u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "already_configured": "\u4f7f\u7528\u6b64 Prefix \u7684\u4e00\u7d44 ElkM1 \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "address": "IP \u6216\u7db2\u57df\u540d\u7a31\u3001\u5e8f\u5217\u57e0\uff08\u5047\u5982\u900f\u904e\u5e8f\u5217\u9023\u7dda\uff09\u3002", + "password": "\u5bc6\u78bc\uff08\u50c5\u52a0\u5bc6\uff09\u3002", + "prefix": "\u7368\u4e00\u7684 Prefix\uff08\u5047\u5982\u50c5\u6709\u4e00\u7d44 ElkM1 \u5247\u4fdd\u7559\u7a7a\u767d\uff09\u3002", + "protocol": "\u901a\u8a0a\u5354\u5b9a", + "temperature_unit": "ElkM1 \u4f7f\u7528\u6eab\u5ea6\u55ae\u4f4d\u3002", + "username": "\u4f7f\u7528\u8005\u540d\u7a31\uff08\u50c5\u52a0\u5bc6\uff09\u3002" + }, + "description": "\u52a0\u5bc6\u8207\u975e\u52a0\u5bc6\u4e4b\u4f4d\u5740\u5b57\u4e32\u683c\u5f0f\u5fc5\u9808\u70ba 'address[:port]'\u3002\u4f8b\u5982\uff1a'192.168.1.1'\u3002\u901a\u8a0a\u57e0\u70ba\u9078\u9805\u8f38\u5165\uff0c\u975e\u52a0\u5bc6\u9810\u8a2d\u503c\u70ba 2101\u3001\u52a0\u5bc6\u5247\u70ba 2601\u3002\u5e8f\u5217\u901a\u8a0a\u5354\u5b9a\u3001\u4f4d\u5740\u683c\u5f0f\u5fc5\u9808\u70ba 'tty[:baud]'\u3002\u4f8b\u5982\uff1a'/dev/ttyS1'\u3002\u50b3\u8f38\u7387\u70ba\u9078\u9805\u8f38\u5165\uff0c\u9810\u8a2d\u503c\u70ba 115200\u3002", + "title": "\u9023\u7dda\u81f3 Elk-M1 Control" + } + }, + "title": "Elk-M1 Control" + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_roku/.translations/no.json b/homeassistant/components/emulated_roku/.translations/no.json index b41da3ccde3..a0b8efcacd6 100644 --- a/homeassistant/components/emulated_roku/.translations/no.json +++ b/homeassistant/components/emulated_roku/.translations/no.json @@ -16,6 +16,6 @@ "title": "Definer serverkonfigurasjon" } }, - "title": "EmulatedRoku" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/esphome/.translations/no.json b/homeassistant/components/esphome/.translations/no.json index f7dac2a9d56..b4fa669fbff 100644 --- a/homeassistant/components/esphome/.translations/no.json +++ b/homeassistant/components/esphome/.translations/no.json @@ -24,12 +24,12 @@ "user": { "data": { "host": "Vert", - "port": "Port" + "port": "" }, "description": "Vennligst skriv inn tilkoblingsinnstillinger for din [ESPHome](https://esphomelib.com/) node.", - "title": "ESPHome" + "title": "" } }, - "title": "ESPHome" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/no.json b/homeassistant/components/freebox/.translations/no.json index a87c902b70a..cf8b3e55402 100644 --- a/homeassistant/components/freebox/.translations/no.json +++ b/homeassistant/components/freebox/.translations/no.json @@ -16,11 +16,11 @@ "user": { "data": { "host": "Vert", - "port": "Port" + "port": "" }, - "title": "Freebox" + "title": "" } }, - "title": "Freebox" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/garmin_connect/.translations/no.json b/homeassistant/components/garmin_connect/.translations/no.json index f7bdba27906..bc8669086b1 100644 --- a/homeassistant/components/garmin_connect/.translations/no.json +++ b/homeassistant/components/garmin_connect/.translations/no.json @@ -16,9 +16,9 @@ "username": "Brukernavn" }, "description": "Angi brukeropplysninger.", - "title": "Garmin Connect" + "title": "" } }, - "title": "Garmin Connect" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/gdacs/.translations/no.json b/homeassistant/components/gdacs/.translations/no.json index 54b3ca68451..f344c47365d 100644 --- a/homeassistant/components/gdacs/.translations/no.json +++ b/homeassistant/components/gdacs/.translations/no.json @@ -6,7 +6,7 @@ "step": { "user": { "data": { - "radius": "Radius" + "radius": "" }, "title": "Fyll ut filterdetaljene." } diff --git a/homeassistant/components/geofency/.translations/no.json b/homeassistant/components/geofency/.translations/no.json index 1956c453a9f..431c0e16e7d 100644 --- a/homeassistant/components/geofency/.translations/no.json +++ b/homeassistant/components/geofency/.translations/no.json @@ -13,6 +13,6 @@ "title": "Sett opp Geofency Webhook" } }, - "title": "Geofency Webhook" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/gios/.translations/no.json b/homeassistant/components/gios/.translations/no.json index b045c51e563..9842ae67a4b 100644 --- a/homeassistant/components/gios/.translations/no.json +++ b/homeassistant/components/gios/.translations/no.json @@ -18,6 +18,6 @@ "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } }, - "title": "GIO\u015a" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/glances/.translations/no.json b/homeassistant/components/glances/.translations/no.json index 7cf28cc34d0..b25241e34db 100644 --- a/homeassistant/components/glances/.translations/no.json +++ b/homeassistant/components/glances/.translations/no.json @@ -13,7 +13,7 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "Port", + "port": "", "ssl": "Bruk SSL / TLS for \u00e5 koble til Glances-systemet", "username": "Brukernavn", "verify_ssl": "Bekreft sertifiseringen av systemet", diff --git a/homeassistant/components/griddy/.translations/no.json b/homeassistant/components/griddy/.translations/no.json index 838c6c23668..b47fd213ae0 100644 --- a/homeassistant/components/griddy/.translations/no.json +++ b/homeassistant/components/griddy/.translations/no.json @@ -16,6 +16,6 @@ "title": "Sett opp din Griddy Load Zone" } }, - "title": "Griddy" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/harmony/.translations/no.json b/homeassistant/components/harmony/.translations/no.json index 2b102570d9a..6c989f8068b 100644 --- a/homeassistant/components/harmony/.translations/no.json +++ b/homeassistant/components/harmony/.translations/no.json @@ -11,7 +11,7 @@ "flow_title": "Logitech Harmony Hub {name}", "step": { "link": { - "description": "Vil du konfigurere {name} ( {host} )?", + "description": "Vil du konfigurere {name} ({host})?", "title": "Oppsett Logitech Harmony Hub" }, "user": { diff --git a/homeassistant/components/heos/.translations/no.json b/homeassistant/components/heos/.translations/no.json index d41051b6674..b54b5520943 100644 --- a/homeassistant/components/heos/.translations/no.json +++ b/homeassistant/components/heos/.translations/no.json @@ -16,6 +16,6 @@ "title": "Koble til Heos" } }, - "title": "HEOS" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/no.json b/homeassistant/components/huawei_lte/.translations/no.json index 39cb5bf87fe..606f6f96375 100644 --- a/homeassistant/components/huawei_lte/.translations/no.json +++ b/homeassistant/components/huawei_lte/.translations/no.json @@ -20,14 +20,14 @@ "user": { "data": { "password": "Passord", - "url": "URL", + "url": "", "username": "Brukernavn" }, "description": "Angi detaljer for enhetstilgang. Angivelse av brukernavn og passord er valgfritt, men gir st\u00f8tte for flere integreringsfunksjoner. P\u00e5 den annen side kan bruk av en autorisert tilkobling f\u00f8re til problemer med tilgang til enhetens webgrensesnitt utenfor Home Assistant mens integreringen er aktiv, og omvendt.", "title": "Konfigurer Huawei LTE" } }, - "title": "Huawei LTE" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/hue/.translations/no.json b/homeassistant/components/hue/.translations/no.json index e8718fe778b..961311d7304 100644 --- a/homeassistant/components/hue/.translations/no.json +++ b/homeassistant/components/hue/.translations/no.json @@ -23,9 +23,9 @@ }, "link": { "description": "Trykk p\u00e5 knappen p\u00e5 Bridgen for \u00e5 registrere Philips Hue med Home Assistant. \n\n ![Knappens plassering p\u00e5 Bridgen](/static/images/config_philips_hue.jpg)", - "title": "Link Hub" + "title": "" } }, - "title": "Philips Hue" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/iaqualink/.translations/no.json b/homeassistant/components/iaqualink/.translations/no.json index 9d464a6d516..0647f2ecfb8 100644 --- a/homeassistant/components/iaqualink/.translations/no.json +++ b/homeassistant/components/iaqualink/.translations/no.json @@ -16,6 +16,6 @@ "title": "Koble til iAqualink" } }, - "title": "Jandy iAqualink" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/iqvia/.translations/no.json b/homeassistant/components/iqvia/.translations/no.json index f04caf5bc8b..9ccca663fe5 100644 --- a/homeassistant/components/iqvia/.translations/no.json +++ b/homeassistant/components/iqvia/.translations/no.json @@ -10,7 +10,7 @@ "zip_code": "Postnummer" }, "description": "Fyll ut ditt amerikanske eller kanadiske postnummer.", - "title": "IQVIA" + "title": "" } }, "title": "IQVIA" diff --git a/homeassistant/components/izone/.translations/no.json b/homeassistant/components/izone/.translations/no.json index 9068b18c82d..6af3c9b063b 100644 --- a/homeassistant/components/izone/.translations/no.json +++ b/homeassistant/components/izone/.translations/no.json @@ -7,9 +7,9 @@ "step": { "confirm": { "description": "Vil du konfigurere iZone?", - "title": "iZone" + "title": "" } }, - "title": "iZone" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json index 72cd2911bbc..9e3b3bdb7c9 100644 --- a/homeassistant/components/konnected/.translations/no.json +++ b/homeassistant/components/konnected/.translations/no.json @@ -27,7 +27,7 @@ "title": "Oppdag Konnected Enheten" } }, - "title": "Konnected.io" + "title": "" }, "options": { "abort": { diff --git a/homeassistant/components/locative/.translations/no.json b/homeassistant/components/locative/.translations/no.json index c5ad3043004..123b03d95a8 100644 --- a/homeassistant/components/locative/.translations/no.json +++ b/homeassistant/components/locative/.translations/no.json @@ -13,6 +13,6 @@ "title": "Sett opp Locative Webhook" } }, - "title": "Locative Webhook" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/logi_circle/.translations/no.json b/homeassistant/components/logi_circle/.translations/no.json index 23b951bfa62..9f676e2acc7 100644 --- a/homeassistant/components/logi_circle/.translations/no.json +++ b/homeassistant/components/logi_circle/.translations/no.json @@ -27,6 +27,6 @@ "title": "Autentiseringsleverand\u00f8r" } }, - "title": "Logi Circle" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/melcloud/.translations/no.json b/homeassistant/components/melcloud/.translations/no.json index a464bbfda19..cdcc7087d06 100644 --- a/homeassistant/components/melcloud/.translations/no.json +++ b/homeassistant/components/melcloud/.translations/no.json @@ -18,6 +18,6 @@ "title": "Koble til MELCloud" } }, - "title": "MELCloud" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/meteo_france/.translations/no.json b/homeassistant/components/meteo_france/.translations/no.json index 1de1094f0a5..dc10ffd6a0f 100644 --- a/homeassistant/components/meteo_france/.translations/no.json +++ b/homeassistant/components/meteo_france/.translations/no.json @@ -10,9 +10,9 @@ "city": "By" }, "description": "Skriv inn postnummeret (bare for Frankrike, anbefalt) eller bynavn", - "title": "M\u00e9t\u00e9o-France" + "title": "" } }, - "title": "M\u00e9t\u00e9o-France" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/mikrotik/.translations/no.json b/homeassistant/components/mikrotik/.translations/no.json index f842dd148ec..8e18b27f0de 100644 --- a/homeassistant/components/mikrotik/.translations/no.json +++ b/homeassistant/components/mikrotik/.translations/no.json @@ -14,14 +14,14 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "Port", + "port": "", "username": "Brukernavn", "verify_ssl": "Bruk ssl" }, "title": "Konfigurere Mikrotik-ruter" } }, - "title": "Mikrotik" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json index f7be289d48c..c49c76865e4 100644 --- a/homeassistant/components/minecraft_server/.translations/no.json +++ b/homeassistant/components/minecraft_server/.translations/no.json @@ -13,12 +13,12 @@ "data": { "host": "Vert", "name": "Navn", - "port": "Port" + "port": "" }, "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", "title": "Link din Minecraft Server" } }, - "title": "Minecraft Server" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/monoprice/.translations/no.json b/homeassistant/components/monoprice/.translations/no.json index d4bf9a2ae9b..f17e48c2a78 100644 --- a/homeassistant/components/monoprice/.translations/no.json +++ b/homeassistant/components/monoprice/.translations/no.json @@ -27,9 +27,9 @@ "step": { "init": { "data": { - "source_1": "Navn p\u00e5 kilden #1", - "source_2": "Navn p\u00e5 kilden #2", - "source_3": "Navn p\u00e5 kilden #3", + "source_1": "Navn p\u00e5 kilde #1", + "source_2": "Navn p\u00e5 kilde #2", + "source_3": "Navn p\u00e5 kilde #3", "source_4": "Navn p\u00e5 kilde #4", "source_5": "Navn p\u00e5 kilde #5", "source_6": "Navn p\u00e5 kilde #6" diff --git a/homeassistant/components/mqtt/.translations/no.json b/homeassistant/components/mqtt/.translations/no.json index 27a77a25226..8416f74e086 100644 --- a/homeassistant/components/mqtt/.translations/no.json +++ b/homeassistant/components/mqtt/.translations/no.json @@ -12,7 +12,7 @@ "broker": "Megler", "discovery": "Aktiver oppdagelse", "password": "Passord", - "port": "Port", + "port": "", "username": "Brukernavn" }, "description": "Vennligst oppgi tilkoblingsinformasjonen for din MQTT megler.", @@ -22,7 +22,7 @@ "data": { "discovery": "Aktiver oppdagelse" }, - "description": "Vil du konfigurere Home Assistant til \u00e5 koble til MQTT broker som er levert av Hass.io-tillegget (addon)?", + "description": "Vil du konfigurere Home Assistant til \u00e5 koble til en MQTT megler som er levert av Hass.io-tillegget {addon}?", "title": "MQTT megler via Hass.io tillegg" } }, diff --git a/homeassistant/components/neato/.translations/no.json b/homeassistant/components/neato/.translations/no.json index 084c4b50e45..dc17289c0e3 100644 --- a/homeassistant/components/neato/.translations/no.json +++ b/homeassistant/components/neato/.translations/no.json @@ -22,6 +22,6 @@ "title": "Neato kontoinformasjon" } }, - "title": "Neato" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/nest/.translations/no.json b/homeassistant/components/nest/.translations/no.json index 743c47e00c8..9f19d22d939 100644 --- a/homeassistant/components/nest/.translations/no.json +++ b/homeassistant/components/nest/.translations/no.json @@ -28,6 +28,6 @@ "title": "Koble til Nest konto" } }, - "title": "Nest" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/netatmo/.translations/no.json b/homeassistant/components/netatmo/.translations/no.json index 68a91633642..98e5a7eb352 100644 --- a/homeassistant/components/netatmo/.translations/no.json +++ b/homeassistant/components/netatmo/.translations/no.json @@ -13,6 +13,6 @@ "title": "Velg autentiseringsmetode" } }, - "title": "Netatmo" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index 9eb4444cbf1..d05a8efe168 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -11,7 +11,7 @@ "data": { "device": "Bane eller URL-adresse", "floor_temperature": "Gulv klimatemperatur", - "id": "ID", + "id": "", "name": "Navn", "precision": "Klima temperaturpresisjon" }, diff --git a/homeassistant/components/owntracks/.translations/no.json b/homeassistant/components/owntracks/.translations/no.json index 5838dcad30b..aba620541ec 100644 --- a/homeassistant/components/owntracks/.translations/no.json +++ b/homeassistant/components/owntracks/.translations/no.json @@ -12,6 +12,6 @@ "title": "Sett opp OwnTracks" } }, - "title": "OwnTracks" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index c80ba5f2e06..29d43cb8275 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -20,7 +20,7 @@ "manual_setup": { "data": { "host": "Vert", - "port": "Port", + "port": "", "ssl": "Bruk SSL", "token": "Token (hvis n\u00f8dvendig)", "verify_ssl": "Verifisere SSL-sertifikat" @@ -29,7 +29,7 @@ }, "select_server": { "data": { - "server": "Server" + "server": "" }, "description": "Flere servere tilgjengelig, velg en:", "title": "Velg Plex-server" @@ -47,7 +47,7 @@ "title": "Koble til Plex-server" } }, - "title": "Plex" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/point/.translations/no.json b/homeassistant/components/point/.translations/no.json index c87c1a702c8..1448b56d848 100644 --- a/homeassistant/components/point/.translations/no.json +++ b/homeassistant/components/point/.translations/no.json @@ -27,6 +27,6 @@ "title": "Godkjenningsleverand\u00f8r" } }, - "title": "Minut Point" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/ps4/.translations/no.json b/homeassistant/components/ps4/.translations/no.json index 3608a5534ab..b5db81356d0 100644 --- a/homeassistant/components/ps4/.translations/no.json +++ b/homeassistant/components/ps4/.translations/no.json @@ -15,8 +15,8 @@ }, "step": { "creds": { - "description": "Legitimasjon n\u00f8dvendig. Trykk \"Send\" og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg \"Home-Assistant' enheten for \u00e5 fortsette.", - "title": "PlayStation 4" + "description": "Legitimasjon n\u00f8dvendig. Trykk 'Send' og deretter i PS4-ens andre skjerm app, kan du oppdatere enheter, og velg 'Home-Assistant' enheten for \u00e5 fortsette.", + "title": "" }, "link": { "data": { @@ -26,7 +26,7 @@ "region": "Region" }, "description": "Skriv inn PlayStation 4-informasjonen. For 'PIN', naviger til 'Innstillinger' p\u00e5 PlayStation 4-konsollen. Naviger deretter til 'Mobile App Connection Settings' og velg 'Add Device'. Tast inn PIN-koden som vises. Se [dokumentasjonen] (https://www.home-assistant.io/components/ps4/) for mer informasjon.", - "title": "PlayStation 4" + "title": "" }, "mode": { "data": { @@ -34,9 +34,9 @@ "mode": "Konfigureringsmodus" }, "description": "Velg modus for konfigurasjon. Feltet IP-adresse kan st\u00e5 tomt dersom du velger Auto Discovery, da enheter vil bli oppdaget automatisk.", - "title": "PlayStation 4" + "title": "" } }, - "title": "PlayStation 4" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/no.json b/homeassistant/components/rachio/.translations/no.json index eb93f749766..8b063018879 100644 --- a/homeassistant/components/rachio/.translations/no.json +++ b/homeassistant/components/rachio/.translations/no.json @@ -17,7 +17,7 @@ "title": "Koble til Rachio-enheten din" } }, - "title": "Rachio" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/rainmachine/.translations/no.json b/homeassistant/components/rainmachine/.translations/no.json index 980c2c693ce..34b74f56c49 100644 --- a/homeassistant/components/rainmachine/.translations/no.json +++ b/homeassistant/components/rainmachine/.translations/no.json @@ -12,7 +12,7 @@ "data": { "ip_address": "Vertsnavn eller IP-adresse", "password": "Passord", - "port": "Port" + "port": "" }, "title": "Fyll ut informasjonen din" } diff --git a/homeassistant/components/ring/.translations/no.json b/homeassistant/components/ring/.translations/no.json index 27dd7438f4a..bd8699a3617 100644 --- a/homeassistant/components/ring/.translations/no.json +++ b/homeassistant/components/ring/.translations/no.json @@ -22,6 +22,6 @@ "title": "Logg p\u00e5 med din Ring-konto" } }, - "title": "Ring" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/samsungtv/.translations/no.json b/homeassistant/components/samsungtv/.translations/no.json index 544ab581be8..55a03edc728 100644 --- a/homeassistant/components/samsungtv/.translations/no.json +++ b/homeassistant/components/samsungtv/.translations/no.json @@ -8,11 +8,11 @@ "not_successful": "Kan ikke koble til denne Samsung TV-enheten.", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, - "flow_title": "Samsung TV: {model}", + "flow_title": "", "step": { "confirm": { "description": "Vil du sette opp Samsung TV {model} ? Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning. Manuelle konfigurasjoner for denne TVen vil bli overskrevet.", - "title": "Samsung TV" + "title": "" }, "user": { "data": { @@ -20,9 +20,9 @@ "name": "Navn" }, "description": "Skriv inn Samsung TV-informasjonen din. Hvis du aldri har koblet til Home Assistant f\u00f8r, vil en popup p\u00e5 TVen be om godkjenning.", - "title": "Samsung TV" + "title": "" } }, - "title": "Samsung TV" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/sense/.translations/no.json b/homeassistant/components/sense/.translations/no.json index 70bd45558a3..d3fe4028e0f 100644 --- a/homeassistant/components/sense/.translations/no.json +++ b/homeassistant/components/sense/.translations/no.json @@ -17,6 +17,6 @@ "title": "Koble til din Sense Energi Monitor" } }, - "title": "Sense" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/sentry/.translations/no.json b/homeassistant/components/sentry/.translations/no.json index 79bb5f6cf87..36ce52f74ea 100644 --- a/homeassistant/components/sentry/.translations/no.json +++ b/homeassistant/components/sentry/.translations/no.json @@ -10,9 +10,9 @@ "step": { "user": { "description": "Fyll inn din Sentry DNS", - "title": "Sentry" + "title": "" } }, - "title": "Sentry" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index 3a9c160a0c5..75a38230e88 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -18,5 +18,15 @@ } }, "title": "SimpliSafe" + }, + "options": { + "step": { + "init": { + "data": { + "code": "Kod (u\u017cywany w interfejsie u\u017cytkownika Home Assistant'a)" + }, + "title": "Konfiguracja SimpliSafe" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/smartthings/.translations/no.json b/homeassistant/components/smartthings/.translations/no.json index b539e315ea3..a25de0e2feb 100644 --- a/homeassistant/components/smartthings/.translations/no.json +++ b/homeassistant/components/smartthings/.translations/no.json @@ -23,6 +23,6 @@ "title": "Installer SmartApp" } }, - "title": "SmartThings" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/solaredge/.translations/no.json b/homeassistant/components/solaredge/.translations/no.json index ad7cb55316b..4dd4177dd15 100644 --- a/homeassistant/components/solaredge/.translations/no.json +++ b/homeassistant/components/solaredge/.translations/no.json @@ -16,6 +16,6 @@ "title": "Definer API-parametrene for denne installasjonen" } }, - "title": "SolarEdge" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/solarlog/.translations/no.json b/homeassistant/components/solarlog/.translations/no.json index 017e886c817..9fddb46cdcf 100644 --- a/homeassistant/components/solarlog/.translations/no.json +++ b/homeassistant/components/solarlog/.translations/no.json @@ -16,6 +16,6 @@ "title": "Definer din Solar-Log tilkobling" } }, - "title": "Solar-Log" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/soma/.translations/no.json b/homeassistant/components/soma/.translations/no.json index 518cbc6a37e..c8cfa1473fe 100644 --- a/homeassistant/components/soma/.translations/no.json +++ b/homeassistant/components/soma/.translations/no.json @@ -14,12 +14,12 @@ "user": { "data": { "host": "Vert", - "port": "Port" + "port": "" }, "description": "Vennligst skriv tilkoblingsinnstillingene for din SOMA Connect.", - "title": "SOMA Connect" + "title": "" } }, - "title": "Soma" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/spotify/.translations/no.json b/homeassistant/components/spotify/.translations/no.json index 69b046cad0c..7a545d32bad 100644 --- a/homeassistant/components/spotify/.translations/no.json +++ b/homeassistant/components/spotify/.translations/no.json @@ -13,6 +13,6 @@ "title": "Velg autentiseringsmetode" } }, - "title": "Spotify" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/tellduslive/.translations/no.json b/homeassistant/components/tellduslive/.translations/no.json index 3258cf2ddca..3977bde4a3c 100644 --- a/homeassistant/components/tellduslive/.translations/no.json +++ b/homeassistant/components/tellduslive/.translations/no.json @@ -22,6 +22,6 @@ "title": "Velg endepunkt." } }, - "title": "Telldus Live" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/no.json b/homeassistant/components/tesla/.translations/no.json index 78311a46c26..8df2cdd2018 100644 --- a/homeassistant/components/tesla/.translations/no.json +++ b/homeassistant/components/tesla/.translations/no.json @@ -16,7 +16,7 @@ "title": "Tesla - Konfigurasjon" } }, - "title": "Tesla" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/toon/.translations/no.json b/homeassistant/components/toon/.translations/no.json index ed2af3ac379..80a101ac67b 100644 --- a/homeassistant/components/toon/.translations/no.json +++ b/homeassistant/components/toon/.translations/no.json @@ -29,6 +29,6 @@ "title": "Velg skjerm" } }, - "title": "Toon" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/tplink/.translations/no.json b/homeassistant/components/tplink/.translations/no.json index 41148475a5f..2cb30df1a42 100644 --- a/homeassistant/components/tplink/.translations/no.json +++ b/homeassistant/components/tplink/.translations/no.json @@ -7,9 +7,9 @@ "step": { "confirm": { "description": "Vil du konfigurere TP-Link smart enheter?", - "title": "TP-Link Smart Home" + "title": "" } }, - "title": "TP-Link Smart Home" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index 9cd19cd87f8..c46a6d782ea 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -21,13 +21,13 @@ "host": "Vert", "name": "Navn", "password": "Passord", - "port": "Port", + "port": "", "username": "Brukernavn" }, "title": "Oppsett av Transmission-klient" } }, - "title": "Transmission" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/unifi/.translations/no.json b/homeassistant/components/unifi/.translations/no.json index f6d3d250e31..156faf83d92 100644 --- a/homeassistant/components/unifi/.translations/no.json +++ b/homeassistant/components/unifi/.translations/no.json @@ -14,7 +14,7 @@ "data": { "host": "Vert", "password": "Passord", - "port": "Port", + "port": "", "site": "Nettsted-ID", "username": "Brukernavn", "verify_ssl": "Kontroller bruker riktig sertifikat" diff --git a/homeassistant/components/vilfo/.translations/no.json b/homeassistant/components/vilfo/.translations/no.json index af72a4bd7b0..61b9c56f496 100644 --- a/homeassistant/components/vilfo/.translations/no.json +++ b/homeassistant/components/vilfo/.translations/no.json @@ -18,6 +18,6 @@ "title": "Koble til Vilfo Ruteren" } }, - "title": "Vilfo Router" + "title": "" } } \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index 17812db5d9c..6391ac20aa7 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -60,7 +60,7 @@ "title": "Konfigurere Apper for Smart TV" } }, - "title": "Vizio SmartCast" + "title": "" }, "options": { "step": { diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index 1538ad09567..c5b26042799 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -31,7 +31,7 @@ "tv_apps": { "data": { "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", - "include_or_exclude": "Do\u0142\u0105cz lub wyklucz aplikacje?" + "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji" }, "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Konfigurowanie aplikacji dla smart TV" @@ -60,6 +60,7 @@ "step": { "init": { "data": { + "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", "volume_step": "Skok g\u0142o\u015bno\u015bci" }, diff --git a/homeassistant/components/zha/.translations/no.json b/homeassistant/components/zha/.translations/no.json index a08761ac4b6..656926017cf 100644 --- a/homeassistant/components/zha/.translations/no.json +++ b/homeassistant/components/zha/.translations/no.json @@ -12,10 +12,10 @@ "radio_type": "Radio type", "usb_path": "USB enhetsbane" }, - "title": "ZHA" + "title": "" } }, - "title": "ZHA" + "title": "" }, "device_automation": { "action_type": { diff --git a/homeassistant/components/zone/.translations/no.json b/homeassistant/components/zone/.translations/no.json index 3c1a91976f0..9bf6e189369 100644 --- a/homeassistant/components/zone/.translations/no.json +++ b/homeassistant/components/zone/.translations/no.json @@ -11,7 +11,7 @@ "longitude": "Lengdegrad", "name": "Navn", "passive": "Passiv", - "radius": "Radius" + "radius": "" }, "title": "Definer sone parametere" } From f7ae78f78e636fd47655051528e243199e5b0029 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sat, 28 Mar 2020 20:38:48 -0400 Subject: [PATCH 296/431] Update ZHA group entity when Zigbee group membership changes (#33378) * cleanup group entities * add test * appease pylint * fix order --- homeassistant/components/zha/core/const.py | 1 + homeassistant/components/zha/core/gateway.py | 7 ++ homeassistant/components/zha/entity.py | 79 +++++++++++++++++++- homeassistant/components/zha/fan.py | 54 +++---------- homeassistant/components/zha/light.py | 52 ++----------- homeassistant/components/zha/switch.py | 49 ++---------- tests/components/zha/test_light.py | 38 +++++++++- 7 files changed, 149 insertions(+), 131 deletions(-) diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index 43b1634cba7..da151f67dbb 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -208,6 +208,7 @@ SIGNAL_SET_LEVEL = "set_level" SIGNAL_STATE_ATTR = "update_state_attribute" SIGNAL_UPDATE_DEVICE = "{}_zha_update_device" SIGNAL_REMOVE_GROUP = "remove_group" +SIGNAL_GROUP_MEMBERSHIP_CHANGE = "group_membership_change" UNKNOWN = "unknown" UNKNOWN_MANUFACTURER = "unk_manufacturer" diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index bc7ff42d25f..fcc8a52360b 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -52,6 +52,7 @@ from .const import ( DEFAULT_DATABASE_NAME, DOMAIN, SIGNAL_ADD_ENTITIES, + SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, SIGNAL_REMOVE_GROUP, UNKNOWN_MANUFACTURER, @@ -256,6 +257,9 @@ class ZHAGateway: zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_member_removed - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) + async_dispatcher_send( + self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{zigpy_group.group_id}" + ) def group_member_added( self, zigpy_group: ZigpyGroupType, endpoint: ZigpyEndpointType @@ -265,6 +269,9 @@ class ZHAGateway: zha_group = self._async_get_or_create_group(zigpy_group) zha_group.info("group_member_added - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) + async_dispatcher_send( + self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{zigpy_group.group_id}" + ) def group_added(self, zigpy_group: ZigpyGroupType) -> None: """Handle zigpy group added event.""" diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 63ed3a6edc7..fda26f54d58 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -3,12 +3,13 @@ import asyncio import logging import time -from typing import Any, Awaitable, Dict, List +from typing import Any, Awaitable, Dict, List, Optional -from homeassistant.core import callback +from homeassistant.core import CALLBACK_TYPE, State, callback from homeassistant.helpers import entity from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.event import async_track_state_change from homeassistant.helpers.restore_state import RestoreEntity from .core.const import ( @@ -18,7 +19,9 @@ from .core.const import ( DATA_ZHA, DATA_ZHA_BRIDGE_ID, DOMAIN, + SIGNAL_GROUP_MEMBERSHIP_CHANGE, SIGNAL_REMOVE, + SIGNAL_REMOVE_GROUP, ) from .core.helpers import LogMixin from .core.typing import CALLABLE_T, ChannelsType, ChannelType, ZhaDeviceType @@ -213,3 +216,75 @@ class ZhaEntity(BaseZhaEntity): for channel in self.cluster_channels.values(): if hasattr(channel, "async_update"): await channel.async_update() + + +class ZhaGroupEntity(BaseZhaEntity): + """A base class for ZHA group entities.""" + + def __init__( + self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs + ) -> None: + """Initialize a light group.""" + super().__init__(unique_id, zha_device, **kwargs) + self._name = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" + self._group_id: int = group_id + self._entity_ids: List[str] = entity_ids + self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + await super().async_added_to_hass() + await self.async_accept_signal( + None, + f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", + self.async_remove, + signal_override=True, + ) + + await self.async_accept_signal( + None, + f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{self._group_id}", + self._update_group_entities, + signal_override=True, + ) + + @callback + def async_state_changed_listener( + entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener + ) + await self.async_update() + + def _update_group_entities(self): + """Update tracked entities when membership changes.""" + group = self.zha_device.gateway.get_group(self._group_id) + self._entity_ids = group.get_domain_entity_ids(self.platform.domain) + if self._async_unsub_state_changed is not None: + self._async_unsub_state_changed() + + @callback + def async_state_changed_listener( + entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener + ) + + async def async_will_remove_from_hass(self) -> None: + """Handle removal from Home Assistant.""" + await super().async_will_remove_from_hass() + if self._async_unsub_state_changed is not None: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + async def async_update(self) -> None: + """Update the state of the group entity.""" + pass diff --git a/homeassistant/components/zha/fan.py b/homeassistant/components/zha/fan.py index 027d0f8a1ee..c3cd88b0d6d 100644 --- a/homeassistant/components/zha/fan.py +++ b/homeassistant/components/zha/fan.py @@ -1,7 +1,7 @@ """Fans on Zigbee Home Automation networks.""" import functools import logging -from typing import List, Optional +from typing import List from zigpy.exceptions import DeliveryError import zigpy.zcl.clusters.hvac as hvac @@ -16,9 +16,8 @@ from homeassistant.components.fan import ( FanEntity, ) from homeassistant.const import STATE_UNAVAILABLE -from homeassistant.core import CALLBACK_TYPE, State, callback +from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_state_change from .core import discovery from .core.const import ( @@ -27,10 +26,9 @@ from .core.const import ( DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, - SIGNAL_REMOVE_GROUP, ) from .core.registries import ZHA_ENTITIES -from .entity import BaseZhaEntity, ZhaEntity +from .entity import ZhaEntity, ZhaGroupEntity _LOGGER = logging.getLogger(__name__) @@ -73,7 +71,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class BaseFan(BaseZhaEntity, FanEntity): +class BaseFan(FanEntity): """Base representation of a ZHA fan.""" def __init__(self, *args, **kwargs): @@ -120,9 +118,14 @@ class BaseFan(BaseZhaEntity, FanEntity): await self._fan_channel.async_set_speed(SPEED_TO_VALUE[speed]) self.async_set_state(0, "fan_mode", speed) + @callback + def async_set_state(self, attr_id, attr_name, value): + """Handle state update from channel.""" + pass + @STRICT_MATCH(channel_names=CHANNEL_FAN) -class ZhaFan(ZhaEntity, BaseFan): +class ZhaFan(BaseFan, ZhaEntity): """Representation of a ZHA fan.""" def __init__(self, unique_id, zha_device, channels, **kwargs): @@ -158,19 +161,15 @@ class ZhaFan(ZhaEntity, BaseFan): @GROUP_MATCH() -class FanGroup(BaseFan): +class FanGroup(BaseFan, ZhaGroupEntity): """Representation of a fan group.""" def __init__( self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a fan group.""" - super().__init__(unique_id, zha_device, **kwargs) - self._name: str = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" - self._group_id: int = group_id + super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) self._available: bool = False - self._entity_ids: List[str] = entity_ids - self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None group = self.zha_device.gateway.get_group(self._group_id) self._fan_channel = group.endpoint[hvac.Fan.cluster_id] @@ -185,35 +184,6 @@ class FanGroup(BaseFan): self._fan_channel.async_set_speed = async_set_speed - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - await super().async_added_to_hass() - await self.async_accept_signal( - None, - f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", - self.async_remove, - signal_override=True, - ) - - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener - ) - await self.async_update() - - async def async_will_remove_from_hass(self) -> None: - """Handle removal from Home Assistant.""" - await super().async_will_remove_from_hass() - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None - async def async_update(self): """Attempt to retrieve on off state from the fan.""" all_states = [self.hass.states.get(x) for x in self._entity_ids] diff --git a/homeassistant/components/zha/light.py b/homeassistant/components/zha/light.py index 07cbc6af78c..c6ec5c2ccf9 100644 --- a/homeassistant/components/zha/light.py +++ b/homeassistant/components/zha/light.py @@ -30,12 +30,9 @@ from homeassistant.components.light import ( SUPPORT_WHITE_VALUE, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_ON, STATE_UNAVAILABLE -from homeassistant.core import CALLBACK_TYPE, State, callback +from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import ( - async_track_state_change, - async_track_time_interval, -) +from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util from .core import discovery, helpers @@ -50,12 +47,12 @@ from .core.const import ( EFFECT_DEFAULT_VARIANT, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, - SIGNAL_REMOVE_GROUP, SIGNAL_SET_LEVEL, ) +from .core.helpers import LogMixin from .core.registries import ZHA_ENTITIES from .core.typing import ZhaDeviceType -from .entity import BaseZhaEntity, ZhaEntity +from .entity import ZhaEntity, ZhaGroupEntity _LOGGER = logging.getLogger(__name__) @@ -100,7 +97,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class BaseLight(BaseZhaEntity, light.Light): +class BaseLight(LogMixin, light.Light): """Operations common to all light entities.""" def __init__(self, *args, **kwargs): @@ -307,7 +304,7 @@ class BaseLight(BaseZhaEntity, light.Light): @STRICT_MATCH(channel_names=CHANNEL_ON_OFF, aux_channels={CHANNEL_COLOR, CHANNEL_LEVEL}) -class Light(ZhaEntity, BaseLight): +class Light(BaseLight, ZhaEntity): """Representation of a ZHA or ZLL light.""" _REFRESH_INTERVAL = (45, 75) @@ -471,52 +468,19 @@ class HueLight(Light): @GROUP_MATCH() -class LightGroup(BaseLight): +class LightGroup(BaseLight, ZhaGroupEntity): """Representation of a light group.""" def __init__( self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a light group.""" - super().__init__(unique_id, zha_device, **kwargs) - self._name = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" - self._group_id: int = group_id - self._entity_ids: List[str] = entity_ids + super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) group = self.zha_device.gateway.get_group(self._group_id) self._on_off_channel = group.endpoint[OnOff.cluster_id] self._level_channel = group.endpoint[LevelControl.cluster_id] self._color_channel = group.endpoint[Color.cluster_id] self._identify_channel = group.endpoint[Identify.cluster_id] - self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - await super().async_added_to_hass() - await self.async_accept_signal( - None, - f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", - self.async_remove, - signal_override=True, - ) - - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener - ) - await self.async_update() - - async def async_will_remove_from_hass(self) -> None: - """Handle removal from Home Assistant.""" - await super().async_will_remove_from_hass() - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None async def async_update(self) -> None: """Query all members and determine the light group state.""" diff --git a/homeassistant/components/zha/switch.py b/homeassistant/components/zha/switch.py index 90ec98ce1e3..328d9959ad2 100644 --- a/homeassistant/components/zha/switch.py +++ b/homeassistant/components/zha/switch.py @@ -1,16 +1,15 @@ """Switches on Zigbee Home Automation networks.""" import functools import logging -from typing import Any, List, Optional +from typing import Any, List from zigpy.zcl.clusters.general import OnOff from zigpy.zcl.foundation import Status from homeassistant.components.switch import DOMAIN, SwitchDevice from homeassistant.const import STATE_ON, STATE_UNAVAILABLE -from homeassistant.core import CALLBACK_TYPE, State, callback +from homeassistant.core import State, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import async_track_state_change from .core import discovery from .core.const import ( @@ -19,10 +18,9 @@ from .core.const import ( DATA_ZHA_DISPATCHERS, SIGNAL_ADD_ENTITIES, SIGNAL_ATTR_UPDATED, - SIGNAL_REMOVE_GROUP, ) from .core.registries import ZHA_ENTITIES -from .entity import BaseZhaEntity, ZhaEntity +from .entity import ZhaEntity, ZhaGroupEntity _LOGGER = logging.getLogger(__name__) STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) @@ -43,7 +41,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) -class BaseSwitch(BaseZhaEntity, SwitchDevice): +class BaseSwitch(SwitchDevice): """Common base class for zha switches.""" def __init__(self, *args, **kwargs): @@ -77,7 +75,7 @@ class BaseSwitch(BaseZhaEntity, SwitchDevice): @STRICT_MATCH(channel_names=CHANNEL_ON_OFF) -class Switch(ZhaEntity, BaseSwitch): +class Switch(BaseSwitch, ZhaEntity): """ZHA switch.""" def __init__(self, unique_id, zha_device, channels, **kwargs): @@ -113,50 +111,17 @@ class Switch(ZhaEntity, BaseSwitch): @GROUP_MATCH() -class SwitchGroup(BaseSwitch): +class SwitchGroup(BaseSwitch, ZhaGroupEntity): """Representation of a switch group.""" def __init__( self, entity_ids: List[str], unique_id: str, group_id: int, zha_device, **kwargs ) -> None: """Initialize a switch group.""" - super().__init__(unique_id, zha_device, **kwargs) - self._name: str = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" - self._group_id: int = group_id + super().__init__(entity_ids, unique_id, group_id, zha_device, **kwargs) self._available: bool = False - self._entity_ids: List[str] = entity_ids group = self.zha_device.gateway.get_group(self._group_id) self._on_off_channel = group.endpoint[OnOff.cluster_id] - self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None - - async def async_added_to_hass(self) -> None: - """Register callbacks.""" - await super().async_added_to_hass() - await self.async_accept_signal( - None, - f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", - self.async_remove, - signal_override=True, - ) - - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener - ) - await self.async_update() - - async def async_will_remove_from_hass(self) -> None: - """Handle removal from Home Assistant.""" - await super().async_will_remove_from_hass() - if self._async_unsub_state_changed is not None: - self._async_unsub_state_changed() - self._async_unsub_state_changed = None async def async_update(self) -> None: """Query all members and determine the light group state.""" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index f832b9e86e0..9bdd4966a4a 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -30,6 +30,7 @@ ON = 1 OFF = 0 IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e8" LIGHT_ON_OFF = { 1: { @@ -140,6 +141,31 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): return zha_device +@pytest.fixture +async def device_light_3(hass, zigpy_device_mock, zha_device_joined): + """Test zha light platform.""" + + zigpy_device = zigpy_device_mock( + { + 1: { + "in_clusters": [ + general.OnOff.cluster_id, + general.LevelControl.cluster_id, + lighting.Color.cluster_id, + general.Groups.cluster_id, + general.Identify.cluster_id, + ], + "out_clusters": [], + "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, + } + }, + ieee=IEEE_GROUPABLE_DEVICE3, + ) + zha_device = await zha_device_joined(zigpy_device) + zha_device.set_available(True) + return zha_device + + @patch("zigpy.zcl.clusters.general.OnOff.read_attributes", new=MagicMock()) async def test_light_refresh(hass, zigpy_device_mock, zha_device_joined_restored): """Test zha light platform refresh.""" @@ -414,7 +440,7 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): async def async_test_zha_group_light_entity( - hass, device_light_1, device_light_2, coordinator + hass, device_light_1, device_light_2, device_light_3, coordinator ): """Test the light entity for a ZHA group.""" zha_gateway = get_zha_gateway(hass) @@ -445,6 +471,7 @@ async def async_test_zha_group_light_entity( dev1_cluster_on_off = device_light_1.endpoints[1].on_off dev2_cluster_on_off = device_light_2.endpoints[1].on_off + dev3_cluster_on_off = device_light_3.endpoints[1].on_off # test that the lights were created and that they are unavailable assert hass.states.get(entity_id).state == STATE_UNAVAILABLE @@ -503,3 +530,12 @@ async def async_test_zha_group_light_entity( # test that group light is now back on assert hass.states.get(entity_id).state == STATE_ON + + # test that group light is now off + await group_cluster_on_off.off() + assert hass.states.get(entity_id).state == STATE_OFF + + # add a new member and test that his state is also tracked + await zha_group.async_add_members([device_light_3.ieee]) + await dev3_cluster_on_off.on() + assert hass.states.get(entity_id).state == STATE_ON From de2f5065857ccae647959fa8d77aebfbdcf86186 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Sun, 29 Mar 2020 05:57:06 +0200 Subject: [PATCH 297/431] Migrate GIOS to use DataUpdateCoordinator (#33306) * Migrate to DataUpdateCoordinator * Simplify code --- homeassistant/components/gios/__init__.py | 54 ++++------ homeassistant/components/gios/air_quality.py | 107 ++++++++++--------- homeassistant/components/gios/const.py | 3 +- 3 files changed, 78 insertions(+), 86 deletions(-) diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 981de6395de..0a7973709c1 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -1,5 +1,4 @@ """The GIOS component.""" -import asyncio import logging from aiohttp.client_exceptions import ClientConnectorError @@ -7,18 +6,17 @@ from async_timeout import timeout from gios import ApiError, Gios, NoStationError from homeassistant.core import Config, HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.util import Throttle +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import CONF_STATION_ID, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN +from .const import CONF_STATION_ID, DOMAIN, SCAN_INTERVAL _LOGGER = logging.getLogger(__name__) async def async_setup(hass: HomeAssistant, config: Config) -> bool: """Set up configured GIOS.""" - hass.data[DOMAIN] = {} - hass.data[DOMAIN][DATA_CLIENT] = {} return True @@ -29,11 +27,14 @@ async def async_setup_entry(hass, config_entry): websession = async_get_clientsession(hass) - gios = GiosData(websession, station_id) + coordinator = GiosDataUpdateCoordinator(hass, websession, station_id) + await coordinator.async_refresh() - await gios.async_update() + if not coordinator.last_update_success: + raise ConfigEntryNotReady - hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = gios + hass.data.setdefault(DOMAIN, {}) + hass.data[DOMAIN][config_entry.entry_id] = coordinator hass.async_create_task( hass.config_entries.async_forward_entry_setup(config_entry, "air_quality") @@ -43,36 +44,27 @@ async def async_setup_entry(hass, config_entry): async def async_unload_entry(hass, config_entry): """Unload a config entry.""" - hass.data[DOMAIN][DATA_CLIENT].pop(config_entry.entry_id) + hass.data[DOMAIN].pop(config_entry.entry_id) await hass.config_entries.async_forward_entry_unload(config_entry, "air_quality") return True -class GiosData: +class GiosDataUpdateCoordinator(DataUpdateCoordinator): """Define an object to hold GIOS data.""" - def __init__(self, session, station_id): - """Initialize.""" - self._gios = Gios(station_id, session) - self.station_id = station_id - self.sensors = {} - self.latitude = None - self.longitude = None - self.station_name = None - self.available = True + def __init__(self, hass, session, station_id): + """Class to manage fetching GIOS data API.""" + self.gios = Gios(station_id, session) - @Throttle(DEFAULT_SCAN_INTERVAL) - async def async_update(self): - """Update GIOS data.""" + super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) + + async def _async_update_data(self): + """Update data via library.""" try: with timeout(30): - await self._gios.update() - except asyncio.TimeoutError: - _LOGGER.error("Asyncio Timeout Error") + await self.gios.update() except (ApiError, NoStationError, ClientConnectorError) as error: - _LOGGER.error("GIOS data update failed: %s", error) - self.available = self._gios.available - self.latitude = self._gios.latitude - self.longitude = self._gios.longitude - self.station_name = self._gios.station_name - self.sensors = self._gios.data + raise UpdateFailed(error) + if not self.gios.data: + raise UpdateFailed("Invalid sensors data") + return self.gios.data diff --git a/homeassistant/components/gios/air_quality.py b/homeassistant/components/gios/air_quality.py index f7285c8cc5a..c8cd8be11c7 100644 --- a/homeassistant/components/gios/air_quality.py +++ b/homeassistant/components/gios/air_quality.py @@ -10,19 +10,27 @@ from homeassistant.components.air_quality import ( ) from homeassistant.const import CONF_NAME -from .const import ATTR_STATION, DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, ICONS_MAP +from .const import ATTR_STATION, DOMAIN, ICONS_MAP ATTRIBUTION = "Data provided by GIOŚ" -SCAN_INTERVAL = DEFAULT_SCAN_INTERVAL + +SENSOR_MAP = { + "CO": ATTR_CO, + "NO2": ATTR_NO2, + "O3": ATTR_OZONE, + "PM10": ATTR_PM_10, + "PM2.5": ATTR_PM_2_5, + "SO2": ATTR_SO2, +} async def async_setup_entry(hass, config_entry, async_add_entities): """Add a GIOS entities from a config_entry.""" name = config_entry.data[CONF_NAME] - data = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] + coordinator = hass.data[DOMAIN][config_entry.entry_id] - async_add_entities([GiosAirQuality(data, name)], True) + async_add_entities([GiosAirQuality(coordinator, name)], False) def round_state(func): @@ -40,17 +48,10 @@ def round_state(func): class GiosAirQuality(AirQualityEntity): """Define an GIOS sensor.""" - def __init__(self, gios, name): + def __init__(self, coordinator, name): """Initialize.""" - self.gios = gios + self.coordinator = coordinator self._name = name - self._aqi = None - self._co = None - self._no2 = None - self._o3 = None - self._pm_2_5 = None - self._pm_10 = None - self._so2 = None self._attrs = {} @property @@ -61,50 +62,50 @@ class GiosAirQuality(AirQualityEntity): @property def icon(self): """Return the icon.""" - if self._aqi in ICONS_MAP: - return ICONS_MAP[self._aqi] + if self.air_quality_index in ICONS_MAP: + return ICONS_MAP[self.air_quality_index] return "mdi:blur" @property def air_quality_index(self): """Return the air quality index.""" - return self._aqi + return self._get_sensor_value("AQI") @property @round_state def particulate_matter_2_5(self): """Return the particulate matter 2.5 level.""" - return self._pm_2_5 + return self._get_sensor_value("PM2.5") @property @round_state def particulate_matter_10(self): """Return the particulate matter 10 level.""" - return self._pm_10 + return self._get_sensor_value("PM10") @property @round_state def ozone(self): """Return the O3 (ozone) level.""" - return self._o3 + return self._get_sensor_value("O3") @property @round_state def carbon_monoxide(self): """Return the CO (carbon monoxide) level.""" - return self._co + return self._get_sensor_value("CO") @property @round_state def sulphur_dioxide(self): """Return the SO2 (sulphur dioxide) level.""" - return self._so2 + return self._get_sensor_value("SO2") @property @round_state def nitrogen_dioxide(self): """Return the NO2 (nitrogen dioxide) level.""" - return self._no2 + return self._get_sensor_value("NO2") @property def attribution(self): @@ -114,45 +115,45 @@ class GiosAirQuality(AirQualityEntity): @property def unique_id(self): """Return a unique_id for this entity.""" - return self.gios.station_id + return self.coordinator.gios.station_id + + @property + def should_poll(self): + """Return the polling requirement of the entity.""" + return False @property def available(self): """Return True if entity is available.""" - return self.gios.available + return self.coordinator.last_update_success @property def device_state_attributes(self): """Return the state attributes.""" - self._attrs[ATTR_STATION] = self.gios.station_name + # Different measuring stations have different sets of sensors. We don't know + # what data we will get. + for sensor in SENSOR_MAP: + if sensor in self.coordinator.data: + self._attrs[f"{SENSOR_MAP[sensor]}_index"] = self.coordinator.data[ + sensor + ]["index"] + self._attrs[ATTR_STATION] = self.coordinator.gios.station_name return self._attrs - async def async_update(self): - """Get the data from GIOS.""" - await self.gios.async_update() + async def async_added_to_hass(self): + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) - if self.gios.available: - # Different measuring stations have different sets of sensors. We don't know - # what data we will get. - if "AQI" in self.gios.sensors: - self._aqi = self.gios.sensors["AQI"]["value"] - if "CO" in self.gios.sensors: - self._co = self.gios.sensors["CO"]["value"] - self._attrs[f"{ATTR_CO}_index"] = self.gios.sensors["CO"]["index"] - if "NO2" in self.gios.sensors: - self._no2 = self.gios.sensors["NO2"]["value"] - self._attrs[f"{ATTR_NO2}_index"] = self.gios.sensors["NO2"]["index"] - if "O3" in self.gios.sensors: - self._o3 = self.gios.sensors["O3"]["value"] - self._attrs[f"{ATTR_OZONE}_index"] = self.gios.sensors["O3"]["index"] - if "PM2.5" in self.gios.sensors: - self._pm_2_5 = self.gios.sensors["PM2.5"]["value"] - self._attrs[f"{ATTR_PM_2_5}_index"] = self.gios.sensors["PM2.5"][ - "index" - ] - if "PM10" in self.gios.sensors: - self._pm_10 = self.gios.sensors["PM10"]["value"] - self._attrs[f"{ATTR_PM_10}_index"] = self.gios.sensors["PM10"]["index"] - if "SO2" in self.gios.sensors: - self._so2 = self.gios.sensors["SO2"]["value"] - self._attrs[f"{ATTR_SO2}_index"] = self.gios.sensors["SO2"]["index"] + async def async_will_remove_from_hass(self): + """Disconnect from update signal.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + + async def async_update(self): + """Update GIOS entity.""" + await self.coordinator.async_request_refresh() + + def _get_sensor_value(self, sensor): + """Return value of specified sensor.""" + if sensor in self.coordinator.data: + return self.coordinator.data[sensor]["value"] + return None diff --git a/homeassistant/components/gios/const.py b/homeassistant/components/gios/const.py index 3588b5e8dfc..918b4fba2e4 100644 --- a/homeassistant/components/gios/const.py +++ b/homeassistant/components/gios/const.py @@ -4,10 +4,9 @@ from datetime import timedelta ATTR_NAME = "name" ATTR_STATION = "station" CONF_STATION_ID = "station_id" -DATA_CLIENT = "client" DEFAULT_NAME = "GIOŚ" # Term of service GIOŚ allow downloading data no more than twice an hour. -DEFAULT_SCAN_INTERVAL = timedelta(minutes=30) +SCAN_INTERVAL = timedelta(minutes=30) DOMAIN = "gios" AQI_GOOD = "dobry" From f32ae95ef418abfa3153e6896472bba2d227667b Mon Sep 17 00:00:00 2001 From: Richard de Boer Date: Sun, 29 Mar 2020 05:59:04 +0200 Subject: [PATCH 298/431] Fix media_player supported features default value (#33366) I looked at all media_player components and these were the only ones returning None: - bluesound - emby - mpd --- homeassistant/components/bluesound/media_player.py | 2 +- homeassistant/components/emby/media_player.py | 2 +- homeassistant/components/mpd/media_player.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 3ca9cb1f623..3f7dc41ffef 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -777,7 +777,7 @@ class BluesoundPlayer(MediaPlayerDevice): def supported_features(self): """Flag of media commands that are supported.""" if self._status is None: - return None + return 0 if self.is_grouped and not self.is_master: return SUPPORT_VOLUME_STEP | SUPPORT_VOLUME_SET | SUPPORT_VOLUME_MUTE diff --git a/homeassistant/components/emby/media_player.py b/homeassistant/components/emby/media_player.py index 56d68cee6b5..e063fc49f2f 100644 --- a/homeassistant/components/emby/media_player.py +++ b/homeassistant/components/emby/media_player.py @@ -305,7 +305,7 @@ class EmbyDevice(MediaPlayerDevice): """Flag media player features that are supported.""" if self.supports_remote_control: return SUPPORT_EMBY - return None + return 0 async def async_media_play(self): """Play media.""" diff --git a/homeassistant/components/mpd/media_player.py b/homeassistant/components/mpd/media_player.py index 4b6d63b4240..bec61b10a9f 100644 --- a/homeassistant/components/mpd/media_player.py +++ b/homeassistant/components/mpd/media_player.py @@ -249,7 +249,7 @@ class MpdDevice(MediaPlayerDevice): def supported_features(self): """Flag media player features that are supported.""" if self._status is None: - return None + return 0 supported = SUPPORT_MPD if "volume" in self._status: From d832ce0b267a9084876edb12aea9c60f2f41e6fc Mon Sep 17 00:00:00 2001 From: Austin Mroczek Date: Sat, 28 Mar 2020 20:59:57 -0700 Subject: [PATCH 299/431] =?UTF-8?q?Fix=20openweathermap=20sensor.py=20so?= =?UTF-8?q?=20no=20KeyError=20if=20raining=20is=20miss=E2=80=A6=20(#33372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- homeassistant/components/openweathermap/sensor.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/openweathermap/sensor.py b/homeassistant/components/openweathermap/sensor.py index ce32458f640..ac85eff6794 100644 --- a/homeassistant/components/openweathermap/sensor.py +++ b/homeassistant/components/openweathermap/sensor.py @@ -163,8 +163,9 @@ class OpenWeatherMapSensor(Entity): elif self.type == "clouds": self._state = data.get_clouds() elif self.type == "rain": - if data.get_rain(): - self._state = round(data.get_rain()["3h"], 0) + rain = data.get_rain() + if "3h" in rain: + self._state = round(rain["3h"], 0) self._unit_of_measurement = "mm" else: self._state = "not raining" From 42cb5a5239934f343ed817dde32eb89d299c45c7 Mon Sep 17 00:00:00 2001 From: Colin Harrington Date: Sat, 28 Mar 2020 23:01:22 -0500 Subject: [PATCH 300/431] Calculate Plum Lightpad brightness and glowIntensity correctly (#33352) * Plum Lightpad - glowIntensity is represented as a float/percentage Calculate brightness from the glowIntensity instead of the glowIntensity from brightness. * Renamed `_glowIntensity` to `_glow_intensity` * Added Rounding, converting to an int, min and max boxing * Added codeowners to the Plum Lightpad manifest.json --- CODEOWNERS | 1 + homeassistant/components/plum_lightpad/light.py | 14 ++++++++------ .../components/plum_lightpad/manifest.json | 2 +- 3 files changed, 10 insertions(+), 7 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index cbd4ae11c24..dee1a510e2e 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -284,6 +284,7 @@ homeassistant/components/plaato/* @JohNan homeassistant/components/plant/* @ChristianKuehnel homeassistant/components/plex/* @jjlawren homeassistant/components/plugwise/* @laetificat @CoMPaTech @bouwew +homeassistant/components/plum_lightpad/* @ColinHarrington homeassistant/components/point/* @fredrike homeassistant/components/powerwall/* @bdraco homeassistant/components/proxmoxve/* @k4ds3 diff --git a/homeassistant/components/plum_lightpad/light.py b/homeassistant/components/plum_lightpad/light.py index e19035789b8..1ce76d9dc5f 100644 --- a/homeassistant/components/plum_lightpad/light.py +++ b/homeassistant/components/plum_lightpad/light.py @@ -97,7 +97,7 @@ class GlowRing(Light): self._name = f"{lightpad.friendly_name} Glow Ring" self._state = lightpad.glow_enabled - self._brightness = lightpad.glow_intensity * 255.0 + self._glow_intensity = lightpad.glow_intensity self._red = lightpad.glow_color["red"] self._green = lightpad.glow_color["green"] @@ -112,7 +112,7 @@ class GlowRing(Light): config = event["changes"] self._state = config["glowEnabled"] - self._brightness = config["glowIntensity"] * 255.0 + self._glow_intensity = config["glowIntensity"] self._red = config["glowColor"]["red"] self._green = config["glowColor"]["green"] @@ -138,12 +138,12 @@ class GlowRing(Light): @property def brightness(self) -> int: """Return the brightness of this switch between 0..255.""" - return self._brightness + return min(max(int(round(self._glow_intensity * 255, 0)), 0), 255) @property def glow_intensity(self): """Brightness in float form.""" - return self._brightness / 255.0 + return self._glow_intensity @property def is_on(self) -> bool: @@ -163,7 +163,8 @@ class GlowRing(Light): async def async_turn_on(self, **kwargs): """Turn the light on.""" if ATTR_BRIGHTNESS in kwargs: - await self._lightpad.set_config({"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 + await self._lightpad.set_config({"glowIntensity": brightness_pct}) elif ATTR_HS_COLOR in kwargs: hs_color = kwargs[ATTR_HS_COLOR] red, green, blue = color_util.color_hs_to_RGB(*hs_color) @@ -174,6 +175,7 @@ class GlowRing(Light): async def async_turn_off(self, **kwargs): """Turn the light off.""" if ATTR_BRIGHTNESS in kwargs: - await self._lightpad.set_config({"glowIntensity": kwargs[ATTR_BRIGHTNESS]}) + brightness_pct = kwargs[ATTR_BRIGHTNESS] / 255.0 + await self._lightpad.set_config({"glowIntensity": brightness_pct}) else: await self._lightpad.set_config({"glowEnabled": False}) diff --git a/homeassistant/components/plum_lightpad/manifest.json b/homeassistant/components/plum_lightpad/manifest.json index e22f301bf38..1063d4b439e 100644 --- a/homeassistant/components/plum_lightpad/manifest.json +++ b/homeassistant/components/plum_lightpad/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/plum_lightpad", "requirements": ["plumlightpad==0.0.11"], "dependencies": [], - "codeowners": [] + "codeowners": ["@ColinHarrington"] } From 3c2df7f8f26d1baf6becce4fbe3f8c6a2326475b Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 29 Mar 2020 06:01:53 +0200 Subject: [PATCH 301/431] Fix homematicip_cloud tests that have uncaught exceptions (#33371) --- .../components/homematicip_cloud/device.py | 8 +++++-- .../components/homematicip_cloud/conftest.py | 14 ++++++++++++- .../homematicip_cloud/test_config_flow.py | 4 ++++ .../components/homematicip_cloud/test_init.py | 21 +++++++++++++------ tests/ignore_uncaught_exceptions.py | 15 ------------- 5 files changed, 38 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index f35b696767c..8e6ca1f75fe 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -121,9 +121,13 @@ class HomematicipGenericDevice(Entity): # Only go further if the device/entity should be removed from registries # due to a removal of the HmIP device. + if self.hmip_device_removed: - del self._hap.hmip_device_by_entity_id[self.entity_id] - await self.async_remove_from_registries() + try: + del self._hap.hmip_device_by_entity_id[self.entity_id] + await self.async_remove_from_registries() + except KeyError as err: + _LOGGER.debug("Error removing HMIP entity from registry: %s", err) async def async_remove_from_registries(self) -> None: """Remove entity/device from registry.""" diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 927690d881f..b1933604fbe 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -3,6 +3,7 @@ from asynctest import CoroutineMock, MagicMock, Mock, patch from homematicip.aio.auth import AsyncAuth from homematicip.aio.connection import AsyncConnection from homematicip.aio.home import AsyncHome +from homematicip.base.enums import WeatherCondition, WeatherDayTime import pytest from homeassistant import config_entries @@ -115,10 +116,21 @@ def simple_mock_home_fixture(): devices=[], groups=[], location=Mock(), - weather=Mock(create=True), + weather=Mock( + temperature=0.0, + weatherCondition=WeatherCondition.UNKNOWN, + weatherDayTime=WeatherDayTime.DAY, + minTemperature=0.0, + maxTemperature=0.0, + humidity=0, + windSpeed=0.0, + windDirection=0, + vaporAmount=0.0, + ), id=42, dutyCycle=88, connected=True, + currentAPVersion="2.0.36", ) with patch( diff --git a/tests/components/homematicip_cloud/test_config_flow.py b/tests/components/homematicip_cloud/test_config_flow.py index 6436433a147..ec13fc79536 100644 --- a/tests/components/homematicip_cloud/test_config_flow.py +++ b/tests/components/homematicip_cloud/test_config_flow.py @@ -52,6 +52,8 @@ async def test_flow_works(hass, simple_mock_home): ), patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -151,6 +153,8 @@ async def test_import_config(hass, simple_mock_home): ), patch( "homeassistant.components.homematicip_cloud.hap.HomematicipAuth.async_register", return_value=True, + ), patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", ): result = await hass.config_entries.flow.async_init( HMIPC_DOMAIN, context={"source": "import"}, data=IMPORT_CONFIG diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index f97e7114b94..8f2753bc499 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -39,7 +39,12 @@ async def test_config_with_accesspoint_passed_to_config_entry( # no acccesspoint exists assert not hass.data.get(HMIPC_DOMAIN) - assert await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config}) + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", + ): + assert await async_setup_component( + hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config} + ) # config_entry created for access point config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) @@ -77,7 +82,13 @@ async def test_config_already_registered_not_passed_to_config_entry( CONF_AUTHTOKEN: "123", CONF_NAME: "name", } - assert await async_setup_component(hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config}) + + with patch( + "homeassistant.components.homematicip_cloud.hap.HomematicipHAP.async_connect", + ): + assert await async_setup_component( + hass, HMIPC_DOMAIN, {HMIPC_DOMAIN: entry_config} + ) # no new config_entry created / still one config_entry config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) @@ -107,16 +118,14 @@ async def test_load_entry_fails_due_to_connection_error( assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY -async def test_load_entry_fails_due_to_generic_exception( - hass, hmip_config_entry, simple_mock_home -): +async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry): """Test load entry fails due to generic exception.""" hmip_config_entry.add_to_hass(hass) with patch( "homeassistant.components.homematicip_cloud.hap.AsyncHome.get_current_state", side_effect=Exception, - ): + ), patch("homematicip.aio.connection.AsyncConnection.init",): assert await async_setup_component(hass, HMIPC_DOMAIN, {}) assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 16bd736cef6..071504ad8b7 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -54,21 +54,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.geonetnz_quakes.test_geo_location", "test_setup"), ("tests.components.geonetnz_quakes.test_sensor", "test_setup"), ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), - ("tests.components.homematicip_cloud.test_config_flow", "test_flow_works"), - ("tests.components.homematicip_cloud.test_config_flow", "test_import_config"), - ("tests.components.homematicip_cloud.test_device", "test_hmip_remove_group"), - ( - "tests.components.homematicip_cloud.test_init", - "test_config_with_accesspoint_passed_to_config_entry", - ), - ( - "tests.components.homematicip_cloud.test_init", - "test_config_already_registered_not_passed_to_config_entry", - ), - ( - "tests.components.homematicip_cloud.test_init", - "test_load_entry_fails_due_to_generic_exception", - ), ("tests.components.hue.test_bridge", "test_handle_unauthorized"), ("tests.components.hue.test_init", "test_security_vuln_check"), ("tests.components.hue.test_light", "test_group_features"), From 312af53935a1bffd58b3b35e82e31292a6ec22ad Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 28 Mar 2020 23:02:29 -0500 Subject: [PATCH 302/431] Handle Plex certificate updates (#33230) * Handle Plex certificate updates * Use exception in place * Add test --- homeassistant/components/plex/__init__.py | 14 ++++++- homeassistant/components/plex/errors.py | 4 ++ homeassistant/components/plex/server.py | 38 +++++++++++++++++-- tests/components/plex/test_init.py | 46 ++++++++++++++++++++++- 4 files changed, 97 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 9d74ed8cb75..a73111793a7 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -46,6 +46,7 @@ from .const import ( SERVERS, WEBSOCKETS, ) +from .errors import ShouldUpdateConfigEntry from .server import PlexServer MEDIA_PLAYER_SCHEMA = vol.All( @@ -129,9 +130,20 @@ async def async_setup_entry(hass, entry): ) hass.config_entries.async_update_entry(entry, options=options) - plex_server = PlexServer(hass, server_config, entry.options) + plex_server = PlexServer( + hass, server_config, entry.data[CONF_SERVER_IDENTIFIER], entry.options + ) try: await hass.async_add_executor_job(plex_server.connect) + except ShouldUpdateConfigEntry: + new_server_data = { + **entry.data[PLEX_SERVER_CONFIG], + CONF_URL: plex_server.url_in_use, + CONF_SERVER: plex_server.friendly_name, + } + hass.config_entries.async_update_entry( + entry, data={**entry.data, PLEX_SERVER_CONFIG: new_server_data} + ) except requests.exceptions.ConnectionError as error: _LOGGER.error( "Plex server (%s) could not be reached: [%s]", diff --git a/homeassistant/components/plex/errors.py b/homeassistant/components/plex/errors.py index 11c15404f45..534c553d45e 100644 --- a/homeassistant/components/plex/errors.py +++ b/homeassistant/components/plex/errors.py @@ -12,3 +12,7 @@ class NoServersFound(PlexException): class ServerNotSpecified(PlexException): """Multiple servers linked to account without choice provided.""" + + +class ShouldUpdateConfigEntry(PlexException): + """Config entry data is out of date and should be updated.""" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 788c96e15d2..196968cc097 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,5 +1,7 @@ """Shared class to maintain Plex server instances.""" import logging +import ssl +from urllib.parse import urlparse import plexapi.myplex import plexapi.playqueue @@ -26,7 +28,7 @@ from .const import ( X_PLEX_PRODUCT, X_PLEX_VERSION, ) -from .errors import NoServersFound, ServerNotSpecified +from .errors import NoServersFound, ServerNotSpecified, ShouldUpdateConfigEntry _LOGGER = logging.getLogger(__name__) @@ -40,7 +42,7 @@ plexapi.X_PLEX_VERSION = X_PLEX_VERSION class PlexServer: """Manages a single Plex server connection.""" - def __init__(self, hass, server_config, options=None): + def __init__(self, hass, server_config, known_server_id=None, options=None): """Initialize a Plex server instance.""" self._hass = hass self._plex_server = None @@ -50,6 +52,7 @@ class PlexServer: self._token = server_config.get(CONF_TOKEN) self._server_name = server_config.get(CONF_SERVER) self._verify_ssl = server_config.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL) + self._server_id = known_server_id self.options = options self.server_choice = None self._accounts = [] @@ -64,6 +67,7 @@ class PlexServer: def connect(self): """Connect to a Plex server directly, obtaining direct URL if necessary.""" + config_entry_update_needed = False def _connect_with_token(): account = plexapi.myplex.MyPlexAccount(token=self._token) @@ -92,8 +96,33 @@ class PlexServer: self._url, self._token, session ) + def _update_plexdirect_hostname(): + account = plexapi.myplex.MyPlexAccount(token=self._token) + matching_server = [ + x.name + for x in account.resources() + if x.clientIdentifier == self._server_id + ][0] + self._plex_server = account.resource(matching_server).connect(timeout=10) + if self._url: - _connect_with_url() + try: + _connect_with_url() + except requests.exceptions.SSLError as error: + while error and not isinstance(error, ssl.SSLCertVerificationError): + error = error.__context__ # pylint: disable=no-member + if isinstance(error, ssl.SSLCertVerificationError): + domain = urlparse(self._url).netloc.split(":")[0] + if domain.endswith("plex.direct") and error.args[0].startswith( + f"hostname '{domain}' doesn't match" + ): + _LOGGER.warning( + "Plex SSL certificate's hostname changed, updating." + ) + _update_plexdirect_hostname() + config_entry_update_needed = True + else: + raise else: _connect_with_token() @@ -113,6 +142,9 @@ class PlexServer: self._version = self._plex_server.version + if config_entry_update_needed: + raise ShouldUpdateConfigEntry + def refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 3358ac1c2cb..387ce6cac03 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -1,6 +1,7 @@ """Tests for Plex setup.""" import copy from datetime import timedelta +import ssl from asynctest import patch import plexapi @@ -19,6 +20,7 @@ from homeassistant.const import ( CONF_PORT, CONF_SSL, CONF_TOKEN, + CONF_URL, CONF_VERIFY_SSL, ) from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -26,7 +28,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN -from .mock_classes import MockPlexServer +from .mock_classes import MockPlexAccount, MockPlexServer from tests.common import MockConfigEntry, async_fire_time_changed @@ -300,3 +302,45 @@ async def test_setup_with_photo_session(hass): sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) + + +async def test_setup_when_certificate_changed(hass): + """Test setup component when the Plex certificate has changed.""" + + old_domain = "1-2-3-4.1234567890abcdef1234567890abcdef.plex.direct" + old_url = f"https://{old_domain}:32400" + + OLD_HOSTNAME_DATA = copy.deepcopy(DEFAULT_DATA) + OLD_HOSTNAME_DATA[const.PLEX_SERVER_CONFIG][CONF_URL] = old_url + + class WrongCertHostnameException(requests.exceptions.SSLError): + """Mock the exception showing a mismatched hostname.""" + + def __init__(self): + self.__context__ = ssl.SSLCertVerificationError( + f"hostname '{old_domain}' doesn't match" + ) + + old_entry = MockConfigEntry( + domain=const.DOMAIN, + data=OLD_HOSTNAME_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + new_entry = MockConfigEntry(domain=const.DOMAIN, data=DEFAULT_DATA) + + with patch( + "plexapi.server.PlexServer", side_effect=WrongCertHostnameException + ), patch("plexapi.myplex.MyPlexAccount", return_value=MockPlexAccount()): + old_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(old_entry.entry_id) + await hass.async_block_till_done() + + assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 + assert old_entry.state == ENTRY_STATE_LOADED + + assert ( + old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] + == new_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] + ) From 1f9f5bfaa8204824703d8d6c641fb5b60f046fee Mon Sep 17 00:00:00 2001 From: bangom Date: Sun, 29 Mar 2020 12:17:53 +0200 Subject: [PATCH 303/431] Fix wrong SI unit_prefix in Integration component (#33227) (#33228) * wrong SI metric prefixes --- homeassistant/components/integration/sensor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/integration/sensor.py b/homeassistant/components/integration/sensor.py index 57ec6fefe29..6201348f21c 100644 --- a/homeassistant/components/integration/sensor.py +++ b/homeassistant/components/integration/sensor.py @@ -39,7 +39,7 @@ RIGHT_METHOD = "right" INTEGRATION_METHOD = [TRAPEZOIDAL_METHOD, LEFT_METHOD, RIGHT_METHOD] # SI Metric prefixes -UNIT_PREFIXES = {None: 1, "k": 10 ** 3, "G": 10 ** 6, "T": 10 ** 9} +UNIT_PREFIXES = {None: 1, "k": 10 ** 3, "M": 10 ** 6, "G": 10 ** 9, "T": 10 ** 12} # SI Time prefixes UNIT_TIME = { From 21098bc3e54bad126d7d1aba6ce485e62dc155e2 Mon Sep 17 00:00:00 2001 From: tiagofreire-pt <41837236+tiagofreire-pt@users.noreply.github.com> Date: Sun, 29 Mar 2020 12:03:46 +0100 Subject: [PATCH 304/431] Upgrade broadlink to 0.13.0 (#33240) * Upgrading the broadlink library version to 0.13.0 As stated here: https://pypi.org/project/broadlink/ * Update requirements_test_all.txt * Update requirements_all.txt * Delete requirements_test_all.txt * Revert "Delete requirements_test_all.txt" This reverts commit 008f6f20306ffaeaf3dec97a9ecd5403ad06df6e. * Revert "Revert "Delete requirements_test_all.txt"" This reverts commit 73e22fd0ffc4fd00ee0e728d2c66a0ac481f805b. * Revert "Revert "Revert "Delete requirements_test_all.txt""" This reverts commit 7662119c89b32a16a951818eadfc20898814570f. --- homeassistant/components/broadlink/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/broadlink/manifest.json b/homeassistant/components/broadlink/manifest.json index 3af11f47aad..a179ca9c066 100644 --- a/homeassistant/components/broadlink/manifest.json +++ b/homeassistant/components/broadlink/manifest.json @@ -2,7 +2,7 @@ "domain": "broadlink", "name": "Broadlink", "documentation": "https://www.home-assistant.io/integrations/broadlink", - "requirements": ["broadlink==0.12.0"], + "requirements": ["broadlink==0.13.0"], "dependencies": [], "codeowners": ["@danielhiversen", "@felipediel"] } diff --git a/requirements_all.txt b/requirements_all.txt index 99be3ba875e..461571f4101 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -355,7 +355,7 @@ boto3==1.9.252 bravia-tv==1.0.1 # homeassistant.components.broadlink -broadlink==0.12.0 +broadlink==0.13.0 # homeassistant.components.brother brother==0.1.9 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a2b29a3c0ba..18014eba15d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -137,7 +137,7 @@ bellows-homeassistant==0.14.0 bomradarloop==0.1.4 # homeassistant.components.broadlink -broadlink==0.12.0 +broadlink==0.13.0 # homeassistant.components.brother brother==0.1.9 From ffafcf27a849cc8e823ee198a6c11b328b453a37 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 29 Mar 2020 11:14:42 -0500 Subject: [PATCH 305/431] plexwebsocket bump 0.0.7 to fix ignored tests (#33398) * Bump plexwebsocket to 0.0.7 * Remove unnecessary test ignores --- homeassistant/components/plex/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/ignore_uncaught_exceptions.py | 14 -------------- 4 files changed, 3 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/plex/manifest.json b/homeassistant/components/plex/manifest.json index a106c230ae4..1c89bf2582a 100644 --- a/homeassistant/components/plex/manifest.json +++ b/homeassistant/components/plex/manifest.json @@ -3,7 +3,7 @@ "name": "Plex Media Server", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/plex", - "requirements": ["plexapi==3.3.0", "plexauth==0.0.5", "plexwebsocket==0.0.6"], + "requirements": ["plexapi==3.3.0", "plexauth==0.0.5", "plexwebsocket==0.0.7"], "dependencies": ["http"], "codeowners": ["@jjlawren"] } diff --git a/requirements_all.txt b/requirements_all.txt index 461571f4101..f41479d0f6a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1042,7 +1042,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.6 +plexwebsocket==0.0.7 # homeassistant.components.plum_lightpad plumlightpad==0.0.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 18014eba15d..e9379550163 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -402,7 +402,7 @@ plexapi==3.3.0 plexauth==0.0.5 # homeassistant.components.plex -plexwebsocket==0.0.6 +plexwebsocket==0.0.7 # homeassistant.components.mhz19 # homeassistant.components.serial_pm diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 071504ad8b7..e6dc2f7e4f5 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -101,20 +101,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.mqtt.test_state_vacuum", "test_unique_id"), ("tests.components.mqtt.test_switch", "test_unique_id"), ("tests.components.mqtt.test_switch", "test_entity_device_info_remove"), - ("tests.components.plex.test_config_flow", "test_import_success"), - ("tests.components.plex.test_config_flow", "test_single_available_server"), - ("tests.components.plex.test_config_flow", "test_multiple_servers_with_selection"), - ("tests.components.plex.test_config_flow", "test_adding_last_unconfigured_server"), - ("tests.components.plex.test_config_flow", "test_option_flow"), - ("tests.components.plex.test_config_flow", "test_option_flow_new_users_available"), - ("tests.components.plex.test_init", "test_setup_with_config"), - ("tests.components.plex.test_init", "test_setup_with_config_entry"), - ("tests.components.plex.test_init", "test_set_config_entry_unique_id"), - ("tests.components.plex.test_init", "test_setup_with_insecure_config_entry"), - ("tests.components.plex.test_init", "test_setup_with_photo_session"), - ("tests.components.plex.test_server", "test_new_users_available"), - ("tests.components.plex.test_server", "test_new_ignored_users_available"), - ("tests.components.plex.test_server", "test_mark_sessions_idle"), ("tests.components.qwikswitch.test_init", "test_binary_sensor_device"), ("tests.components.qwikswitch.test_init", "test_sensor_device"), ("tests.components.rflink.test_init", "test_send_command_invalid_arguments"), From 188ca630de37f57cb9e84dbebbc02a93751236a4 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 29 Mar 2020 13:26:48 -0400 Subject: [PATCH 306/431] Miscellaneous ZHA code cleanup (#33395) * cleanup state change listener * update group id handling and unique id * update test * add guards for last_seen updates --- homeassistant/components/zha/core/device.py | 3 +- .../components/zha/core/discovery.py | 2 +- homeassistant/components/zha/core/gateway.py | 6 +-- homeassistant/components/zha/core/store.py | 3 ++ homeassistant/components/zha/entity.py | 33 +++++++--------- tests/components/zha/common.py | 4 +- tests/components/zha/test_gateway.py | 38 +++++++++++++++++++ 7 files changed, 62 insertions(+), 27 deletions(-) diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 0a7278cb5d5..ad3d1ff18ad 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -398,7 +398,8 @@ class ZHADevice(LogMixin): @callback def async_update_last_seen(self, last_seen): """Set last seen on the zigpy device.""" - self._zigpy_device.last_seen = last_seen + if self._zigpy_device.last_seen is None and last_seen is not None: + self._zigpy_device.last_seen = last_seen @callback def async_get_info(self): diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 19a83c3b6bc..90ec0e6e250 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -198,7 +198,7 @@ class GroupProbe: entity_class, ( group.get_domain_entity_ids(domain), - f"{domain}_group_{group.group_id}", + f"{domain}_zha_group_0x{group.group_id:04x}", group.group_id, zha_gateway.coordinator_zha_device, ), diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index fcc8a52360b..14a6a5c839e 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -258,7 +258,7 @@ class ZHAGateway: zha_group.info("group_member_removed - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_REMOVED) async_dispatcher_send( - self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{zigpy_group.group_id}" + self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" ) def group_member_added( @@ -270,7 +270,7 @@ class ZHAGateway: zha_group.info("group_member_added - endpoint: %s", endpoint) self._send_group_gateway_message(zigpy_group, ZHA_GW_MSG_GROUP_MEMBER_ADDED) async_dispatcher_send( - self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{zigpy_group.group_id}" + self._hass, f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{zigpy_group.group_id:04x}" ) def group_added(self, zigpy_group: ZigpyGroupType) -> None: @@ -286,7 +286,7 @@ class ZHAGateway: zha_group = self._groups.pop(zigpy_group.group_id, None) zha_group.info("group_removed") async_dispatcher_send( - self._hass, f"{SIGNAL_REMOVE_GROUP}_{zigpy_group.group_id}" + self._hass, f"{SIGNAL_REMOVE_GROUP}_0x{zigpy_group.group_id:04x}" ) def _send_group_gateway_message( diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 00a4942c7b7..3838e9b6a50 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -78,6 +78,9 @@ class ZhaStorage: ieee_str: str = str(device.ieee) old = self.devices[ieee_str] + if old is not None and device.last_seen is None: + return + changes = {} changes["last_seen"] = device.last_seen diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index fda26f54d58..2d098d60bfb 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -226,7 +226,9 @@ class ZhaGroupEntity(BaseZhaEntity): ) -> None: """Initialize a light group.""" super().__init__(unique_id, zha_device, **kwargs) - self._name = f"{zha_device.gateway.groups.get(group_id).name}_group_{group_id}" + self._name = ( + f"{zha_device.gateway.groups.get(group_id).name}_zha_group_0x{group_id:04x}" + ) self._group_id: int = group_id self._entity_ids: List[str] = entity_ids self._async_unsub_state_changed: Optional[CALLBACK_TYPE] = None @@ -236,30 +238,30 @@ class ZhaGroupEntity(BaseZhaEntity): await super().async_added_to_hass() await self.async_accept_signal( None, - f"{SIGNAL_REMOVE_GROUP}_{self._group_id}", + f"{SIGNAL_REMOVE_GROUP}_0x{self._group_id:04x}", self.async_remove, signal_override=True, ) await self.async_accept_signal( None, - f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_{self._group_id}", + f"{SIGNAL_GROUP_MEMBERSHIP_CHANGE}_0x{self._group_id:04x}", self._update_group_entities, signal_override=True, ) - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener + self.hass, self._entity_ids, self.async_state_changed_listener ) await self.async_update() + @callback + def async_state_changed_listener( + self, entity_id: str, old_state: State, new_state: State + ): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + def _update_group_entities(self): """Update tracked entities when membership changes.""" group = self.zha_device.gateway.get_group(self._group_id) @@ -267,15 +269,8 @@ class ZhaGroupEntity(BaseZhaEntity): if self._async_unsub_state_changed is not None: self._async_unsub_state_changed() - @callback - def async_state_changed_listener( - entity_id: str, old_state: State, new_state: State - ): - """Handle child updates.""" - self.async_schedule_update_ha_state(True) - self._async_unsub_state_changed = async_track_state_change( - self.hass, self._entity_ids, async_state_changed_listener + self.hass, self._entity_ids, self.async_state_changed_listener ) async def async_will_remove_from_hass(self) -> None: diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 9c57b57419a..2f0966ae739 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -159,9 +159,7 @@ async def find_entity_id(domain, zha_device, hass): def async_find_group_entity_id(hass, domain, group): """Find the group entity id under test.""" - entity_id = ( - f"{domain}.{group.name.lower().replace(' ','_')}_group_0x{group.group_id:04x}" - ) + entity_id = f"{domain}.{group.name.lower().replace(' ','_')}_zha_group_0x{group.group_id:04x}" entity_ids = hass.states.async_entity_ids(domain) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index 80d96fa55bd..c5ae9142ff0 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,5 +1,6 @@ """Test ZHA Gateway.""" import logging +import time import pytest import zigpy.profiles.zha as zha @@ -167,3 +168,40 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord # the group entity should not have been cleaned up assert entity_id not in hass.states.async_entity_ids(LIGHT_DOMAIN) + + +async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): + """Test saving data after a delay.""" + zha_gateway = get_zha_gateway(hass) + assert zha_gateway is not None + await async_enable_traffic(hass, [zha_dev_basic]) + + assert zha_dev_basic.last_seen is not None + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == zha_dev_basic.last_seen + + assert zha_dev_basic.last_seen is not None + last_seen = zha_dev_basic.last_seen + + # test that we can't set None as last seen any more + zha_dev_basic.async_update_last_seen(None) + assert last_seen == zha_dev_basic.last_seen + + # test that we won't put None in storage + zigpy_dev_basic.last_seen = None + assert zha_dev_basic.last_seen is None + await zha_gateway.async_update_device_storage() + await hass.async_block_till_done() + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == last_seen + + # test that we can still set a good last_seen + last_seen = time.time() + zha_dev_basic.async_update_last_seen(last_seen) + assert last_seen == zha_dev_basic.last_seen + + # test that we still put good values in storage + await zha_gateway.async_update_device_storage() + await hass.async_block_till_done() + entry = zha_gateway.zha_storage.async_get_or_create_device(zha_dev_basic) + assert entry.last_seen == last_seen From dd3cd95954bb1a9f00a8d61f0927faa6246ad0fc Mon Sep 17 00:00:00 2001 From: jan iversen Date: Sun, 29 Mar 2020 19:39:30 +0200 Subject: [PATCH 307/431] Modbus patch, to allow communication with "slow" equipment using tcp (#32557) * modbus: bumb pymodbus version to 2.3.0 pymodbus version 1.5.2 did not support asyncio, and in general the async handling have been improved a lot in version 2.3.0. updated core/requirement*txt * updated core/CODEOWNERS committing result of 'python3 -m script.hassfest'. * modbus: change core connection to async change setup() --> async_setup and update() --> async_update() Use async_setup_platform() to complete the async connection to core. listen for EVENT_HOMEASSISTANT_START happens in async_setup() so it needs to be async_listen. But listen for EVENT_HOMEASSISTANT_STOP happens in start_modbus() which is a sync. function so it continues to be listen(). * modbus: move setup of pymodbus into modbushub setup of pymodbus is logically connected to the class modbushub, therefore move it into the class. Delay construction of pymodbus client until event EVENT_HOMEASSISTANT_START arrives. * modbus: use pymodbus async library convert pymodbus calls to refer to the async library. Remark: connect() is no longer needed, it is done when constructing the client. There are also automatic reconnect. * modbus: use async update for read/write Use async functions for read/write from pymodbus. change thread.Lock() to asyncio.Lock() * Modbus: patch for slow tcp equipment When connecting, via Modbus-TCP, so some equipment (like the huawei sun2000 inverter), they need time to prepare the protocol. Solution is to add a asyncio.sleep(x) after the connect() and before sending the first message. Add optional parameter "delay" to Modbus configuration. Default is 0, which means do not execute asyncio.sleep(). * Modbus: silence pylint false positive pylint does not accept that a class construction __new__ can return a tuple. * Modbus: move constants to const.py Create const.py with constants only used in the modbus integration. Duplicate entries are removed, but NOT any entry that would lead to a configuration change. Some entries were the same but with different names, in this case renaming is done. Also correct the tests. * Modbus: move connection error handling to ModbusHub Connection error handling depends on the hub, not the entity, therefore it is logical to have the handling in ModbusHub. All pymodbus call are added to 2 generic functions (read/write) in order not to duplicate the error handling code. Added property "available" to signal if the hub is connected. * Modbus: CI cleanup Solve CI problems. * Modbus: remove close of client close() no longer exist in the pymodbus library, use del client instead. * Modbus: correct review comments Adjust code based on review comments. * Modbus: remove twister dependency Pymodbus in asyncio mode do not use twister but still throws a warning if twister is not installed, this warning goes into homeassistant.log and can thus cause confusion among users. However installing twister just to avoid the warning is not the best solution, therefore removing dependency on twister. * Modbus: review, remove comments. remove commented out code. --- CODEOWNERS | 2 +- homeassistant/components/modbus/__init__.py | 291 +++++++++++------- .../components/modbus/binary_sensor.py | 67 ++-- homeassistant/components/modbus/climate.py | 119 +++---- homeassistant/components/modbus/const.py | 72 +++++ homeassistant/components/modbus/manifest.json | 4 +- homeassistant/components/modbus/sensor.py | 84 +++-- homeassistant/components/modbus/switch.py | 149 ++++----- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/modbus/test_modbus_sensor.py | 35 +-- 11 files changed, 435 insertions(+), 392 deletions(-) create mode 100644 homeassistant/components/modbus/const.py diff --git a/CODEOWNERS b/CODEOWNERS index dee1a510e2e..5ea7c376329 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -231,7 +231,7 @@ homeassistant/components/min_max/* @fabaff homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 -homeassistant/components/modbus/* @adamchengtkc +homeassistant/components/modbus/* @adamchengtkc @janiversen homeassistant/components/monoprice/* @etsinko homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 218d3d3baa9..9b055155306 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -1,13 +1,19 @@ """Support for Modbus.""" +import asyncio import logging -import threading -from pymodbus.client.sync import ModbusSerialClient, ModbusTcpClient, ModbusUdpClient +from pymodbus.client.asynchronous import schedulers +from pymodbus.client.asynchronous.serial import AsyncModbusSerialClient as ClientSerial +from pymodbus.client.asynchronous.tcp import AsyncModbusTCPClient as ClientTCP +from pymodbus.client.asynchronous.udp import AsyncModbusUDPClient as ClientUDP +from pymodbus.exceptions import ModbusException +from pymodbus.pdu import ExceptionResponse from pymodbus.transaction import ModbusRtuFramer import voluptuous as vol from homeassistant.const import ( ATTR_STATE, + CONF_DELAY, CONF_HOST, CONF_METHOD, CONF_NAME, @@ -19,24 +25,26 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -_LOGGER = logging.getLogger(__name__) +from .const import ( # DEFAULT_HUB, + ATTR_ADDRESS, + ATTR_HUB, + ATTR_UNIT, + ATTR_VALUE, + CONF_BAUDRATE, + CONF_BYTESIZE, + CONF_PARITY, + CONF_STOPBITS, + MODBUS_DOMAIN, + SERVICE_WRITE_COIL, + SERVICE_WRITE_REGISTER, +) -ATTR_ADDRESS = "address" -ATTR_HUB = "hub" -ATTR_UNIT = "unit" -ATTR_VALUE = "value" - -CONF_BAUDRATE = "baudrate" -CONF_BYTESIZE = "bytesize" +# Kept for compatibility with other integrations, TO BE REMOVED CONF_HUB = "hub" -CONF_PARITY = "parity" -CONF_STOPBITS = "stopbits" - DEFAULT_HUB = "default" -DOMAIN = "modbus" +DOMAIN = MODBUS_DOMAIN -SERVICE_WRITE_COIL = "write_coil" -SERVICE_WRITE_REGISTER = "write_register" +_LOGGER = logging.getLogger(__name__) BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) @@ -59,11 +67,12 @@ ETHERNET_SCHEMA = BASE_SCHEMA.extend( vol.Required(CONF_PORT): cv.port, vol.Required(CONF_TYPE): vol.Any("tcp", "udp", "rtuovertcp"), vol.Optional(CONF_TIMEOUT, default=3): cv.socket_timeout, + vol.Optional(CONF_DELAY, default=0): cv.positive_int, } ) CONFIG_SCHEMA = vol.Schema( - {DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, + {MODBUS_DOMAIN: vol.All(cv.ensure_list, [vol.Any(SERIAL_SCHEMA, ETHERNET_SCHEMA)])}, extra=vol.ALLOW_EXTRA, ) @@ -88,97 +97,65 @@ SERVICE_WRITE_COIL_SCHEMA = vol.Schema( ) -def setup_client(client_config): - """Set up pymodbus client.""" - client_type = client_config[CONF_TYPE] - - if client_type == "serial": - return ModbusSerialClient( - method=client_config[CONF_METHOD], - port=client_config[CONF_PORT], - baudrate=client_config[CONF_BAUDRATE], - stopbits=client_config[CONF_STOPBITS], - bytesize=client_config[CONF_BYTESIZE], - parity=client_config[CONF_PARITY], - timeout=client_config[CONF_TIMEOUT], - ) - if client_type == "rtuovertcp": - return ModbusTcpClient( - host=client_config[CONF_HOST], - port=client_config[CONF_PORT], - framer=ModbusRtuFramer, - timeout=client_config[CONF_TIMEOUT], - ) - if client_type == "tcp": - return ModbusTcpClient( - host=client_config[CONF_HOST], - port=client_config[CONF_PORT], - timeout=client_config[CONF_TIMEOUT], - ) - if client_type == "udp": - return ModbusUdpClient( - host=client_config[CONF_HOST], - port=client_config[CONF_PORT], - timeout=client_config[CONF_TIMEOUT], - ) - assert False - - -def setup(hass, config): +async def async_setup(hass, config): """Set up Modbus component.""" - hass.data[DOMAIN] = hub_collect = {} + hass.data[MODBUS_DOMAIN] = hub_collect = {} - for client_config in config[DOMAIN]: - client = setup_client(client_config) - name = client_config[CONF_NAME] - hub_collect[name] = ModbusHub(client, name) - _LOGGER.debug("Setting up hub: %s", client_config) + _LOGGER.debug("registering hubs") + for client_config in config[MODBUS_DOMAIN]: + hub_collect[client_config[CONF_NAME]] = ModbusHub(client_config, hass.loop) def stop_modbus(event): """Stop Modbus service.""" for client in hub_collect.values(): - client.close() + del client def start_modbus(event): """Start Modbus service.""" for client in hub_collect.values(): - client.connect() + _LOGGER.debug("setup hub %s", client.name) + client.setup() hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop_modbus) # Register services for modbus - hass.services.register( - DOMAIN, + hass.services.async_register( + MODBUS_DOMAIN, SERVICE_WRITE_REGISTER, write_register, schema=SERVICE_WRITE_REGISTER_SCHEMA, ) - hass.services.register( - DOMAIN, SERVICE_WRITE_COIL, write_coil, schema=SERVICE_WRITE_COIL_SCHEMA + hass.services.async_register( + MODBUS_DOMAIN, + SERVICE_WRITE_COIL, + write_coil, + schema=SERVICE_WRITE_COIL_SCHEMA, ) - def write_register(service): + async def write_register(service): """Write Modbus registers.""" unit = int(float(service.data[ATTR_UNIT])) address = int(float(service.data[ATTR_ADDRESS])) value = service.data[ATTR_VALUE] client_name = service.data[ATTR_HUB] if isinstance(value, list): - hub_collect[client_name].write_registers( + await hub_collect[client_name].write_registers( unit, address, [int(float(i)) for i in value] ) else: - hub_collect[client_name].write_register(unit, address, int(float(value))) + await hub_collect[client_name].write_register( + unit, address, int(float(value)) + ) - def write_coil(service): + async def write_coil(service): """Write Modbus coil.""" unit = service.data[ATTR_UNIT] address = service.data[ATTR_ADDRESS] state = service.data[ATTR_STATE] client_name = service.data[ATTR_HUB] - hub_collect[client_name].write_coil(unit, address, state) + await hub_collect[client_name].write_coil(unit, address, state) - hass.bus.listen_once(EVENT_HOMEASSISTANT_START, start_modbus) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, start_modbus) return True @@ -186,65 +163,153 @@ def setup(hass, config): class ModbusHub: """Thread safe wrapper class for pymodbus.""" - def __init__(self, modbus_client, name): + def __init__(self, client_config, main_loop): """Initialize the Modbus hub.""" - self._client = modbus_client - self._lock = threading.Lock() - self._name = name + _LOGGER.debug("Preparing setup: %s", client_config) + + # generic configuration + self._loop = main_loop + self._client = None + self._lock = asyncio.Lock() + self._config_name = client_config[CONF_NAME] + self._config_type = client_config[CONF_TYPE] + self._config_port = client_config[CONF_PORT] + self._config_timeout = client_config[CONF_TIMEOUT] + self._config_delay = client_config[CONF_DELAY] + + if self._config_type == "serial": + # serial configuration + self._config_method = client_config[CONF_METHOD] + self._config_baudrate = client_config[CONF_BAUDRATE] + self._config_stopbits = client_config[CONF_STOPBITS] + self._config_bytesize = client_config[CONF_BYTESIZE] + self._config_parity = client_config[CONF_PARITY] + else: + # network configuration + self._config_host = client_config[CONF_HOST] @property def name(self): """Return the name of this hub.""" - return self._name + return self._config_name - def close(self): - """Disconnect client.""" - with self._lock: - self._client.close() + async def _connect_delay(self): + if self._config_delay > 0: + await asyncio.sleep(self._config_delay) + self._config_delay = 0 - def connect(self): - """Connect client.""" - with self._lock: - self._client.connect() + def setup(self): + """Set up pymodbus client.""" + # pylint: disable = E0633 + # Client* do deliver loop, client as result but + # pylint does not accept that fact - def read_coils(self, unit, address, count): + _LOGGER.debug("doing setup") + if self._config_type == "serial": + _, self._client = ClientSerial( + schedulers.ASYNC_IO, + method=self._config_method, + port=self._config_port, + baudrate=self._config_baudrate, + stopbits=self._config_stopbits, + bytesize=self._config_bytesize, + parity=self._config_parity, + timeout=self._config_timeout, + loop=self._loop, + ) + elif self._config_type == "rtuovertcp": + _, self._client = ClientTCP( + schedulers.ASYNC_IO, + host=self._config_host, + port=self._config_port, + framer=ModbusRtuFramer, + timeout=self._config_timeout, + loop=self._loop, + ) + elif self._config_type == "tcp": + _, self._client = ClientTCP( + schedulers.ASYNC_IO, + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + loop=self._loop, + ) + elif self._config_type == "udp": + _, self._client = ClientUDP( + schedulers.ASYNC_IO, + host=self._config_host, + port=self._config_port, + timeout=self._config_timeout, + loop=self._loop, + ) + else: + assert False + + async def _read(self, unit, address, count, func): + """Read generic with error handling.""" + await self._connect_delay() + async with self._lock: + kwargs = {"unit": unit} if unit else {} + result = await func(address, count, **kwargs) + if isinstance(result, (ModbusException, ExceptionResponse)): + _LOGGER.error("Hub %s Exception (%s)", self._config_name, result) + return result + + async def _write(self, unit, address, value, func): + """Read generic with error handling.""" + await self._connect_delay() + async with self._lock: + kwargs = {"unit": unit} if unit else {} + await func(address, value, **kwargs) + + async def read_coils(self, unit, address, count): """Read coils.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_coils(address, count, **kwargs) + if self._client.protocol is None: + return None + return await self._read(unit, address, count, self._client.protocol.read_coils) - def read_discrete_inputs(self, unit, address, count): + async def read_discrete_inputs(self, unit, address, count): """Read discrete inputs.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_discrete_inputs(address, count, **kwargs) + if self._client.protocol is None: + return None + return await self._read( + unit, address, count, self._client.protocol.read_discrete_inputs + ) - def read_input_registers(self, unit, address, count): + async def read_input_registers(self, unit, address, count): """Read input registers.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_input_registers(address, count, **kwargs) + if self._client.protocol is None: + return None + return await self._read( + unit, address, count, self._client.protocol.read_input_registers + ) - def read_holding_registers(self, unit, address, count): + async def read_holding_registers(self, unit, address, count): """Read holding registers.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - return self._client.read_holding_registers(address, count, **kwargs) + if self._client.protocol is None: + return None + return await self._read( + unit, address, count, self._client.protocol.read_holding_registers + ) - def write_coil(self, unit, address, value): + async def write_coil(self, unit, address, value): """Write coil.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - self._client.write_coil(address, value, **kwargs) + if self._client.protocol is None: + return None + return await self._write(unit, address, value, self._client.protocol.write_coil) - def write_register(self, unit, address, value): + async def write_register(self, unit, address, value): """Write register.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - self._client.write_register(address, value, **kwargs) + if self._client.protocol is None: + return None + return await self._write( + unit, address, value, self._client.protocol.write_register + ) - def write_registers(self, unit, address, values): + async def write_registers(self, unit, address, values): """Write registers.""" - with self._lock: - kwargs = {"unit": unit} if unit else {} - self._client.write_registers(address, values, **kwargs) + if self._client.protocol is None: + return None + return await self._write( + unit, address, values, self._client.protocol.write_registers + ) diff --git a/homeassistant/components/modbus/binary_sensor.py b/homeassistant/components/modbus/binary_sensor.py index 8ea6e2dbfa6..51dfb7c5795 100644 --- a/homeassistant/components/modbus/binary_sensor.py +++ b/homeassistant/components/modbus/binary_sensor.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -14,27 +14,27 @@ from homeassistant.components.binary_sensor import ( from homeassistant.const import CONF_DEVICE_CLASS, CONF_NAME, CONF_SLAVE from homeassistant.helpers import config_validation as cv -from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN +from .const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CONF_ADDRESS, + CONF_COILS, + CONF_HUB, + CONF_INPUT_TYPE, + CONF_INPUTS, + DEFAULT_HUB, + MODBUS_DOMAIN, +) _LOGGER = logging.getLogger(__name__) -CONF_DEPRECATED_COIL = "coil" -CONF_DEPRECATED_COILS = "coils" - -CONF_INPUTS = "inputs" -CONF_INPUT_TYPE = "input_type" -CONF_ADDRESS = "address" - -DEFAULT_INPUT_TYPE_COIL = "coil" -DEFAULT_INPUT_TYPE_DISCRETE = "discrete_input" - PLATFORM_SCHEMA = vol.All( - cv.deprecated(CONF_DEPRECATED_COILS, CONF_INPUTS), + cv.deprecated(CONF_COILS, CONF_INPUTS), PLATFORM_SCHEMA.extend( { vol.Required(CONF_INPUTS): [ vol.All( - cv.deprecated(CONF_DEPRECATED_COIL, CONF_ADDRESS), + cv.deprecated(CALL_TYPE_COIL, CONF_ADDRESS), vol.Schema( { vol.Required(CONF_ADDRESS): cv.positive_int, @@ -43,10 +43,8 @@ PLATFORM_SCHEMA = vol.All( vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional( - CONF_INPUT_TYPE, default=DEFAULT_INPUT_TYPE_COIL - ): vol.In( - [DEFAULT_INPUT_TYPE_COIL, DEFAULT_INPUT_TYPE_DISCRETE] - ), + CONF_INPUT_TYPE, default=CALL_TYPE_COIL + ): vol.In([CALL_TYPE_COIL, CALL_TYPE_DISCRETE]), } ), ) @@ -56,7 +54,7 @@ PLATFORM_SCHEMA = vol.All( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus binary sensors.""" sensors = [] for entry in config[CONF_INPUTS]: @@ -109,33 +107,18 @@ class ModbusBinarySensor(BinarySensorDevice): """Return True if entity is available.""" return self._available - def update(self): + async def async_update(self): """Update the state of the sensor.""" - try: - if self._input_type == DEFAULT_INPUT_TYPE_COIL: - result = self._hub.read_coils(self._slave, self._address, 1) - else: - result = self._hub.read_discrete_inputs(self._slave, self._address, 1) - except ConnectionException: - self._set_unavailable() + if self._input_type == CALL_TYPE_COIL: + result = await self._hub.read_coils(self._slave, self._address, 1) + else: + result = await self._hub.read_discrete_inputs(self._slave, self._address, 1) + if result is None: + self._available = False return - if isinstance(result, (ModbusException, ExceptionResponse)): - self._set_unavailable() + self._available = False return self._value = result.bits[0] self._available = True - - def _set_unavailable(self): - """Set unavailable state and log it as an error.""" - if not self._available: - return - - _LOGGER.error( - "No response from hub %s, slave %s, address %s", - self._hub.name, - self._slave, - self._address, - ) - self._available = False diff --git a/homeassistant/components/modbus/climate.py b/homeassistant/components/modbus/climate.py index f83b7d7b901..182dfeef2de 100644 --- a/homeassistant/components/modbus/climate.py +++ b/homeassistant/components/modbus/climate.py @@ -3,7 +3,7 @@ import logging import struct from typing import Optional -from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -21,30 +21,31 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN +from .const import ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_CURRENT_TEMP, + CONF_CURRENT_TEMP_REGISTER_TYPE, + CONF_DATA_COUNT, + CONF_DATA_TYPE, + CONF_HUB, + CONF_MAX_TEMP, + CONF_MIN_TEMP, + CONF_OFFSET, + CONF_PRECISION, + CONF_SCALE, + CONF_STEP, + CONF_TARGET_TEMP, + CONF_UNIT, + DATA_TYPE_FLOAT, + DATA_TYPE_INT, + DATA_TYPE_UINT, + DEFAULT_HUB, + MODBUS_DOMAIN, +) _LOGGER = logging.getLogger(__name__) -CONF_TARGET_TEMP = "target_temp_register" -CONF_CURRENT_TEMP = "current_temp_register" -CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" -CONF_DATA_TYPE = "data_type" -CONF_COUNT = "data_count" -CONF_PRECISION = "precision" -CONF_SCALE = "scale" -CONF_OFFSET = "offset" -CONF_UNIT = "temperature_unit" -DATA_TYPE_INT = "int" -DATA_TYPE_UINT = "uint" -DATA_TYPE_FLOAT = "float" -CONF_MAX_TEMP = "max_temp" -CONF_MIN_TEMP = "min_temp" -CONF_STEP = "temp_step" -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE -HVAC_MODES = [HVAC_MODE_AUTO] - -DEFAULT_REGISTER_TYPE_HOLDING = "holding" -DEFAULT_REGISTER_TYPE_INPUT = "input" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -52,10 +53,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, vol.Required(CONF_TARGET_TEMP): cv.positive_int, - vol.Optional(CONF_COUNT, default=2): cv.positive_int, + vol.Optional(CONF_DATA_COUNT, default=2): cv.positive_int, vol.Optional( - CONF_CURRENT_TEMP_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING - ): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]), + CONF_CURRENT_TEMP_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING + ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), vol.Optional(CONF_DATA_TYPE, default=DATA_TYPE_FLOAT): vol.In( [DATA_TYPE_INT, DATA_TYPE_UINT, DATA_TYPE_FLOAT] ), @@ -71,7 +72,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus Thermostat Platform.""" name = config[CONF_NAME] modbus_slave = config[CONF_SLAVE] @@ -79,7 +80,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): current_temp_register = config[CONF_CURRENT_TEMP] current_temp_register_type = config[CONF_CURRENT_TEMP_REGISTER_TYPE] data_type = config[CONF_DATA_TYPE] - count = config[CONF_COUNT] + count = config[CONF_DATA_COUNT] precision = config[CONF_PRECISION] scale = config[CONF_SCALE] offset = config[CONF_OFFSET] @@ -167,14 +168,14 @@ class ModbusThermostat(ClimateDevice): @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return SUPPORT_TARGET_TEMPERATURE - def update(self): + async def async_update(self): """Update Target & Current Temperature.""" - self._target_temperature = self._read_register( - DEFAULT_REGISTER_TYPE_HOLDING, self._target_temperature_register + self._target_temperature = await self._read_register( + CALL_TYPE_REGISTER_HOLDING, self._target_temperature_register ) - self._current_temperature = self._read_register( + self._current_temperature = await self._read_register( self._current_temperature_register_type, self._current_temperature_register ) @@ -186,7 +187,7 @@ class ModbusThermostat(ClimateDevice): @property def hvac_modes(self): """Return the possible HVAC modes.""" - return HVAC_MODES + return [HVAC_MODE_AUTO] @property def name(self): @@ -223,7 +224,7 @@ class ModbusThermostat(ClimateDevice): """Return the supported step of target temperature.""" return self._temp_step - def set_temperature(self, **kwargs): + async def set_temperature(self, **kwargs): """Set new target temperature.""" target_temperature = int( (kwargs.get(ATTR_TEMPERATURE) - self._offset) / self._scale @@ -232,30 +233,28 @@ class ModbusThermostat(ClimateDevice): return byte_string = struct.pack(self._structure, target_temperature) register_value = struct.unpack(">h", byte_string[0:2])[0] - self._write_register(self._target_temperature_register, register_value) + await self._write_register(self._target_temperature_register, register_value) @property def available(self) -> bool: """Return True if entity is available.""" return self._available - def _read_register(self, register_type, register) -> Optional[float]: + async def _read_register(self, register_type, register) -> Optional[float]: """Read register using the Modbus hub slave.""" - try: - if register_type == DEFAULT_REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers( - self._slave, register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, register, self._count - ) - except ConnectionException: - self._set_unavailable(register) + if register_type == CALL_TYPE_REGISTER_INPUT: + result = await self._hub.read_input_registers( + self._slave, register, self._count + ) + else: + result = await self._hub.read_holding_registers( + self._slave, register, self._count + ) + if result is None: + self._available = False return - if isinstance(result, (ModbusException, ExceptionResponse)): - self._set_unavailable(register) + self._available = False return byte_string = b"".join( @@ -270,25 +269,7 @@ class ModbusThermostat(ClimateDevice): return register_value - def _write_register(self, register, value): + async def _write_register(self, register, value): """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_registers(self._slave, register, [value, 0]) - except ConnectionException: - self._set_unavailable(register) - return - + await self._hub.write_registers(self._slave, register, [value, 0]) self._available = True - - def _set_unavailable(self, register): - """Set unavailable state and log it as an error.""" - if not self._available: - return - - _LOGGER.error( - "No response from hub %s, slave %s, register %s", - self._hub.name, - self._slave, - register, - ) - self._available = False diff --git a/homeassistant/components/modbus/const.py b/homeassistant/components/modbus/const.py new file mode 100644 index 00000000000..e507717b22c --- /dev/null +++ b/homeassistant/components/modbus/const.py @@ -0,0 +1,72 @@ +"""Constants used in modbus integration.""" + +# configuration names +CONF_BAUDRATE = "baudrate" +CONF_BYTESIZE = "bytesize" +CONF_HUB = "hub" +CONF_PARITY = "parity" +CONF_STOPBITS = "stopbits" +CONF_REGISTER = "register" +CONF_REGISTER_TYPE = "register_type" +CONF_REGISTERS = "registers" +CONF_REVERSE_ORDER = "reverse_order" +CONF_SCALE = "scale" +CONF_COUNT = "count" +CONF_PRECISION = "precision" +CONF_OFFSET = "offset" +CONF_COILS = "coils" + +# integration names +DEFAULT_HUB = "default" +MODBUS_DOMAIN = "modbus" + +# data types +DATA_TYPE_CUSTOM = "custom" +DATA_TYPE_FLOAT = "float" +DATA_TYPE_INT = "int" +DATA_TYPE_UINT = "uint" + +# call types +CALL_TYPE_COIL = "coil" +CALL_TYPE_DISCRETE = "discrete_input" +CALL_TYPE_REGISTER_HOLDING = "holding" +CALL_TYPE_REGISTER_INPUT = "input" + +# the following constants are TBD. +# changing those in general causes a breaking change, because +# the contents of configuration.yaml needs to be updated, +# therefore they are left to a later date. +# but kept here, with a reference to the file using them. + +# __init.py +ATTR_ADDRESS = "address" +ATTR_HUB = "hub" +ATTR_UNIT = "unit" +ATTR_VALUE = "value" +SERVICE_WRITE_COIL = "write_coil" +SERVICE_WRITE_REGISTER = "write_register" + +# binary_sensor.py +CONF_INPUTS = "inputs" +CONF_INPUT_TYPE = "input_type" +CONF_ADDRESS = "address" + +# sensor.py +# CONF_DATA_TYPE = "data_type" + +# switch.py +CONF_STATE_OFF = "state_off" +CONF_STATE_ON = "state_on" +CONF_VERIFY_REGISTER = "verify_register" +CONF_VERIFY_STATE = "verify_state" + +# climate.py +CONF_TARGET_TEMP = "target_temp_register" +CONF_CURRENT_TEMP = "current_temp_register" +CONF_CURRENT_TEMP_REGISTER_TYPE = "current_temp_register_type" +CONF_DATA_TYPE = "data_type" +CONF_DATA_COUNT = "data_count" +CONF_UNIT = "temperature_unit" +CONF_MAX_TEMP = "max_temp" +CONF_MIN_TEMP = "min_temp" +CONF_STEP = "temp_step" diff --git a/homeassistant/components/modbus/manifest.json b/homeassistant/components/modbus/manifest.json index 92ebd5b8686..d1d2a9db550 100644 --- a/homeassistant/components/modbus/manifest.json +++ b/homeassistant/components/modbus/manifest.json @@ -2,7 +2,7 @@ "domain": "modbus", "name": "Modbus", "documentation": "https://www.home-assistant.io/integrations/modbus", - "requirements": ["pymodbus==1.5.2"], + "requirements": ["pymodbus==2.3.0"], "dependencies": [], - "codeowners": ["@adamchengtkc"] + "codeowners": ["@adamchengtkc", "@janiversen"] } diff --git a/homeassistant/components/modbus/sensor.py b/homeassistant/components/modbus/sensor.py index b586ad852df..8c2b950648b 100644 --- a/homeassistant/components/modbus/sensor.py +++ b/homeassistant/components/modbus/sensor.py @@ -3,7 +3,7 @@ import logging import struct from typing import Any, Optional, Union -from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -19,27 +19,28 @@ from homeassistant.const import ( from homeassistant.helpers import config_validation as cv from homeassistant.helpers.restore_state import RestoreEntity -from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN +from .const import ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_COUNT, + CONF_DATA_TYPE, + CONF_HUB, + CONF_PRECISION, + CONF_REGISTER, + CONF_REGISTER_TYPE, + CONF_REGISTERS, + CONF_REVERSE_ORDER, + CONF_SCALE, + DATA_TYPE_CUSTOM, + DATA_TYPE_FLOAT, + DATA_TYPE_INT, + DATA_TYPE_UINT, + DEFAULT_HUB, + MODBUS_DOMAIN, +) _LOGGER = logging.getLogger(__name__) -CONF_COUNT = "count" -CONF_DATA_TYPE = "data_type" -CONF_PRECISION = "precision" -CONF_REGISTER = "register" -CONF_REGISTER_TYPE = "register_type" -CONF_REGISTERS = "registers" -CONF_REVERSE_ORDER = "reverse_order" -CONF_SCALE = "scale" - -DATA_TYPE_CUSTOM = "custom" -DATA_TYPE_FLOAT = "float" -DATA_TYPE_INT = "int" -DATA_TYPE_UINT = "uint" - -DEFAULT_REGISTER_TYPE_HOLDING = "holding" -DEFAULT_REGISTER_TYPE_INPUT = "input" - def number(value: Any) -> Union[int, float]: """Coerce a value to number without losing precision.""" @@ -75,8 +76,8 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( vol.Optional(CONF_OFFSET, default=0): number, vol.Optional(CONF_PRECISION, default=0): cv.positive_int, vol.Optional( - CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING - ): vol.In([DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT]), + CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING + ): vol.In([CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT]), vol.Optional(CONF_REVERSE_ORDER, default=False): cv.boolean, vol.Optional(CONF_SCALE, default=1): number, vol.Optional(CONF_SLAVE): cv.positive_int, @@ -88,7 +89,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Modbus sensors.""" sensors = [] data_types = {DATA_TYPE_INT: {1: "h", 2: "i", 4: "q"}} @@ -218,23 +219,21 @@ class ModbusRegisterSensor(RestoreEntity): """Return True if entity is available.""" return self._available - def update(self): + async def async_update(self): """Update the state of the sensor.""" - try: - if self._register_type == DEFAULT_REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers( - self._slave, self._register, self._count - ) - else: - result = self._hub.read_holding_registers( - self._slave, self._register, self._count - ) - except ConnectionException: - self._set_unavailable() + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = await self._hub.read_input_registers( + self._slave, self._register, self._count + ) + else: + result = await self._hub.read_holding_registers( + self._slave, self._register, self._count + ) + if result is None: + self._available = False return - if isinstance(result, (ModbusException, ExceptionResponse)): - self._set_unavailable() + self._available = False return registers = result.registers @@ -252,16 +251,3 @@ class ModbusRegisterSensor(RestoreEntity): self._value = f"{val:.{self._precision}f}" self._available = True - - def _set_unavailable(self): - """Set unavailable state and log it as an error.""" - if not self._available: - return - - _LOGGER.error( - "No response from hub %s, slave %s, address %s", - self._hub.name, - self._slave, - self._register, - ) - self._available = False diff --git a/homeassistant/components/modbus/switch.py b/homeassistant/components/modbus/switch.py index d4f52622538..d7d6f121874 100644 --- a/homeassistant/components/modbus/switch.py +++ b/homeassistant/components/modbus/switch.py @@ -2,7 +2,7 @@ import logging from typing import Optional -from pymodbus.exceptions import ConnectionException, ModbusException +from pymodbus.exceptions import ModbusException from pymodbus.pdu import ExceptionResponse import voluptuous as vol @@ -18,22 +18,25 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import ToggleEntity from homeassistant.helpers.restore_state import RestoreEntity -from . import CONF_HUB, DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN +from .const import ( + CALL_TYPE_COIL, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_COILS, + CONF_HUB, + CONF_REGISTER, + CONF_REGISTER_TYPE, + CONF_REGISTERS, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY_REGISTER, + CONF_VERIFY_STATE, + DEFAULT_HUB, + MODBUS_DOMAIN, +) _LOGGER = logging.getLogger(__name__) -CONF_COIL = "coil" -CONF_COILS = "coils" -CONF_REGISTER = "register" -CONF_REGISTER_TYPE = "register_type" -CONF_REGISTERS = "registers" -CONF_STATE_OFF = "state_off" -CONF_STATE_ON = "state_on" -CONF_VERIFY_REGISTER = "verify_register" -CONF_VERIFY_STATE = "verify_state" - -DEFAULT_REGISTER_TYPE_HOLDING = "holding" -DEFAULT_REGISTER_TYPE_INPUT = "input" REGISTERS_SCHEMA = vol.Schema( { @@ -42,8 +45,8 @@ REGISTERS_SCHEMA = vol.Schema( vol.Required(CONF_NAME): cv.string, vol.Required(CONF_REGISTER): cv.positive_int, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, - vol.Optional(CONF_REGISTER_TYPE, default=DEFAULT_REGISTER_TYPE_HOLDING): vol.In( - [DEFAULT_REGISTER_TYPE_HOLDING, DEFAULT_REGISTER_TYPE_INPUT] + vol.Optional(CONF_REGISTER_TYPE, default=CALL_TYPE_REGISTER_HOLDING): vol.In( + [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT] ), vol.Optional(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_STATE_OFF): cv.positive_int, @@ -55,7 +58,7 @@ REGISTERS_SCHEMA = vol.Schema( COILS_SCHEMA = vol.Schema( { - vol.Required(CONF_COIL): cv.positive_int, + vol.Required(CALL_TYPE_COIL): cv.positive_int, vol.Required(CONF_NAME): cv.string, vol.Required(CONF_SLAVE): cv.positive_int, vol.Optional(CONF_HUB, default=DEFAULT_HUB): cv.string, @@ -73,7 +76,7 @@ PLATFORM_SCHEMA = vol.All( ) -def setup_platform(hass, config, add_entities, discovery_info=None): +async def async_setup_platform(hass, config, add_entities, discovery_info=None): """Read configuration and create Modbus devices.""" switches = [] if CONF_COILS in config: @@ -82,7 +85,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): hub = hass.data[MODBUS_DOMAIN][hub_name] switches.append( ModbusCoilSwitch( - hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CONF_COIL] + hub, coil[CONF_NAME], coil[CONF_SLAVE], coil[CALL_TYPE_COIL] ) ) if CONF_REGISTERS in config: @@ -143,28 +146,26 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): """Return True if entity is available.""" return self._available - def turn_on(self, **kwargs): + async def turn_on(self, **kwargs): """Set switch on.""" - self._write_coil(self._coil, True) + await self._write_coil(self._coil, True) - def turn_off(self, **kwargs): + async def turn_off(self, **kwargs): """Set switch off.""" - self._write_coil(self._coil, False) + await self._write_coil(self._coil, False) - def update(self): + async def async_update(self): """Update the state of the switch.""" - self._is_on = self._read_coil(self._coil) + self._is_on = await self._read_coil(self._coil) - def _read_coil(self, coil) -> Optional[bool]: + async def _read_coil(self, coil) -> Optional[bool]: """Read coil using the Modbus hub slave.""" - try: - result = self._hub.read_coils(self._slave, coil, 1) - except ConnectionException: - self._set_unavailable() + result = await self._hub.read_coils(self._slave, coil, 1) + if result is None: + self._available = False return - if isinstance(result, (ModbusException, ExceptionResponse)): - self._set_unavailable() + self._available = False return value = bool(result.bits[0]) @@ -172,29 +173,11 @@ class ModbusCoilSwitch(ToggleEntity, RestoreEntity): return value - def _write_coil(self, coil, value): + async def _write_coil(self, coil, value): """Write coil using the Modbus hub slave.""" - try: - self._hub.write_coil(self._slave, coil, value) - except ConnectionException: - self._set_unavailable() - return - + await self._hub.write_coil(self._slave, coil, value) self._available = True - def _set_unavailable(self): - """Set unavailable state and log it as an error.""" - if not self._available: - return - - _LOGGER.error( - "No response from hub %s, slave %s, coil %s", - self._hub.name, - self._slave, - self._coil, - ) - self._available = False - class ModbusRegisterSwitch(ModbusCoilSwitch): """Representation of a Modbus register switch.""" @@ -238,21 +221,21 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): self._is_on = None - def turn_on(self, **kwargs): + async def turn_on(self, **kwargs): """Set switch on.""" # Only holding register is writable - if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING: - self._write_register(self._command_on) + if self._register_type == CALL_TYPE_REGISTER_HOLDING: + await self._write_register(self._command_on) if not self._verify_state: self._is_on = True - def turn_off(self, **kwargs): + async def turn_off(self, **kwargs): """Set switch off.""" # Only holding register is writable - if self._register_type == DEFAULT_REGISTER_TYPE_HOLDING: - self._write_register(self._command_off) + if self._register_type == CALL_TYPE_REGISTER_HOLDING: + await self._write_register(self._command_off) if not self._verify_state: self._is_on = False @@ -261,12 +244,12 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): """Return True if entity is available.""" return self._available - def update(self): + async def async_update(self): """Update the state of the switch.""" if not self._verify_state: return - value = self._read_register() + value = await self._read_register() if value == self._state_on: self._is_on = True elif value == self._state_off: @@ -280,20 +263,20 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): value, ) - def _read_register(self) -> Optional[int]: - try: - if self._register_type == DEFAULT_REGISTER_TYPE_INPUT: - result = self._hub.read_input_registers(self._slave, self._register, 1) - else: - result = self._hub.read_holding_registers( - self._slave, self._register, 1 - ) - except ConnectionException: - self._set_unavailable() + async def _read_register(self) -> Optional[int]: + if self._register_type == CALL_TYPE_REGISTER_INPUT: + result = await self._hub.read_input_registers( + self._slave, self._register, 1 + ) + else: + result = await self._hub.read_holding_registers( + self._slave, self._register, 1 + ) + if result is None: + self._available = False return - if isinstance(result, (ModbusException, ExceptionResponse)): - self._set_unavailable() + self._available = False return value = int(result.registers[0]) @@ -301,25 +284,7 @@ class ModbusRegisterSwitch(ModbusCoilSwitch): return value - def _write_register(self, value): + async def _write_register(self, value): """Write holding register using the Modbus hub slave.""" - try: - self._hub.write_register(self._slave, self._register, value) - except ConnectionException: - self._set_unavailable() - return - + await self._hub.write_register(self._slave, self._register, value) self._available = True - - def _set_unavailable(self): - """Set unavailable state and log it as an error.""" - if not self._available: - return - - _LOGGER.error( - "No response from hub %s, slave %s, register %s", - self._hub.name, - self._slave, - self._register, - ) - self._available = False diff --git a/requirements_all.txt b/requirements_all.txt index f41479d0f6a..55ed045a709 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1399,7 +1399,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==1.5.2 +pymodbus==2.3.0 # homeassistant.components.monoprice pymonoprice==0.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e9379550163..210725ef045 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -543,7 +543,7 @@ pymfy==0.7.1 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==1.5.2 +pymodbus==2.3.0 # homeassistant.components.monoprice pymonoprice==0.3 diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index 16d8f9a1936..1c4094387a9 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -4,10 +4,12 @@ from unittest import mock import pytest -from homeassistant.components.modbus import DEFAULT_HUB, DOMAIN as MODBUS_DOMAIN -from homeassistant.components.modbus.sensor import ( +from homeassistant.components.modbus.const import ( + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, CONF_COUNT, CONF_DATA_TYPE, + CONF_OFFSET, CONF_PRECISION, CONF_REGISTER, CONF_REGISTER_TYPE, @@ -17,16 +19,10 @@ from homeassistant.components.modbus.sensor import ( DATA_TYPE_FLOAT, DATA_TYPE_INT, DATA_TYPE_UINT, - DEFAULT_REGISTER_TYPE_HOLDING, - DEFAULT_REGISTER_TYPE_INPUT, -) -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.const import ( - CONF_NAME, - CONF_OFFSET, - CONF_PLATFORM, - CONF_SCAN_INTERVAL, + DEFAULT_HUB, + MODBUS_DOMAIN, ) +from homeassistant.const import CONF_NAME, CONF_PLATFORM, CONF_SCAN_INTERVAL from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -61,7 +57,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected): sensor_name = "modbus_test_sensor" scan_interval = 5 config = { - SENSOR_DOMAIN: { + MODBUS_DOMAIN: { CONF_PLATFORM: "modbus", CONF_SCAN_INTERVAL: scan_interval, CONF_REGISTERS: [ @@ -72,7 +68,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected): # Setup inputs for the sensor read_result = ReadResult(register_words) - if register_config.get(CONF_REGISTER_TYPE) == DEFAULT_REGISTER_TYPE_INPUT: + if register_config.get(CONF_REGISTER_TYPE) == CALL_TYPE_REGISTER_INPUT: mock_hub.read_input_registers.return_value = read_result else: mock_hub.read_holding_registers.return_value = read_result @@ -80,7 +76,7 @@ async def run_test(hass, mock_hub, register_config, register_words, expected): # Initialize sensor now = dt_util.utcnow() with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): - assert await async_setup_component(hass, SENSOR_DOMAIN, config) + assert await async_setup_component(hass, MODBUS_DOMAIN, config) # Trigger update call with time_changed event now += timedelta(seconds=scan_interval + 1) @@ -88,11 +84,6 @@ async def run_test(hass, mock_hub, register_config, register_words, expected): async_fire_time_changed(hass, now) await hass.async_block_till_done() - # Check state - entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" - state = hass.states.get(entity_id).state - assert state == expected - async def test_simple_word_register(hass, mock_hub): """Test conversion of single word register.""" @@ -310,7 +301,7 @@ async def test_two_word_input_register(hass, mock_hub): """Test reaging of input register.""" register_config = { CONF_COUNT: 2, - CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_INPUT, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT, CONF_DATA_TYPE: DATA_TYPE_UINT, CONF_SCALE: 1, CONF_OFFSET: 0, @@ -329,7 +320,7 @@ async def test_two_word_holding_register(hass, mock_hub): """Test reaging of holding register.""" register_config = { CONF_COUNT: 2, - CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_DATA_TYPE: DATA_TYPE_UINT, CONF_SCALE: 1, CONF_OFFSET: 0, @@ -348,7 +339,7 @@ async def test_float_data_type(hass, mock_hub): """Test floating point register data type.""" register_config = { CONF_COUNT: 2, - CONF_REGISTER_TYPE: DEFAULT_REGISTER_TYPE_HOLDING, + CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, CONF_DATA_TYPE: DATA_TYPE_FLOAT, CONF_SCALE: 1, CONF_OFFSET: 0, From d45c386149bfee8712ce6d356fcf8dc635aecfef Mon Sep 17 00:00:00 2001 From: cgtobi Date: Sun, 29 Mar 2020 21:30:00 +0200 Subject: [PATCH 308/431] Bump opencv to 4.2.0.32 (#33391) * Bump opencv to 4.2.0.32 --- homeassistant/components/opencv/manifest.json | 7 +++++-- requirements_all.txt | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index 40ab3a8a7ed..0ba1ad6c9e3 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,7 +2,10 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.18.1", "opencv-python-headless==4.1.2.30"], + "requirements": [ + "numpy==1.18.1", + "opencv-python-headless==4.2.0.32" + ], "dependencies": [], "codeowners": [] -} +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 55ed045a709..9bc82958deb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -964,7 +964,7 @@ onkyo-eiscp==1.2.7 onvif-zeep-async==0.2.0 # homeassistant.components.opencv -# opencv-python-headless==4.1.2.30 +# opencv-python-headless==4.2.0.32 # homeassistant.components.openevse openevsewifi==0.4 From 1cd0e764b6a11d06ea2f640804493fe944986ff7 Mon Sep 17 00:00:00 2001 From: SukramJ Date: Sun, 29 Mar 2020 21:38:59 +0200 Subject: [PATCH 309/431] Use new HMIP labels for HomematicIP Cloud multi devices (#32925) * Use new labels for HomematicIP Cloud multi devices * Update homeassistant/components/homematicip_cloud/device.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Update name composition Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- .../components/homematicip_cloud/device.py | 11 ++++++-- .../components/homematicip_cloud/light.py | 11 ++++++++ .../components/homematicip_cloud/switch.py | 8 ++++++ .../homematicip_cloud/test_device.py | 28 +++++++++---------- .../homematicip_cloud/test_light.py | 10 +++---- tests/fixtures/homematicip_cloud.json | 8 +++--- 6 files changed, 51 insertions(+), 25 deletions(-) diff --git a/homeassistant/components/homematicip_cloud/device.py b/homeassistant/components/homematicip_cloud/device.py index 8e6ca1f75fe..0407a1a0fe2 100644 --- a/homeassistant/components/homematicip_cloud/device.py +++ b/homeassistant/components/homematicip_cloud/device.py @@ -166,12 +166,19 @@ class HomematicipGenericDevice(Entity): def name(self) -> str: """Return the name of the generic device.""" name = self._device.label - if self._home.name is not None and self._home.name != "": + if name and self._home.name: name = f"{self._home.name} {name}" - if self.post is not None and self.post != "": + if name and self.post: name = f"{name} {self.post}" return name + def _get_label_by_channel(self, channel: int) -> str: + """Return the name of the channel.""" + name = self._device.functionalChannels[channel].label + if name and self._home.name: + name = f"{self._home.name} {name}" + return name + @property def should_poll(self) -> bool: """No polling needed.""" diff --git a/homeassistant/components/homematicip_cloud/light.py b/homeassistant/components/homematicip_cloud/light.py index cead186db95..42c18239ac2 100644 --- a/homeassistant/components/homematicip_cloud/light.py +++ b/homeassistant/components/homematicip_cloud/light.py @@ -71,6 +71,14 @@ class HomematicipLight(HomematicipGenericDevice, Light): """Initialize the light device.""" super().__init__(hap, device) + @property + def name(self) -> str: + """Return the name of the multi switch channel.""" + label = self._get_label_by_channel(1) + if label: + return label + return super().name + @property def is_on(self) -> bool: """Return true if device is on.""" @@ -193,6 +201,9 @@ class HomematicipNotificationLight(HomematicipGenericDevice, Light): @property def name(self) -> str: """Return the name of the generic device.""" + label = self._get_label_by_channel(self.channel) + if label: + return label return f"{super().name} Notification" @property diff --git a/homeassistant/components/homematicip_cloud/switch.py b/homeassistant/components/homematicip_cloud/switch.py index 45adf54df2b..79f7b9dfa5c 100644 --- a/homeassistant/components/homematicip_cloud/switch.py +++ b/homeassistant/components/homematicip_cloud/switch.py @@ -153,6 +153,14 @@ class HomematicipMultiSwitch(HomematicipGenericDevice, SwitchDevice): self.channel = channel super().__init__(hap, device, f"Channel{channel}") + @property + def name(self) -> str: + """Return the name of the multi switch channel.""" + label = self._get_label_by_channel(self.channel) + if label: + return label + return super().name + @property def unique_id(self) -> str: """Return a unique ID.""" diff --git a/tests/components/homematicip_cloud/test_device.py b/tests/components/homematicip_cloud/test_device.py index 3cb45182399..71efac3a7c9 100644 --- a/tests/components/homematicip_cloud/test_device.py +++ b/tests/components/homematicip_cloud/test_device.py @@ -26,11 +26,11 @@ async def test_hmip_load_all_supported_devices(hass, default_mock_hap_factory): async def test_hmip_remove_device(hass, default_mock_hap_factory): """Test Remove of hmip device.""" - entity_id = "light.treppe" - entity_name = "Treppe" + entity_id = "light.treppe_ch" + entity_name = "Treppe CH" device_model = "HmIP-BSL" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=[entity_name] + test_devices=["Treppe"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -58,11 +58,11 @@ async def test_hmip_remove_device(hass, default_mock_hap_factory): async def test_hmip_add_device(hass, default_mock_hap_factory, hmip_config_entry): """Test Remove of hmip device.""" - entity_id = "light.treppe" - entity_name = "Treppe" + entity_id = "light.treppe_ch" + entity_name = "Treppe CH" device_model = "HmIP-BSL" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=[entity_name] + test_devices=["Treppe"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -137,11 +137,11 @@ async def test_all_devices_unavailable_when_hap_not_connected( hass, default_mock_hap_factory ): """Test make all devices unavaulable when hap is not connected.""" - entity_id = "light.treppe" - entity_name = "Treppe" + entity_id = "light.treppe_ch" + entity_name = "Treppe CH" device_model = "HmIP-BSL" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=[entity_name] + test_devices=["Treppe"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -161,11 +161,11 @@ async def test_all_devices_unavailable_when_hap_not_connected( async def test_hap_reconnected(hass, default_mock_hap_factory): """Test reconnect hap.""" - entity_id = "light.treppe" - entity_name = "Treppe" + entity_id = "light.treppe_ch" + entity_name = "Treppe CH" device_model = "HmIP-BSL" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=[entity_name] + test_devices=["Treppe"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -192,8 +192,8 @@ async def test_hap_reconnected(hass, default_mock_hap_factory): async def test_hap_with_name(hass, mock_connection, hmip_config_entry): """Test hap with name.""" home_name = "TestName" - entity_id = f"light.{home_name.lower()}_treppe" - entity_name = f"{home_name} Treppe" + entity_id = f"light.{home_name.lower()}_treppe_ch" + entity_name = f"{home_name} Treppe CH" device_model = "HmIP-BSL" hmip_config_entry.data = {**hmip_config_entry.data, "name": home_name} diff --git a/tests/components/homematicip_cloud/test_light.py b/tests/components/homematicip_cloud/test_light.py index 8909e469ee9..8ab62019c3d 100644 --- a/tests/components/homematicip_cloud/test_light.py +++ b/tests/components/homematicip_cloud/test_light.py @@ -27,11 +27,11 @@ async def test_manually_configured_platform(hass): async def test_hmip_light(hass, default_mock_hap_factory): """Test HomematicipLight.""" - entity_id = "light.treppe" - entity_name = "Treppe" + entity_id = "light.treppe_ch" + entity_name = "Treppe CH" device_model = "HmIP-BSL" mock_hap = await default_mock_hap_factory.async_get_mock_hap( - test_devices=[entity_name] + test_devices=["Treppe"] ) ha_state, hmip_device = get_and_check_entity_basics( @@ -66,8 +66,8 @@ async def test_hmip_light(hass, default_mock_hap_factory): async def test_hmip_notification_light(hass, default_mock_hap_factory): """Test HomematicipNotificationLight.""" - entity_id = "light.treppe_top_notification" - entity_name = "Treppe Top Notification" + entity_id = "light.alarm_status" + entity_name = "Alarm Status" device_model = "HmIP-BSL" mock_hap = await default_mock_hap_factory.async_get_mock_hap( test_devices=["Treppe"] diff --git a/tests/fixtures/homematicip_cloud.json b/tests/fixtures/homematicip_cloud.json index a97b1247e2c..e85401aa1ec 100644 --- a/tests/fixtures/homematicip_cloud.json +++ b/tests/fixtures/homematicip_cloud.json @@ -1591,7 +1591,7 @@ "groupIndex": 1, "groups": [], "index": 1, - "label": "", + "label": "Treppe CH", "on": true, "profileMode": "AUTOMATIC", "userDesiredProfileMode": "AUTOMATIC" @@ -1603,7 +1603,7 @@ "groupIndex": 2, "groups": [], "index": 2, - "label": "", + "label": "Alarm Status", "on": null, "profileMode": "AUTOMATIC", "simpleRGBColorState": "RED", @@ -4576,7 +4576,7 @@ "00000000-0000-0000-0000-000000000042" ], "index": 1, - "label": "", + "label": "SW1", "on": false, "profileMode": "AUTOMATIC", "userDesiredProfileMode": "AUTOMATIC" @@ -4590,7 +4590,7 @@ "00000000-0000-0000-0000-000000000040" ], "index": 2, - "label": "", + "label": "SW2", "on": false, "profileMode": "AUTOMATIC", "userDesiredProfileMode": "AUTOMATIC" From 68e86c5e3aa5b475f5e52b204a47bed89ab2920d Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 29 Mar 2020 15:15:12 -0500 Subject: [PATCH 310/431] Handle Ecobee service timeouts (#33381) * Bump pyecobee to 0.2.4 * Bump pyecobee to 0.2.5 --- homeassistant/components/ecobee/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ecobee/manifest.json b/homeassistant/components/ecobee/manifest.json index dd96191e4fe..d6bc3b1eaa1 100644 --- a/homeassistant/components/ecobee/manifest.json +++ b/homeassistant/components/ecobee/manifest.json @@ -4,6 +4,6 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ecobee", "dependencies": [], - "requirements": ["python-ecobee-api==0.2.3"], + "requirements": ["python-ecobee-api==0.2.5"], "codeowners": ["@marthoc"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9bc82958deb..d0a86c9b951 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1596,7 +1596,7 @@ python-clementine-remote==1.0.1 python-digitalocean==1.13.2 # homeassistant.components.ecobee -python-ecobee-api==0.2.3 +python-ecobee-api==0.2.5 # homeassistant.components.eq3btsmart # python-eq3bt==0.1.11 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 210725ef045..05e84443e5a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -602,7 +602,7 @@ pysonos==0.0.25 pyspcwebgw==0.4.0 # homeassistant.components.ecobee -python-ecobee-api==0.2.3 +python-ecobee-api==0.2.5 # homeassistant.components.darksky python-forecastio==1.4.0 From 0f9790f5f17232f14485571a74e4c5a2535f530e Mon Sep 17 00:00:00 2001 From: Anders Liljekvist Date: Sun, 29 Mar 2020 22:38:03 +0200 Subject: [PATCH 311/431] Bluesound volume stepper bugfix (#33404) --- homeassistant/components/bluesound/media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/bluesound/media_player.py b/homeassistant/components/bluesound/media_player.py index 3f7dc41ffef..e2cc0dd31e2 100644 --- a/homeassistant/components/bluesound/media_player.py +++ b/homeassistant/components/bluesound/media_player.py @@ -1021,16 +1021,16 @@ class BluesoundPlayer(MediaPlayerDevice): async def async_volume_up(self): """Volume up the media player.""" current_vol = self.volume_level - if not current_vol or current_vol < 0: + if not current_vol or current_vol >= 1: return - return self.async_set_volume_level(((current_vol * 100) + 1) / 100) + return await self.async_set_volume_level(current_vol + 0.01) async def async_volume_down(self): """Volume down the media player.""" current_vol = self.volume_level - if not current_vol or current_vol < 0: + if not current_vol or current_vol <= 0: return - return self.async_set_volume_level(((current_vol * 100) - 1) / 100) + return await self.async_set_volume_level(current_vol - 0.01) async def async_set_volume_level(self, volume): """Send volume_up command to media player.""" From ad3c5240c28bd659a015735f0b0cdbc735ab5842 Mon Sep 17 00:00:00 2001 From: Jc2k Date: Sun, 29 Mar 2020 21:38:52 +0100 Subject: [PATCH 312/431] Bump aiohomekit version to fix Ecobee Switch (#33396) --- homeassistant/components/homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 392495e34ea..009dc285150 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[IP]==0.2.35"], + "requirements": ["aiohomekit[IP]==0.2.37"], "dependencies": [], "zeroconf": ["_hap._tcp.local."], "codeowners": ["@Jc2k"] diff --git a/requirements_all.txt b/requirements_all.txt index d0a86c9b951..9360ccc9bf8 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -168,7 +168,7 @@ aioftp==0.12.0 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.35 +aiohomekit[IP]==0.2.37 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 05e84443e5a..3861268e0a7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -72,7 +72,7 @@ aiofreepybox==0.0.8 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.35 +aiohomekit[IP]==0.2.37 # homeassistant.components.emulated_hue # homeassistant.components.http From de546590975ea53d44b28ca61833fdb5bda3ebee Mon Sep 17 00:00:00 2001 From: Malte Franken Date: Mon, 30 Mar 2020 07:40:09 +1100 Subject: [PATCH 313/431] Fix GeoNet NZ Quakes tests (#33383) * enable tests * only remove entity if exists --- homeassistant/components/geonetnz_quakes/geo_location.py | 3 ++- tests/ignore_uncaught_exceptions.py | 2 -- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/geonetnz_quakes/geo_location.py b/homeassistant/components/geonetnz_quakes/geo_location.py index 7d29f5ed3ec..ed0b9f9f714 100644 --- a/homeassistant/components/geonetnz_quakes/geo_location.py +++ b/homeassistant/components/geonetnz_quakes/geo_location.py @@ -96,7 +96,8 @@ class GeonetnzQuakesEvent(GeolocationEvent): self._remove_signal_update() # Remove from entity registry. entity_registry = await async_get_registry(self.hass) - entity_registry.async_remove(self.entity_id) + if self.entity_id in entity_registry.entities: + entity_registry.async_remove(self.entity_id) @callback def _delete_callback(self): diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index e6dc2f7e4f5..b0feb2bddb3 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -51,8 +51,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.dyson.test_fan", "test_purecool_update_state_filter_inv"), ("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"), ("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"), - ("tests.components.geonetnz_quakes.test_geo_location", "test_setup"), - ("tests.components.geonetnz_quakes.test_sensor", "test_setup"), ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), ("tests.components.hue.test_bridge", "test_handle_unauthorized"), ("tests.components.hue.test_init", "test_security_vuln_check"), From ef61118d49071b2fcc64637b132debd4673e00a1 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Sun, 29 Mar 2020 17:42:37 -0400 Subject: [PATCH 314/431] fix ZHA IASWD commands (#33402) --- homeassistant/components/zha/api.py | 17 +++++++++++++---- .../components/zha/core/channels/security.py | 4 ++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index ea5586ef96f..f3b6e2eebd9 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -950,6 +950,15 @@ def async_load_api(hass): schema=SERVICE_SCHEMAS[SERVICE_ISSUE_ZIGBEE_GROUP_COMMAND], ) + def _get_ias_wd_channel(zha_device): + """Get the IASWD channel for a device.""" + cluster_channels = { + ch.name: ch + for pool in zha_device.channels.pools + for ch in pool.claimed_channels.values() + } + return cluster_channels.get(CHANNEL_IAS_WD) + async def warning_device_squawk(service): """Issue the squawk command for an IAS warning device.""" ieee = service.data[ATTR_IEEE] @@ -959,9 +968,9 @@ def async_load_api(hass): zha_device = zha_gateway.get_device(ieee) if zha_device is not None: - channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + channel = _get_ias_wd_channel(zha_device) if channel: - await channel.squawk(mode, strobe, level) + await channel.issue_squawk(mode, strobe, level) else: _LOGGER.error( "Squawking IASWD: %s: [%s] is missing the required IASWD channel!", @@ -1003,9 +1012,9 @@ def async_load_api(hass): zha_device = zha_gateway.get_device(ieee) if zha_device is not None: - channel = zha_device.cluster_channels.get(CHANNEL_IAS_WD) + channel = _get_ias_wd_channel(zha_device) if channel: - await channel.start_warning( + await channel.issue_start_warning( mode, strobe, level, duration, duty_mode, intensity ) else: diff --git a/homeassistant/components/zha/core/channels/security.py b/homeassistant/components/zha/core/channels/security.py index 2616161de03..822ae8dd911 100644 --- a/homeassistant/components/zha/core/channels/security.py +++ b/homeassistant/components/zha/core/channels/security.py @@ -51,7 +51,7 @@ class IasWd(ZigbeeChannel): """Get the specified bit from the value.""" return (value & (1 << bit)) != 0 - async def squawk( + async def issue_squawk( self, mode=WARNING_DEVICE_SQUAWK_MODE_ARMED, strobe=WARNING_DEVICE_STROBE_YES, @@ -76,7 +76,7 @@ class IasWd(ZigbeeChannel): await self.squawk(value) - async def start_warning( + async def issue_start_warning( self, mode=WARNING_DEVICE_MODE_EMERGENCY, strobe=WARNING_DEVICE_STROBE_YES, From fe0db80fb8d09b5cf561259e979fe62c03863df3 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Sun, 29 Mar 2020 16:28:37 -0600 Subject: [PATCH 315/431] Bump aioambient to 1.1.0 (#33414) --- .../components/ambient_station/__init__.py | 20 ------------------- .../components/ambient_station/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 3 insertions(+), 23 deletions(-) diff --git a/homeassistant/components/ambient_station/__init__.py b/homeassistant/components/ambient_station/__init__.py index b840a1b7171..f3f2397d214 100644 --- a/homeassistant/components/ambient_station/__init__.py +++ b/homeassistant/components/ambient_station/__init__.py @@ -41,7 +41,6 @@ _LOGGER = logging.getLogger(__name__) DATA_CONFIG = "config" DEFAULT_SOCKET_MIN_RETRY = 15 -DEFAULT_WATCHDOG_SECONDS = 5 * 60 TYPE_24HOURRAININ = "24hourrainin" TYPE_BAROMABSIN = "baromabsin" @@ -342,7 +341,6 @@ class AmbientStation: self._config_entry = config_entry self._entry_setup_complete = False self._hass = hass - self._watchdog_listener = None self._ws_reconnect_delay = DEFAULT_SOCKET_MIN_RETRY self.client = client self.stations = {} @@ -359,21 +357,9 @@ class AmbientStation: async def ws_connect(self): """Register handlers and connect to the websocket.""" - async def _ws_reconnect(event_time): - """Forcibly disconnect from and reconnect to the websocket.""" - _LOGGER.debug("Watchdog expired; forcing socket reconnection") - await self.client.websocket.disconnect() - await self._attempt_connect() - def on_connect(): """Define a handler to fire when the websocket is connected.""" _LOGGER.info("Connected to websocket") - _LOGGER.debug("Watchdog starting") - if self._watchdog_listener is not None: - self._watchdog_listener() - self._watchdog_listener = async_call_later( - self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect - ) def on_data(data): """Define a handler to fire when the data is received.""" @@ -385,12 +371,6 @@ class AmbientStation: self._hass, f"ambient_station_data_update_{mac_address}" ) - _LOGGER.debug("Resetting watchdog") - self._watchdog_listener() - self._watchdog_listener = async_call_later( - self._hass, DEFAULT_WATCHDOG_SECONDS, _ws_reconnect - ) - def on_disconnect(): """Define a handler to fire when the websocket is disconnected.""" _LOGGER.info("Disconnected from websocket") diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index a6572070a5e..8c4bc1b3cc0 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,7 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.0.4"], + "requirements": ["aioambient==1.1.0"], "dependencies": [], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 9360ccc9bf8..94ce70f6e4e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,7 +140,7 @@ aio_geojson_nsw_rfs_incidents==0.3 aio_georss_gdacs==0.3 # homeassistant.components.ambient_station -aioambient==1.0.4 +aioambient==1.1.0 # homeassistant.components.asuswrt aioasuswrt==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3861268e0a7..f8ff29221ac 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -47,7 +47,7 @@ aio_geojson_nsw_rfs_incidents==0.3 aio_georss_gdacs==0.3 # homeassistant.components.ambient_station -aioambient==1.0.4 +aioambient==1.1.0 # homeassistant.components.asuswrt aioasuswrt==1.2.3 From ad8cf2d0d046edf19aea857ded358c1050c77781 Mon Sep 17 00:00:00 2001 From: jan iversen Date: Mon, 30 Mar 2020 01:35:58 +0200 Subject: [PATCH 316/431] Remove Modbus legacy code (#33407) * Modbus: remove legacy code flexit and stiebel_el are updated to import modbus.const, therefore legacy code can be removed. * Flexit: update const reference from modbus climate.py imports from modbus direct, this is legacy and will result in errors. update import to be modbus.const. * Stiebel_el: update reference to modbus __init.py references modbus directly, this is legacy and will give errors. Update import to be modbus.const. --- homeassistant/components/flexit/climate.py | 6 +----- homeassistant/components/modbus/__init__.py | 8 ++------ homeassistant/components/stiebel_eltron/__init__.py | 6 +----- 3 files changed, 4 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/flexit/climate.py b/homeassistant/components/flexit/climate.py index 8720f67f396..fb031359693 100644 --- a/homeassistant/components/flexit/climate.py +++ b/homeassistant/components/flexit/climate.py @@ -11,11 +11,7 @@ from homeassistant.components.climate.const import ( SUPPORT_FAN_MODE, SUPPORT_TARGET_TEMPERATURE, ) -from homeassistant.components.modbus import ( - CONF_HUB, - DEFAULT_HUB, - DOMAIN as MODBUS_DOMAIN, -) +from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN from homeassistant.const import ( ATTR_TEMPERATURE, CONF_NAME, diff --git a/homeassistant/components/modbus/__init__.py b/homeassistant/components/modbus/__init__.py index 9b055155306..869d9f7ac67 100644 --- a/homeassistant/components/modbus/__init__.py +++ b/homeassistant/components/modbus/__init__.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) import homeassistant.helpers.config_validation as cv -from .const import ( # DEFAULT_HUB, +from .const import ( ATTR_ADDRESS, ATTR_HUB, ATTR_UNIT, @@ -34,16 +34,12 @@ from .const import ( # DEFAULT_HUB, CONF_BYTESIZE, CONF_PARITY, CONF_STOPBITS, + DEFAULT_HUB, MODBUS_DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) -# Kept for compatibility with other integrations, TO BE REMOVED -CONF_HUB = "hub" -DEFAULT_HUB = "default" -DOMAIN = MODBUS_DOMAIN - _LOGGER = logging.getLogger(__name__) BASE_SCHEMA = vol.Schema({vol.Optional(CONF_NAME, default=DEFAULT_HUB): cv.string}) diff --git a/homeassistant/components/stiebel_eltron/__init__.py b/homeassistant/components/stiebel_eltron/__init__.py index 956e629dd9d..3712b47671f 100644 --- a/homeassistant/components/stiebel_eltron/__init__.py +++ b/homeassistant/components/stiebel_eltron/__init__.py @@ -5,11 +5,7 @@ import logging from pystiebeleltron import pystiebeleltron import voluptuous as vol -from homeassistant.components.modbus import ( - CONF_HUB, - DEFAULT_HUB, - DOMAIN as MODBUS_DOMAIN, -) +from homeassistant.components.modbus.const import CONF_HUB, DEFAULT_HUB, MODBUS_DOMAIN from homeassistant.const import CONF_NAME, DEVICE_DEFAULT_NAME from homeassistant.helpers import discovery import homeassistant.helpers.config_validation as cv From d59209ff477d470fd11fa0fe7ebc25da20e3fa7d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 29 Mar 2020 20:53:25 -0500 Subject: [PATCH 317/431] Prevent nut from doing I/O in the event loop (#33420) * Prevent nut from doing I/O in the event loop device_state_attributes would call for an update if the throttle happened to expire. * _format_display_state no self use --- homeassistant/components/nut/sensor.py | 46 +++++++++++++++----------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index f97f7212e10..1b602954414 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -220,6 +220,8 @@ class NUTSensor(Entity): self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0]) self._unit = SENSOR_TYPES[sensor_type][1] self._state = None + self._display_state = None + self._available = False @property def name(self): @@ -241,38 +243,44 @@ class NUTSensor(Entity): """Return the unit of measurement of this entity, if any.""" return self._unit + @property + def available(self): + """Return if the device is polling successfully.""" + return self._available + @property def device_state_attributes(self): """Return the sensor attributes.""" - attr = dict() - attr[ATTR_STATE] = self.display_state() - return attr - - def display_state(self): - """Return UPS display state.""" - if self._data.status is None: - return STATE_TYPES["OFF"] - try: - return " ".join( - STATE_TYPES[state] for state in self._data.status[KEY_STATUS].split() - ) - except KeyError: - return STATE_UNKNOWN + return {ATTR_STATE: self._display_state} def update(self): """Get the latest status and use it to update our sensor state.""" - if self._data.status is None: - self._state = None + status = self._data.status + + if status is None: + self._available = False return + self._available = True + self._display_state = _format_display_state(status) # In case of the display status sensor, keep a human-readable form # as the sensor state. if self.type == KEY_STATUS_DISPLAY: - self._state = self.display_state() - elif self.type not in self._data.status: + self._state = self._display_state + elif self.type not in status: self._state = None else: - self._state = self._data.status[self.type] + self._state = status[self.type] + + +def _format_display_state(status): + """Return UPS display state.""" + if status is None: + return STATE_TYPES["OFF"] + try: + return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split()) + except KeyError: + return STATE_UNKNOWN class PyNUTData: From 0e6b905cdf8a28d295c7085b7f05bcc49349675b Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Sun, 29 Mar 2020 23:05:59 -0400 Subject: [PATCH 318/431] Add konnected multi output (#33412) * add test to for importing multiple output settings * provide option to set multiple output states * tweaks after testing * Update homeassistant/components/konnected/config_flow.py Co-Authored-By: Martin Hjelmare Co-authored-by: Martin Hjelmare --- .../konnected/.translations/en.json | 4 +- .../components/konnected/config_flow.py | 53 ++++++++++++++--- .../components/konnected/strings.json | 5 +- .../components/konnected/test_config_flow.py | 58 ++++++++++++++++++- tests/components/konnected/test_init.py | 38 +++++++++++- 5 files changed, 143 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json index fd0a8e84e37..bc86a5ca549 100644 --- a/homeassistant/components/konnected/.translations/en.json +++ b/homeassistant/components/konnected/.translations/en.json @@ -33,6 +33,7 @@ "abort": { "not_konn_panel": "Not a recognized Konnected.io device" }, + "error": {}, "step": { "options_binary": { "data": { @@ -91,11 +92,12 @@ "data": { "activation": "Output when on", "momentary": "Pulse duration (ms) (optional)", + "more_states": "Configure additional states for this zone", "name": "Name (optional)", "pause": "Pause between pulses (ms) (optional)", "repeat": "Times to repeat (-1=infinite) (optional)" }, - "description": "Please select the output options for {zone}", + "description": "Please select the output options for {zone}: state {state}", "title": "Configure Switchable Output" } }, diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index cb9004c9efe..172f60cd42d 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -57,6 +57,10 @@ CONF_IO_BIN = "Binary Sensor" CONF_IO_DIG = "Digital Sensor" CONF_IO_SWI = "Switchable Output" +CONF_MORE_STATES = "more_states" +CONF_YES = "Yes" +CONF_NO = "No" + KONN_MANUFACTURER = "konnected.io" KONN_PANEL_MODEL_NAMES = { KONN_MODEL: "Konnected Alarm Panel", @@ -117,7 +121,7 @@ SWITCH_SCHEMA = vol.Schema( vol.Required(CONF_ZONE): vol.In(ZONES), vol.Optional(CONF_NAME): cv.string, vol.Optional(CONF_ACTIVATION, default=STATE_HIGH): vol.All( - vol.Lower, vol.Any(STATE_HIGH, STATE_LOW) + vol.Lower, vol.In([STATE_HIGH, STATE_LOW]) ), vol.Optional(CONF_MOMENTARY): vol.All(vol.Coerce(int), vol.Range(min=10)), vol.Optional(CONF_PAUSE): vol.All(vol.Coerce(int), vol.Range(min=10)), @@ -361,6 +365,8 @@ class OptionsFlowHandler(config_entries.OptionsFlow): self.new_opt = {CONF_IO: {}} self.active_cfg = None self.io_cfg = {} + self.current_states = [] + self.current_state = 1 @callback def get_current_cfg(self, io_type, zone): @@ -666,12 +672,21 @@ class OptionsFlowHandler(config_entries.OptionsFlow): if user_input is not None: zone = {"zone": self.active_cfg} zone.update(user_input) + del zone[CONF_MORE_STATES] self.new_opt[CONF_SWITCHES] = self.new_opt.get(CONF_SWITCHES, []) + [zone] - self.io_cfg.pop(self.active_cfg) - self.active_cfg = None + + # iterate through multiple switch states + if self.current_states: + self.current_states.pop(0) + + # only go to next zone if all states are entered + self.current_state += 1 + if user_input[CONF_MORE_STATES] == CONF_NO: + self.io_cfg.pop(self.active_cfg) + self.active_cfg = None if self.active_cfg: - current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg) + current_cfg = next(iter(self.current_states), {}) return self.async_show_form( step_id="options_switch", data_schema=vol.Schema( @@ -682,7 +697,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Optional( CONF_ACTIVATION, default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH), - ): vol.All(vol.Lower, vol.Any(STATE_HIGH, STATE_LOW)), + ): vol.All(vol.Lower, vol.In([STATE_HIGH, STATE_LOW])), vol.Optional( CONF_MOMENTARY, default=current_cfg.get(CONF_MOMENTARY, vol.UNDEFINED), @@ -695,12 +710,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_REPEAT, default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED), ): vol.All(vol.Coerce(int), vol.Range(min=-1)), + vol.Required( + CONF_MORE_STATES, + default=CONF_YES + if len(self.current_states) > 1 + else CONF_NO, + ): vol.In([CONF_YES, CONF_NO]), } ), description_placeholders={ "zone": f"Zone {self.active_cfg}" if len(self.active_cfg) < 3 - else self.active_cfg.upper() + else self.active_cfg.upper(), + "state": str(self.current_state), }, errors=errors, ) @@ -709,7 +731,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): for key, value in self.io_cfg.items(): if value == CONF_IO_SWI: self.active_cfg = key - current_cfg = self.get_current_cfg(CONF_SWITCHES, self.active_cfg) + self.current_states = [ + cfg + for cfg in self.current_opt.get(CONF_SWITCHES, []) + if cfg[CONF_ZONE] == self.active_cfg + ] + current_cfg = next(iter(self.current_states), {}) + self.current_state = 1 return self.async_show_form( step_id="options_switch", data_schema=vol.Schema( @@ -720,7 +748,7 @@ class OptionsFlowHandler(config_entries.OptionsFlow): ): str, vol.Optional( CONF_ACTIVATION, - default=current_cfg.get(CONF_ACTIVATION, "high"), + default=current_cfg.get(CONF_ACTIVATION, STATE_HIGH), ): vol.In(["low", "high"]), vol.Optional( CONF_MOMENTARY, @@ -734,12 +762,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): CONF_REPEAT, default=current_cfg.get(CONF_REPEAT, vol.UNDEFINED), ): vol.All(vol.Coerce(int), vol.Range(min=-1)), + vol.Required( + CONF_MORE_STATES, + default=CONF_YES + if len(self.current_states) > 1 + else CONF_NO, + ): vol.In([CONF_YES, CONF_NO]), } ), description_placeholders={ "zone": f"Zone {self.active_cfg}" if len(self.active_cfg) < 3 - else self.active_cfg.upper() + else self.active_cfg.upper(), + "state": str(self.current_state), }, errors=errors, ) diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index 4d923238df4..f1d7ef43ddc 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -80,13 +80,14 @@ }, "options_switch": { "title": "Configure Switchable Output", - "description": "Please select the output options for {zone}", + "description": "Please select the output options for {zone}: state {state}", "data": { "name": "Name (optional)", "activation": "Output when on", "momentary": "Pulse duration (ms) (optional)", "pause": "Pause between pulses (ms) (optional)", - "repeat": "Times to repeat (-1=infinite) (optional)" + "repeat": "Times to repeat (-1=infinite) (optional)", + "more_states": "Configure additional states for this zone" } }, "options_misc": { diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 3638f40735b..35814154f47 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -403,6 +403,14 @@ async def test_import_existing_config(hass, mock_panel): "pause": 100, "repeat": 4, }, + { + "zone": 8, + "name": "alarm", + "activation": "low", + "momentary": 100, + "pause": 100, + "repeat": -1, + }, {"zone": "out1"}, {"zone": "alarm1"}, ], @@ -463,6 +471,14 @@ async def test_import_existing_config(hass, mock_panel): "pause": 100, "repeat": 4, }, + { + "zone": "8", + "name": "alarm", + "activation": "low", + "momentary": 100, + "pause": 100, + "repeat": -1, + }, {"activation": "high", "zone": "out1"}, {"activation": "high", "zone": "alarm1"}, ], @@ -713,6 +729,7 @@ async def test_option_flow(hass, mock_panel): assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "Zone 4", + "state": "1", } # zone 4 @@ -723,6 +740,7 @@ async def test_option_flow(hass, mock_panel): assert result["step_id"] == "options_switch" assert result["description_placeholders"] == { "zone": "OUT", + "state": "1", } # zone out @@ -734,6 +752,27 @@ async def test_option_flow(hass, mock_panel): "momentary": 50, "pause": 100, "repeat": 4, + "more_states": "Yes", + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "options_switch" + assert result["description_placeholders"] == { + "zone": "OUT", + "state": "2", + } + + # zone out - state 2 + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "name": "alarm", + "activation": "low", + "momentary": 100, + "pause": 100, + "repeat": -1, + "more_states": "No", }, ) @@ -768,6 +807,14 @@ async def test_option_flow(hass, mock_panel): "pause": 100, "repeat": 4, }, + { + "zone": "out", + "name": "alarm", + "activation": "low", + "momentary": 100, + "pause": 100, + "repeat": -1, + }, ], } @@ -977,6 +1024,14 @@ async def test_option_flow_import(hass, mock_panel): "pause": 100, "repeat": 4, }, + { + "zone": "3", + "name": "alarm", + "activation": "low", + "momentary": 100, + "pause": 100, + "repeat": -1, + }, ], } ) @@ -1056,8 +1111,9 @@ async def test_option_flow_import(hass, mock_panel): assert schema["momentary"] == 50 assert schema["pause"] == 100 assert schema["repeat"] == 4 + assert schema["more_states"] == "Yes" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"activation": "high"} + result["flow_id"], user_input={"activation": "high", "more_states": "No"} ) assert result["type"] == "form" assert result["step_id"] == "options_misc" diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index e410aa9d60a..a678716bc03 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -124,7 +124,7 @@ async def test_config_schema(hass): } } - # check pin to zone + # check pin to zone and multiple output config = { konnected.DOMAIN: { konnected.CONF_ACCESS_TOKEN: "abcdefgh", @@ -135,6 +135,22 @@ async def test_config_schema(hass): {"pin": 2, "type": "door"}, {"zone": 1, "type": "door"}, ], + "switches": [ + { + "zone": 3, + "name": "Beep Beep", + "momentary": 65, + "pause": 55, + "repeat": 4, + }, + { + "zone": 3, + "name": "Warning", + "momentary": 100, + "pause": 100, + "repeat": -1, + }, + ], } ], } @@ -153,7 +169,7 @@ async def test_config_schema(hass): "11": "Disabled", "12": "Disabled", "2": "Binary Sensor", - "3": "Disabled", + "3": "Switchable Output", "4": "Disabled", "5": "Disabled", "6": "Disabled", @@ -169,6 +185,24 @@ async def test_config_schema(hass): {"inverse": False, "type": "door", "zone": "2"}, {"inverse": False, "type": "door", "zone": "1"}, ], + "switches": [ + { + "zone": "3", + "activation": "high", + "name": "Beep Beep", + "momentary": 65, + "pause": 55, + "repeat": 4, + }, + { + "zone": "3", + "activation": "high", + "name": "Warning", + "momentary": 100, + "pause": 100, + "repeat": -1, + }, + ], }, "id": "aabbccddeeff", } From eee0a6e9f42f3b766b6720502a6fa3ed04940299 Mon Sep 17 00:00:00 2001 From: vwir <55476159+vwir@users.noreply.github.com> Date: Mon, 30 Mar 2020 09:35:50 +0200 Subject: [PATCH 319/431] Fix for nissan_leaf component getting stuck (#33425) * Fix for nissan_leaf component getting stuck * Remove dots from error messages in nissan_leaf component --- homeassistant/components/nissan_leaf/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/nissan_leaf/__init__.py b/homeassistant/components/nissan_leaf/__init__.py index fba84c936f5..57b9bdb61fa 100644 --- a/homeassistant/components/nissan_leaf/__init__.py +++ b/homeassistant/components/nissan_leaf/__init__.py @@ -380,7 +380,10 @@ class LeafDataStore: ) return server_info except CarwingsError: - _LOGGER.error("An error occurred getting battery status.") + _LOGGER.error("An error occurred getting battery status") + return None + except KeyError: + _LOGGER.error("An error occurred parsing response from server") return None async def async_get_climate(self): From f42804805c4cf8882c4bd602f71c5aeb693c6114 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 30 Mar 2020 09:06:26 -0500 Subject: [PATCH 320/431] Move Tado zone state handling into upstream python-tado library (#33195) * Tado climate state moved to python-tado * Resolve various incorrect states and add tests for known tado zone states * Write state instead of calling for an update This is a redux of pr #32564 with all of the zone state now moved into python-tado and tests added for the various states. * stale string * add missing undos to dispachers * remove unneeded hass * naming * rearrange * fix water heater, add test * fix water heater, add test * switch hvac mode when changing temp if in auto/off/smart --- CODEOWNERS | 2 +- homeassistant/components/tado/__init__.py | 40 +- homeassistant/components/tado/climate.py | 498 +++++++++--------- homeassistant/components/tado/const.py | 116 +++- homeassistant/components/tado/manifest.json | 2 +- homeassistant/components/tado/sensor.py | 224 ++++---- homeassistant/components/tado/water_heater.py | 152 +++--- requirements_test_all.txt | 3 + tests/components/tado/__init__.py | 1 + tests/components/tado/test_climate.py | 59 +++ tests/components/tado/test_sensor.py | 96 ++++ tests/components/tado/test_water_heater.py | 47 ++ tests/components/tado/util.py | 86 +++ .../tado/ac_issue_32294.heat_mode.json | 60 +++ tests/fixtures/tado/devices.json | 22 + tests/fixtures/tado/hvac_action_heat.json | 67 +++ tests/fixtures/tado/me.json | 28 + tests/fixtures/tado/smartac3.auto_mode.json | 57 ++ tests/fixtures/tado/smartac3.cool_mode.json | 67 +++ tests/fixtures/tado/smartac3.dry_mode.json | 57 ++ tests/fixtures/tado/smartac3.fan_mode.json | 57 ++ tests/fixtures/tado/smartac3.heat_mode.json | 67 +++ tests/fixtures/tado/smartac3.hvac_off.json | 55 ++ tests/fixtures/tado/smartac3.manual_off.json | 55 ++ tests/fixtures/tado/smartac3.offline.json | 71 +++ tests/fixtures/tado/smartac3.smart_mode.json | 50 ++ tests/fixtures/tado/smartac3.turning_off.json | 55 ++ .../tado/tadov2.heating.auto_mode.json | 58 ++ .../tado/tadov2.heating.manual_mode.json | 73 +++ .../tado/tadov2.heating.off_mode.json | 67 +++ .../tado/tadov2.water_heater.auto_mode.json | 33 ++ .../tado/tadov2.water_heater.heating.json | 51 ++ .../tado/tadov2.water_heater.manual_mode.json | 48 ++ .../tado/tadov2.water_heater.off_mode.json | 42 ++ .../tado/tadov2.zone_capabilities.json | 19 + tests/fixtures/tado/token.json | 8 + .../tado/water_heater_zone_capabilities.json | 17 + tests/fixtures/tado/zone_capabilities.json | 46 ++ tests/fixtures/tado/zone_state.json | 55 ++ tests/fixtures/tado/zones.json | 179 +++++++ 40 files changed, 2348 insertions(+), 442 deletions(-) create mode 100644 tests/components/tado/__init__.py create mode 100644 tests/components/tado/test_climate.py create mode 100644 tests/components/tado/test_sensor.py create mode 100644 tests/components/tado/test_water_heater.py create mode 100644 tests/components/tado/util.py create mode 100644 tests/fixtures/tado/ac_issue_32294.heat_mode.json create mode 100644 tests/fixtures/tado/devices.json create mode 100644 tests/fixtures/tado/hvac_action_heat.json create mode 100644 tests/fixtures/tado/me.json create mode 100644 tests/fixtures/tado/smartac3.auto_mode.json create mode 100644 tests/fixtures/tado/smartac3.cool_mode.json create mode 100644 tests/fixtures/tado/smartac3.dry_mode.json create mode 100644 tests/fixtures/tado/smartac3.fan_mode.json create mode 100644 tests/fixtures/tado/smartac3.heat_mode.json create mode 100644 tests/fixtures/tado/smartac3.hvac_off.json create mode 100644 tests/fixtures/tado/smartac3.manual_off.json create mode 100644 tests/fixtures/tado/smartac3.offline.json create mode 100644 tests/fixtures/tado/smartac3.smart_mode.json create mode 100644 tests/fixtures/tado/smartac3.turning_off.json create mode 100644 tests/fixtures/tado/tadov2.heating.auto_mode.json create mode 100644 tests/fixtures/tado/tadov2.heating.manual_mode.json create mode 100644 tests/fixtures/tado/tadov2.heating.off_mode.json create mode 100644 tests/fixtures/tado/tadov2.water_heater.auto_mode.json create mode 100644 tests/fixtures/tado/tadov2.water_heater.heating.json create mode 100644 tests/fixtures/tado/tadov2.water_heater.manual_mode.json create mode 100644 tests/fixtures/tado/tadov2.water_heater.off_mode.json create mode 100644 tests/fixtures/tado/tadov2.zone_capabilities.json create mode 100644 tests/fixtures/tado/token.json create mode 100644 tests/fixtures/tado/water_heater_zone_capabilities.json create mode 100644 tests/fixtures/tado/zone_capabilities.json create mode 100644 tests/fixtures/tado/zone_state.json create mode 100644 tests/fixtures/tado/zones.json diff --git a/CODEOWNERS b/CODEOWNERS index 5ea7c376329..8f335bfcc4d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -369,7 +369,7 @@ homeassistant/components/switchmate/* @danielhiversen homeassistant/components/syncthru/* @nielstron homeassistant/components/synology_srm/* @aerialls homeassistant/components/syslog/* @fabaff -homeassistant/components/tado/* @michaelarnauts +homeassistant/components/tado/* @michaelarnauts @bdraco homeassistant/components/tahoma/* @philklei homeassistant/components/tankerkoenig/* @guillempages homeassistant/components/tautulli/* @ludeeus diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index e5e3d1d409c..46dba04a77e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -1,9 +1,9 @@ """Support for the (unofficial) Tado API.""" from datetime import timedelta import logging -import urllib from PyTado.interface import Tado +from requests import RequestException import voluptuous as vol from homeassistant.components.climate.const import PRESET_AWAY, PRESET_HOME @@ -110,7 +110,7 @@ class TadoConnector: """Connect to Tado and fetch the zones.""" try: self.tado = Tado(self._username, self._password) - except (RuntimeError, urllib.error.HTTPError) as exc: + except (RuntimeError, RequestException) as exc: _LOGGER.error("Unable to connect: %s", exc) return False @@ -135,9 +135,14 @@ class TadoConnector: _LOGGER.debug("Updating %s %s", sensor_type, sensor) try: if sensor_type == "zone": - data = self.tado.getState(sensor) + data = self.tado.getZoneState(sensor) elif sensor_type == "device": - data = self.tado.getDevices()[0] + devices_data = self.tado.getDevices() + if not devices_data: + _LOGGER.info("There are no devices to setup on this tado account.") + return + + data = devices_data[0] else: _LOGGER.debug("Unknown sensor: %s", sensor_type) return @@ -174,29 +179,40 @@ class TadoConnector: def set_zone_overlay( self, - zone_id, - overlay_mode, + zone_id=None, + overlay_mode=None, temperature=None, duration=None, device_type="HEATING", mode=None, + fan_speed=None, ): """Set a zone overlay.""" _LOGGER.debug( - "Set overlay for zone %s: mode=%s, temp=%s, duration=%s, type=%s, mode=%s", + "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s, type=%s, mode=%s fan_speed=%s", zone_id, overlay_mode, temperature, duration, device_type, mode, + fan_speed, ) + try: self.tado.setZoneOverlay( - zone_id, overlay_mode, temperature, duration, device_type, "ON", mode + zone_id, + overlay_mode, + temperature, + duration, + device_type, + "ON", + mode, + fan_speed, ) - except urllib.error.HTTPError as exc: - _LOGGER.error("Could not set zone overlay: %s", exc.read()) + + except RequestException as exc: + _LOGGER.error("Could not set zone overlay: %s", exc) self.update_sensor("zone", zone_id) @@ -206,7 +222,7 @@ class TadoConnector: self.tado.setZoneOverlay( zone_id, overlay_mode, None, None, device_type, "OFF" ) - except urllib.error.HTTPError as exc: - _LOGGER.error("Could not set zone overlay: %s", exc.read()) + except RequestException as exc: + _LOGGER.error("Could not set zone overlay: %s", exc) self.update_sensor("zone", zone_id) diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index e202cc49da4..224960ea3eb 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -3,21 +3,13 @@ import logging from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( - CURRENT_HVAC_COOL, - CURRENT_HVAC_HEAT, - CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, - FAN_HIGH, - FAN_LOW, - FAN_MIDDLE, - FAN_OFF, - HVAC_MODE_AUTO, - HVAC_MODE_COOL, + FAN_AUTO, HVAC_MODE_HEAT, - HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, PRESET_AWAY, PRESET_HOME, + SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, ) @@ -27,49 +19,30 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( + CONST_FAN_AUTO, + CONST_FAN_OFF, + CONST_MODE_AUTO, + CONST_MODE_COOL, + CONST_MODE_HEAT, CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, CONST_OVERLAY_TADO_MODE, - CONST_OVERLAY_TIMER, DATA, + HA_TO_TADO_FAN_MODE_MAP, + HA_TO_TADO_HVAC_MODE_MAP, + ORDERED_KNOWN_TADO_MODES, + SUPPORT_PRESET, + TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, + TADO_MODES_WITH_NO_TEMP_SETTING, + TADO_TO_HA_FAN_MODE_MAP, + TADO_TO_HA_HVAC_MODE_MAP, TYPE_AIR_CONDITIONING, TYPE_HEATING, ) _LOGGER = logging.getLogger(__name__) -FAN_MAP_TADO = {"HIGH": FAN_HIGH, "MIDDLE": FAN_MIDDLE, "LOW": FAN_LOW} - -HVAC_MAP_TADO_HEAT = { - CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT, - CONST_OVERLAY_TIMER: HVAC_MODE_HEAT, - CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT, - CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, - CONST_MODE_OFF: HVAC_MODE_OFF, -} -HVAC_MAP_TADO_COOL = { - CONST_OVERLAY_MANUAL: HVAC_MODE_COOL, - CONST_OVERLAY_TIMER: HVAC_MODE_COOL, - CONST_OVERLAY_TADO_MODE: HVAC_MODE_COOL, - CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, - CONST_MODE_OFF: HVAC_MODE_OFF, -} -HVAC_MAP_TADO_HEAT_COOL = { - CONST_OVERLAY_MANUAL: HVAC_MODE_HEAT_COOL, - CONST_OVERLAY_TIMER: HVAC_MODE_HEAT_COOL, - CONST_OVERLAY_TADO_MODE: HVAC_MODE_HEAT_COOL, - CONST_MODE_SMART_SCHEDULE: HVAC_MODE_AUTO, - CONST_MODE_OFF: HVAC_MODE_OFF, -} - -SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE -SUPPORT_HVAC_HEAT = [HVAC_MODE_HEAT, HVAC_MODE_AUTO, HVAC_MODE_OFF] -SUPPORT_HVAC_COOL = [HVAC_MODE_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF] -SUPPORT_HVAC_HEAT_COOL = [HVAC_MODE_HEAT_COOL, HVAC_MODE_AUTO, HVAC_MODE_OFF] -SUPPORT_FAN = [FAN_HIGH, FAN_MIDDLE, FAN_LOW, FAN_OFF] -SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] - def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Tado climate platform.""" @@ -96,29 +69,80 @@ def create_climate_entity(tado, name: str, zone_id: int): _LOGGER.debug("Capabilities for zone %s: %s", zone_id, capabilities) zone_type = capabilities["type"] + support_flags = SUPPORT_PRESET_MODE | SUPPORT_TARGET_TEMPERATURE + supported_hvac_modes = [ + TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_OFF], + TADO_TO_HA_HVAC_MODE_MAP[CONST_MODE_SMART_SCHEDULE], + ] + supported_fan_modes = None + heat_temperatures = None + cool_temperatures = None - ac_support_heat = False if zone_type == TYPE_AIR_CONDITIONING: - # Only use heat if available - # (you don't have to setup a heat mode, but cool is required) # Heat is preferred as it generally has a lower minimum temperature - if "HEAT" in capabilities: - temperatures = capabilities["HEAT"]["temperatures"] - ac_support_heat = True - else: - temperatures = capabilities["COOL"]["temperatures"] - elif "temperatures" in capabilities: - temperatures = capabilities["temperatures"] + for mode in ORDERED_KNOWN_TADO_MODES: + if mode not in capabilities: + continue + + supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode]) + if not capabilities[mode].get("fanSpeeds"): + continue + + support_flags |= SUPPORT_FAN_MODE + + if supported_fan_modes: + continue + + supported_fan_modes = [ + TADO_TO_HA_FAN_MODE_MAP[speed] + for speed in capabilities[mode]["fanSpeeds"] + ] + + cool_temperatures = capabilities[CONST_MODE_COOL]["temperatures"] else: - _LOGGER.debug("Not adding zone %s since it has no temperature", name) + supported_hvac_modes.append(HVAC_MODE_HEAT) + + if CONST_MODE_HEAT in capabilities: + heat_temperatures = capabilities[CONST_MODE_HEAT]["temperatures"] + + if heat_temperatures is None and "temperatures" in capabilities: + heat_temperatures = capabilities["temperatures"] + + if cool_temperatures is None and heat_temperatures is None: + _LOGGER.debug("Not adding zone %s since it has no temperatures", name) return None - min_temp = float(temperatures["celsius"]["min"]) - max_temp = float(temperatures["celsius"]["max"]) - step = temperatures["celsius"].get("step", PRECISION_TENTHS) + heat_min_temp = None + heat_max_temp = None + heat_step = None + cool_min_temp = None + cool_max_temp = None + cool_step = None + + if heat_temperatures is not None: + heat_min_temp = float(heat_temperatures["celsius"]["min"]) + heat_max_temp = float(heat_temperatures["celsius"]["max"]) + heat_step = heat_temperatures["celsius"].get("step", PRECISION_TENTHS) + + if cool_temperatures is not None: + cool_min_temp = float(cool_temperatures["celsius"]["min"]) + cool_max_temp = float(cool_temperatures["celsius"]["max"]) + cool_step = cool_temperatures["celsius"].get("step", PRECISION_TENTHS) entity = TadoClimate( - tado, name, zone_id, zone_type, min_temp, max_temp, step, ac_support_heat, + tado, + name, + zone_id, + zone_type, + heat_min_temp, + heat_max_temp, + heat_step, + cool_min_temp, + cool_max_temp, + cool_step, + supported_hvac_modes, + supported_fan_modes, + support_flags, ) return entity @@ -132,10 +156,15 @@ class TadoClimate(ClimateDevice): zone_name, zone_id, zone_type, - min_temp, - max_temp, - step, - ac_support_heat, + heat_min_temp, + heat_max_temp, + heat_step, + cool_min_temp, + cool_max_temp, + cool_step, + supported_hvac_modes, + supported_fan_modes, + support_flags, ): """Initialize of Tado climate entity.""" self._tado = tado @@ -146,49 +175,51 @@ class TadoClimate(ClimateDevice): self._unique_id = f"{zone_type} {zone_id} {tado.device_id}" self._ac_device = zone_type == TYPE_AIR_CONDITIONING - self._ac_support_heat = ac_support_heat - self._cooling = False + self._supported_hvac_modes = supported_hvac_modes + self._supported_fan_modes = supported_fan_modes + self._support_flags = support_flags - self._active = False - self._device_is_active = False + self._available = False self._cur_temp = None self._cur_humidity = None - self._is_away = False - self._min_temp = min_temp - self._max_temp = max_temp - self._step = step + + self._heat_min_temp = heat_min_temp + self._heat_max_temp = heat_max_temp + self._heat_step = heat_step + + self._cool_min_temp = cool_min_temp + self._cool_max_temp = cool_max_temp + self._cool_step = cool_step + self._target_temp = None - if tado.fallback: - # Fallback to Smart Schedule at next Schedule switch - self._default_overlay = CONST_OVERLAY_TADO_MODE - else: - # Don't fallback to Smart Schedule, but keep in manual mode - self._default_overlay = CONST_OVERLAY_MANUAL + self._current_tado_fan_speed = CONST_FAN_OFF + self._current_tado_hvac_mode = CONST_MODE_OFF + self._current_tado_hvac_action = CURRENT_HVAC_OFF - self._current_fan = CONST_MODE_OFF - self._current_operation = CONST_MODE_SMART_SCHEDULE - self._overlay_mode = CONST_MODE_SMART_SCHEDULE + self._undo_dispatcher = None + self._tado_zone_data = None + self._async_update_zone_data() + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + if self._undo_dispatcher: + self._undo_dispatcher() async def async_added_to_hass(self): """Register for sensor updates.""" - @callback - def async_update_callback(): - """Schedule an entity update.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), - async_update_callback, + self._async_update_callback, ) @property def supported_features(self): """Return the list of supported features.""" - return SUPPORT_FLAGS + return self._support_flags @property def name(self): @@ -208,12 +239,12 @@ class TadoClimate(ClimateDevice): @property def current_humidity(self): """Return the current humidity.""" - return self._cur_humidity + return self._tado_zone_data.current_humidity @property def current_temperature(self): """Return the sensor temperature.""" - return self._cur_temp + return self._tado_zone_data.current_temp @property def hvac_mode(self): @@ -221,11 +252,7 @@ class TadoClimate(ClimateDevice): Need to be one of HVAC_MODE_*. """ - if self._ac_device and self._ac_support_heat: - return HVAC_MAP_TADO_HEAT_COOL.get(self._current_operation) - if self._ac_device and not self._ac_support_heat: - return HVAC_MAP_TADO_COOL.get(self._current_operation) - return HVAC_MAP_TADO_HEAT.get(self._current_operation) + return TADO_TO_HA_HVAC_MODE_MAP.get(self._current_tado_hvac_mode, HVAC_MODE_OFF) @property def hvac_modes(self): @@ -233,11 +260,7 @@ class TadoClimate(ClimateDevice): Need to be a subset of HVAC_MODES. """ - if self._ac_device: - if self._ac_support_heat: - return SUPPORT_HVAC_HEAT_COOL - return SUPPORT_HVAC_COOL - return SUPPORT_HVAC_HEAT + return self._supported_hvac_modes @property def hvac_action(self): @@ -245,40 +268,30 @@ class TadoClimate(ClimateDevice): Need to be one of CURRENT_HVAC_*. """ - if not self._device_is_active: - return CURRENT_HVAC_OFF - if self._ac_device: - if self._active: - if self._ac_support_heat and not self._cooling: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_COOL - return CURRENT_HVAC_IDLE - if self._active: - return CURRENT_HVAC_HEAT - return CURRENT_HVAC_IDLE + return TADO_HVAC_ACTION_TO_HA_HVAC_ACTION.get( + self._tado_zone_data.current_hvac_action, CURRENT_HVAC_OFF + ) @property def fan_mode(self): """Return the fan setting.""" if self._ac_device: - return FAN_MAP_TADO.get(self._current_fan) + return TADO_TO_HA_FAN_MODE_MAP.get(self._current_tado_fan_speed, FAN_AUTO) return None @property def fan_modes(self): """List of available fan modes.""" - if self._ac_device: - return SUPPORT_FAN - return None + return self._supported_fan_modes def set_fan_mode(self, fan_mode: str): """Turn fan on/off.""" - pass + self._control_hvac(fan_mode=HA_TO_TADO_FAN_MODE_MAP[fan_mode]) @property def preset_mode(self): """Return the current preset mode (home, away).""" - if self._is_away: + if self._tado_zone_data.is_away: return PRESET_AWAY return PRESET_HOME @@ -299,12 +312,18 @@ class TadoClimate(ClimateDevice): @property def target_temperature_step(self): """Return the supported step of target temperature.""" - return self._step + if self._tado_zone_data.current_hvac_mode == CONST_MODE_COOL: + return self._cool_step or self._heat_step + return self._heat_step or self._cool_step @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temp + # If the target temperature will be None + # if the device is performing an action + # that does not affect the temperature or + # the device is switching states + return self._tado_zone_data.target_temp or self._tado_zone_data.current_temp def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -312,174 +331,149 @@ class TadoClimate(ClimateDevice): if temperature is None: return - self._current_operation = self._default_overlay - self._overlay_mode = None - self._target_temp = temperature - self._control_heating() + if self._current_tado_hvac_mode not in ( + CONST_MODE_OFF, + CONST_MODE_AUTO, + CONST_MODE_SMART_SCHEDULE, + ): + self._control_hvac(target_temp=temperature) + return + + new_hvac_mode = CONST_MODE_COOL if self._ac_device else CONST_MODE_HEAT + self._control_hvac(target_temp=temperature, hvac_mode=new_hvac_mode) def set_hvac_mode(self, hvac_mode): """Set new target hvac mode.""" - mode = None - if hvac_mode == HVAC_MODE_OFF: - mode = CONST_MODE_OFF - elif hvac_mode == HVAC_MODE_AUTO: - mode = CONST_MODE_SMART_SCHEDULE - elif hvac_mode == HVAC_MODE_HEAT: - mode = self._default_overlay - elif hvac_mode == HVAC_MODE_COOL: - mode = self._default_overlay - elif hvac_mode == HVAC_MODE_HEAT_COOL: - mode = self._default_overlay + self._control_hvac(hvac_mode=HA_TO_TADO_HVAC_MODE_MAP[hvac_mode]) - self._current_operation = mode - self._overlay_mode = None - - # Set a target temperature if we don't have any - # This can happen when we switch from Off to On - if self._target_temp is None: - if self._ac_device: - self._target_temp = self.max_temp - else: - self._target_temp = self.min_temp - self.schedule_update_ha_state() - - self._control_heating() + @property + def available(self): + """Return if the device is available.""" + return self._tado_zone_data.available @property def min_temp(self): """Return the minimum temperature.""" - return self._min_temp + if ( + self._current_tado_hvac_mode == CONST_MODE_COOL + and self._cool_min_temp is not None + ): + return self._cool_min_temp + if self._heat_min_temp is not None: + return self._heat_min_temp + + return self._cool_min_temp @property def max_temp(self): """Return the maximum temperature.""" - return self._max_temp - - def update(self): - """Handle update callbacks.""" - _LOGGER.debug("Updating climate platform for zone %d", self.zone_id) - data = self._tado.data["zone"][self.zone_id] - - if "sensorDataPoints" in data: - sensor_data = data["sensorDataPoints"] - - if "insideTemperature" in sensor_data: - temperature = float(sensor_data["insideTemperature"]["celsius"]) - self._cur_temp = temperature - - if "humidity" in sensor_data: - humidity = float(sensor_data["humidity"]["percentage"]) - self._cur_humidity = humidity - - # temperature setting will not exist when device is off if ( - "temperature" in data["setting"] - and data["setting"]["temperature"] is not None + self._current_tado_hvac_mode == CONST_MODE_HEAT + and self._heat_max_temp is not None ): - setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = setting + return self._heat_max_temp + if self._heat_max_temp is not None: + return self._heat_max_temp - if "tadoMode" in data: - mode = data["tadoMode"] - self._is_away = mode == "AWAY" + return self._heat_max_temp - if "setting" in data: - power = data["setting"]["power"] - if power == "OFF": - self._current_operation = CONST_MODE_OFF - self._current_fan = CONST_MODE_OFF - # There is no overlay, the mode will always be - # "SMART_SCHEDULE" - self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._device_is_active = False - else: - self._device_is_active = True + @callback + def _async_update_zone_data(self): + """Load tado data into zone.""" + self._tado_zone_data = self._tado.data["zone"][self.zone_id] + self._current_tado_fan_speed = self._tado_zone_data.current_fan_speed + self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode + self._current_tado_hvac_action = self._tado_zone_data.current_hvac_action - active = False - if "activityDataPoints" in data: - activity_data = data["activityDataPoints"] - if self._ac_device: - if "acPower" in activity_data and activity_data["acPower"] is not None: - if not activity_data["acPower"]["value"] == "OFF": - active = True - else: - if ( - "heatingPower" in activity_data - and activity_data["heatingPower"] is not None - ): - if float(activity_data["heatingPower"]["percentage"]) > 0.0: - active = True - self._active = active + @callback + def _async_update_callback(self): + """Load tado data and update state.""" + self._async_update_zone_data() + self.async_write_ha_state() - overlay = False - overlay_data = None - termination = CONST_MODE_SMART_SCHEDULE - cooling = False - fan_speed = CONST_MODE_OFF + def _normalize_target_temp_for_hvac_mode(self): + # Set a target temperature if we don't have any + # This can happen when we switch from Off to On + if self._target_temp is None: + self._target_temp = self._tado_zone_data.current_temp + elif self._current_tado_hvac_mode == CONST_MODE_COOL: + if self._target_temp > self._cool_max_temp: + self._target_temp = self._cool_max_temp + elif self._target_temp < self._cool_min_temp: + self._target_temp = self._cool_min_temp + elif self._current_tado_hvac_mode == CONST_MODE_HEAT: + if self._target_temp > self._heat_max_temp: + self._target_temp = self._heat_max_temp + elif self._target_temp < self._heat_min_temp: + self._target_temp = self._heat_min_temp - if "overlay" in data: - overlay_data = data["overlay"] - overlay = overlay_data is not None - - if overlay: - termination = overlay_data["termination"]["type"] - setting = False - setting_data = None - - if "setting" in overlay_data: - setting_data = overlay_data["setting"] - setting = setting_data is not None - - if setting: - if "mode" in setting_data: - cooling = setting_data["mode"] == "COOL" - - if "fanSpeed" in setting_data: - fan_speed = setting_data["fanSpeed"] - - if self._device_is_active: - # If you set mode manually to off, there will be an overlay - # and a termination, but we want to see the mode "OFF" - self._overlay_mode = termination - self._current_operation = termination - - self._cooling = cooling - self._current_fan = fan_speed - - def _control_heating(self): + def _control_hvac(self, hvac_mode=None, target_temp=None, fan_mode=None): """Send new target temperature to Tado.""" - if self._current_operation == CONST_MODE_SMART_SCHEDULE: + + if hvac_mode: + self._current_tado_hvac_mode = hvac_mode + + if target_temp: + self._target_temp = target_temp + + if fan_mode: + self._current_tado_fan_speed = fan_mode + + self._normalize_target_temp_for_hvac_mode() + + # tado does not permit setting the fan speed to + # off, you must turn off the device + if ( + self._current_tado_fan_speed == CONST_FAN_OFF + and self._current_tado_hvac_mode != CONST_MODE_OFF + ): + self._current_tado_fan_speed = CONST_FAN_AUTO + + if self._current_tado_hvac_mode == CONST_MODE_OFF: + _LOGGER.debug( + "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id + ) + self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type) + return + + if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE: _LOGGER.debug( "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) self._tado.reset_zone_overlay(self.zone_id) - self._overlay_mode = self._current_operation - return - - if self._current_operation == CONST_MODE_OFF: - _LOGGER.debug( - "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id - ) - self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, self.zone_type) - self._overlay_mode = self._current_operation return _LOGGER.debug( "Switching to %s for zone %s (%d) with temperature %s °C", - self._current_operation, + self._current_tado_hvac_mode, self.zone_name, self.zone_id, self._target_temp, ) - self._tado.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - self.zone_type, - "COOL" if self._ac_device else None, + + # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled + overlay_mode = ( + CONST_OVERLAY_TADO_MODE if self._tado.fallback else CONST_OVERLAY_MANUAL + ) + + temperature_to_send = self._target_temp + if self._current_tado_hvac_mode in TADO_MODES_WITH_NO_TEMP_SETTING: + # A temperature cannot be passed with these modes + temperature_to_send = None + + self._tado.set_zone_overlay( + zone_id=self.zone_id, + overlay_mode=overlay_mode, # What to do when the period ends + temperature=temperature_to_send, + duration=None, + device_type=self.zone_type, + mode=self._current_tado_hvac_mode, + fan_speed=( + self._current_tado_fan_speed + if (self._support_flags & SUPPORT_FAN_MODE) + else None + ), # api defaults to not sending fanSpeed if not specified ) - self._overlay_mode = self._current_operation diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 8d67e3bf9f8..542437d0af0 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -1,5 +1,48 @@ """Constant values for the Tado component.""" +from PyTado.const import ( + CONST_HVAC_COOL, + CONST_HVAC_DRY, + CONST_HVAC_FAN, + CONST_HVAC_HEAT, + CONST_HVAC_HOT_WATER, + CONST_HVAC_IDLE, + CONST_HVAC_OFF, +) + +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_DRY, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, + FAN_AUTO, + FAN_HIGH, + FAN_LOW, + FAN_MEDIUM, + FAN_OFF, + HVAC_MODE_AUTO, + HVAC_MODE_COOL, + HVAC_MODE_DRY, + HVAC_MODE_FAN_ONLY, + HVAC_MODE_HEAT, + HVAC_MODE_HEAT_COOL, + HVAC_MODE_OFF, + PRESET_AWAY, + PRESET_HOME, +) + +TADO_HVAC_ACTION_TO_HA_HVAC_ACTION = { + CONST_HVAC_HEAT: CURRENT_HVAC_HEAT, + CONST_HVAC_DRY: CURRENT_HVAC_DRY, + CONST_HVAC_FAN: CURRENT_HVAC_FAN, + CONST_HVAC_COOL: CURRENT_HVAC_COOL, + CONST_HVAC_IDLE: CURRENT_HVAC_IDLE, + CONST_HVAC_OFF: CURRENT_HVAC_OFF, + CONST_HVAC_HOT_WATER: CURRENT_HVAC_HEAT, +} + # Configuration CONF_FALLBACK = "fallback" DATA = "data" @@ -10,10 +53,81 @@ TYPE_HEATING = "HEATING" TYPE_HOT_WATER = "HOT_WATER" # Base modes +CONST_MODE_OFF = "OFF" CONST_MODE_SMART_SCHEDULE = "SMART_SCHEDULE" # Use the schedule -CONST_MODE_OFF = "OFF" # Switch off heating in a zone +CONST_MODE_AUTO = "AUTO" +CONST_MODE_COOL = "COOL" +CONST_MODE_HEAT = "HEAT" +CONST_MODE_DRY = "DRY" +CONST_MODE_FAN = "FAN" + +CONST_LINK_OFFLINE = "OFFLINE" + +CONST_FAN_OFF = "OFF" +CONST_FAN_AUTO = "AUTO" +CONST_FAN_LOW = "LOW" +CONST_FAN_MIDDLE = "MIDDLE" +CONST_FAN_HIGH = "HIGH" + # When we change the temperature setting, we need an overlay mode CONST_OVERLAY_TADO_MODE = "TADO_MODE" # wait until tado changes the mode automatic CONST_OVERLAY_MANUAL = "MANUAL" # the user has change the temperature or mode manually CONST_OVERLAY_TIMER = "TIMER" # the temperature will be reset after a timespan + + +# Heat always comes first since we get the +# min and max tempatures for the zone from +# it. +# Heat is preferred as it generally has a lower minimum temperature +ORDERED_KNOWN_TADO_MODES = [ + CONST_MODE_HEAT, + CONST_MODE_COOL, + CONST_MODE_AUTO, + CONST_MODE_DRY, + CONST_MODE_FAN, +] + +TADO_MODES_TO_HA_CURRENT_HVAC_ACTION = { + CONST_MODE_HEAT: CURRENT_HVAC_HEAT, + CONST_MODE_DRY: CURRENT_HVAC_DRY, + CONST_MODE_FAN: CURRENT_HVAC_FAN, + CONST_MODE_COOL: CURRENT_HVAC_COOL, +} + +# These modes will not allow a temp to be set +TADO_MODES_WITH_NO_TEMP_SETTING = [CONST_MODE_AUTO, CONST_MODE_DRY, CONST_MODE_FAN] +# +# HVAC_MODE_HEAT_COOL is mapped to CONST_MODE_AUTO +# This lets tado decide on a temp +# +# HVAC_MODE_AUTO is mapped to CONST_MODE_SMART_SCHEDULE +# This runs the smart schedule +# +HA_TO_TADO_HVAC_MODE_MAP = { + HVAC_MODE_OFF: CONST_MODE_OFF, + HVAC_MODE_HEAT_COOL: CONST_MODE_AUTO, + HVAC_MODE_AUTO: CONST_MODE_SMART_SCHEDULE, + HVAC_MODE_HEAT: CONST_MODE_HEAT, + HVAC_MODE_COOL: CONST_MODE_COOL, + HVAC_MODE_DRY: CONST_MODE_DRY, + HVAC_MODE_FAN_ONLY: CONST_MODE_FAN, +} + +HA_TO_TADO_FAN_MODE_MAP = { + FAN_AUTO: CONST_FAN_AUTO, + FAN_OFF: CONST_FAN_OFF, + FAN_LOW: CONST_FAN_LOW, + FAN_MEDIUM: CONST_FAN_MIDDLE, + FAN_HIGH: CONST_FAN_HIGH, +} + +TADO_TO_HA_HVAC_MODE_MAP = { + value: key for key, value in HA_TO_TADO_HVAC_MODE_MAP.items() +} + +TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP.items()} + +DEFAULT_TADO_PRECISION = 0.1 + +SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index ab0be2d4346..e84072b5985 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -7,6 +7,6 @@ ], "dependencies": [], "codeowners": [ - "@michaelarnauts" + "@michaelarnauts", "@bdraco" ] } diff --git a/homeassistant/components/tado/sensor.py b/homeassistant/components/tado/sensor.py index 2cd40bee3fa..fea81dcb586 100644 --- a/homeassistant/components/tado/sensor.py +++ b/homeassistant/components/tado/sensor.py @@ -31,6 +31,7 @@ ZONE_SENSORS = { "ac", "tado mode", "overlay", + "open window", ], TYPE_HOT_WATER: ["power", "link", "tado mode", "overlay"], } @@ -46,20 +47,27 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for tado in api_list: # Create zone sensors + zones = tado.zones + devices = tado.devices + + for zone in zones: + zone_type = zone["type"] + if zone_type not in ZONE_SENSORS: + _LOGGER.warning("Unknown zone type skipped: %s", zone_type) + continue - for zone in tado.zones: entities.extend( [ - create_zone_sensor(tado, zone["name"], zone["id"], variable) - for variable in ZONE_SENSORS.get(zone["type"]) + TadoZoneSensor(tado, zone["name"], zone["id"], variable) + for variable in ZONE_SENSORS[zone_type] ] ) # Create device sensors - for home in tado.devices: + for device in devices: entities.extend( [ - create_device_sensor(tado, home["name"], home["id"], variable) + TadoDeviceSensor(tado, device["name"], device["id"], variable) for variable in DEVICE_SENSORS ] ) @@ -67,46 +75,38 @@ def setup_platform(hass, config, add_entities, discovery_info=None): add_entities(entities, True) -def create_zone_sensor(tado, name, zone_id, variable): - """Create a zone sensor.""" - return TadoSensor(tado, name, "zone", zone_id, variable) - - -def create_device_sensor(tado, name, device_id, variable): - """Create a device sensor.""" - return TadoSensor(tado, name, "device", device_id, variable) - - -class TadoSensor(Entity): +class TadoZoneSensor(Entity): """Representation of a tado Sensor.""" - def __init__(self, tado, zone_name, sensor_type, zone_id, zone_variable): + def __init__(self, tado, zone_name, zone_id, zone_variable): """Initialize of the Tado Sensor.""" self._tado = tado self.zone_name = zone_name self.zone_id = zone_id self.zone_variable = zone_variable - self.sensor_type = sensor_type self._unique_id = f"{zone_variable} {zone_id} {tado.device_id}" self._state = None self._state_attributes = None + self._tado_zone_data = None + self._undo_dispatcher = None + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + if self._undo_dispatcher: + self._undo_dispatcher() async def async_added_to_hass(self): """Register for sensor updates.""" - @callback - def async_update_callback(): - """Schedule an entity update.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, - SIGNAL_TADO_UPDATE_RECEIVED.format(self.sensor_type, self.zone_id), - async_update_callback, + SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), + self._async_update_callback, ) + self._async_update_zone_data() @property def unique_id(self): @@ -138,7 +138,7 @@ class TadoSensor(Entity): if self.zone_variable == "heating": return UNIT_PERCENTAGE if self.zone_variable == "ac": - return "" + return None @property def icon(self): @@ -149,97 +149,143 @@ class TadoSensor(Entity): return "mdi:water-percent" @property - def should_poll(self) -> bool: + def should_poll(self): """Do not poll.""" return False - def update(self): + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_zone_data() + self.async_write_ha_state() + + @callback + def _async_update_zone_data(self): """Handle update callbacks.""" try: - data = self._tado.data[self.sensor_type][self.zone_id] + self._tado_zone_data = self._tado.data["zone"][self.zone_id] except KeyError: return - unit = TEMP_CELSIUS - if self.zone_variable == "temperature": - if "sensorDataPoints" in data: - sensor_data = data["sensorDataPoints"] - temperature = float(sensor_data["insideTemperature"]["celsius"]) - - self._state = self.hass.config.units.temperature(temperature, unit) - self._state_attributes = { - "time": sensor_data["insideTemperature"]["timestamp"], - "setting": 0, # setting is used in climate device - } - - # temperature setting will not exist when device is off - if ( - "temperature" in data["setting"] - and data["setting"]["temperature"] is not None - ): - temperature = float(data["setting"]["temperature"]["celsius"]) - - self._state_attributes[ - "setting" - ] = self.hass.config.units.temperature(temperature, unit) + self._state = self.hass.config.units.temperature( + self._tado_zone_data.current_temp, TEMP_CELSIUS + ) + self._state_attributes = { + "time": self._tado_zone_data.current_temp_timestamp, + "setting": 0, # setting is used in climate device + } elif self.zone_variable == "humidity": - if "sensorDataPoints" in data: - sensor_data = data["sensorDataPoints"] - self._state = float(sensor_data["humidity"]["percentage"]) - self._state_attributes = {"time": sensor_data["humidity"]["timestamp"]} + self._state = self._tado_zone_data.current_humidity + self._state_attributes = { + "time": self._tado_zone_data.current_humidity_timestamp + } elif self.zone_variable == "power": - if "setting" in data: - self._state = data["setting"]["power"] + self._state = self._tado_zone_data.power elif self.zone_variable == "link": - if "link" in data: - self._state = data["link"]["state"] + self._state = self._tado_zone_data.link elif self.zone_variable == "heating": - if "activityDataPoints" in data: - activity_data = data["activityDataPoints"] - - if ( - "heatingPower" in activity_data - and activity_data["heatingPower"] is not None - ): - self._state = float(activity_data["heatingPower"]["percentage"]) - self._state_attributes = { - "time": activity_data["heatingPower"]["timestamp"] - } + self._state = self._tado_zone_data.heating_power_percentage + self._state_attributes = { + "time": self._tado_zone_data.heating_power_timestamp + } elif self.zone_variable == "ac": - if "activityDataPoints" in data: - activity_data = data["activityDataPoints"] - - if "acPower" in activity_data and activity_data["acPower"] is not None: - self._state = activity_data["acPower"]["value"] - self._state_attributes = { - "time": activity_data["acPower"]["timestamp"] - } + self._state = self._tado_zone_data.ac_power + self._state_attributes = {"time": self._tado_zone_data.ac_power_timestamp} elif self.zone_variable == "tado bridge status": - if "connectionState" in data: - self._state = data["connectionState"]["value"] + self._state = self._tado_zone_data.connection elif self.zone_variable == "tado mode": - if "tadoMode" in data: - self._state = data["tadoMode"] + self._state = self._tado_zone_data.tado_mode elif self.zone_variable == "overlay": - self._state = "overlay" in data and data["overlay"] is not None + self._state = self._tado_zone_data.overlay_active self._state_attributes = ( - {"termination": data["overlay"]["termination"]["type"]} - if self._state + {"termination": self._tado_zone_data.overlay_termination_type} + if self._tado_zone_data.overlay_active else {} ) elif self.zone_variable == "early start": - self._state = "preparation" in data and data["preparation"] is not None + self._state = self._tado_zone_data.preparation elif self.zone_variable == "open window": - self._state = "openWindow" in data and data["openWindow"] is not None - self._state_attributes = data["openWindow"] if self._state else {} + self._state = self._tado_zone_data.open_window + self._state_attributes = self._tado_zone_data.open_window_attr + + +class TadoDeviceSensor(Entity): + """Representation of a tado Sensor.""" + + def __init__(self, tado, device_name, device_id, device_variable): + """Initialize of the Tado Sensor.""" + self._tado = tado + + self.device_name = device_name + self.device_id = device_id + self.device_variable = device_variable + + self._unique_id = f"{device_variable} {device_id} {tado.device_id}" + + self._state = None + self._state_attributes = None + self._tado_device_data = None + self._undo_dispatcher = None + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + if self._undo_dispatcher: + self._undo_dispatcher() + + async def async_added_to_hass(self): + """Register for sensor updates.""" + + self._undo_dispatcher = async_dispatcher_connect( + self.hass, + SIGNAL_TADO_UPDATE_RECEIVED.format("device", self.device_id), + self._async_update_callback, + ) + self._async_update_device_data() + + @property + def unique_id(self): + """Return the unique id.""" + return self._unique_id + + @property + def name(self): + """Return the name of the sensor.""" + return f"{self.device_name} {self.device_variable}" + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def should_poll(self): + """Do not poll.""" + return False + + @callback + def _async_update_callback(self): + """Update and write state.""" + self._async_update_device_data() + self.async_write_ha_state() + + @callback + def _async_update_device_data(self): + """Handle update callbacks.""" + try: + data = self._tado.data["device"][self.device_id] + except KeyError: + return + + if self.device_variable == "tado bridge status": + self._state = data.get("connectionState", {}).get("value", False) diff --git a/homeassistant/components/tado/water_heater.py b/homeassistant/components/tado/water_heater.py index fc3a9ce9cf4..51ff2ede57d 100644 --- a/homeassistant/components/tado/water_heater.py +++ b/homeassistant/components/tado/water_heater.py @@ -12,6 +12,9 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect from . import DOMAIN, SIGNAL_TADO_UPDATE_RECEIVED from .const import ( + CONST_HVAC_HEAT, + CONST_MODE_AUTO, + CONST_MODE_HEAT, CONST_MODE_OFF, CONST_MODE_SMART_SCHEDULE, CONST_OVERLAY_MANUAL, @@ -33,6 +36,7 @@ WATER_HEATER_MAP_TADO = { CONST_OVERLAY_MANUAL: MODE_HEAT, CONST_OVERLAY_TIMER: MODE_HEAT, CONST_OVERLAY_TADO_MODE: MODE_HEAT, + CONST_HVAC_HEAT: MODE_HEAT, CONST_MODE_SMART_SCHEDULE: MODE_AUTO, CONST_MODE_OFF: MODE_OFF, } @@ -50,7 +54,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): for tado in api_list: for zone in tado.zones: - if zone["type"] in [TYPE_HOT_WATER]: + if zone["type"] == TYPE_HOT_WATER: entity = create_water_heater_entity(tado, zone["name"], zone["id"]) entities.append(entity) @@ -61,6 +65,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): def create_water_heater_entity(tado, name: str, zone_id: int): """Create a Tado water heater device.""" capabilities = tado.get_capabilities(zone_id) + supports_temperature_control = capabilities["canSetTemperature"] if supports_temperature_control and "temperatures" in capabilities: @@ -98,7 +103,6 @@ class TadoWaterHeater(WaterHeaterDevice): self._unique_id = f"{zone_id} {tado.device_id}" self._device_is_active = False - self._is_away = False self._supports_temperature_control = supports_temperature_control self._min_temperature = min_temp @@ -110,29 +114,25 @@ class TadoWaterHeater(WaterHeaterDevice): if self._supports_temperature_control: self._supported_features |= SUPPORT_TARGET_TEMPERATURE - if tado.fallback: - # Fallback to Smart Schedule at next Schedule switch - self._default_overlay = CONST_OVERLAY_TADO_MODE - else: - # Don't fallback to Smart Schedule, but keep in manual mode - self._default_overlay = CONST_OVERLAY_MANUAL - - self._current_operation = CONST_MODE_SMART_SCHEDULE + self._current_tado_hvac_mode = CONST_MODE_SMART_SCHEDULE self._overlay_mode = CONST_MODE_SMART_SCHEDULE + self._tado_zone_data = None + self._undo_dispatcher = None + + async def async_will_remove_from_hass(self): + """When entity will be removed from hass.""" + if self._undo_dispatcher: + self._undo_dispatcher() async def async_added_to_hass(self): """Register for sensor updates.""" - @callback - def async_update_callback(): - """Schedule an entity update.""" - self.async_schedule_update_ha_state(True) - - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_TADO_UPDATE_RECEIVED.format("zone", self.zone_id), - async_update_callback, + self._async_update_callback, ) + self._async_update_data() @property def supported_features(self): @@ -157,17 +157,17 @@ class TadoWaterHeater(WaterHeaterDevice): @property def current_operation(self): """Return current readable operation mode.""" - return WATER_HEATER_MAP_TADO.get(self._current_operation) + return WATER_HEATER_MAP_TADO.get(self._current_tado_hvac_mode) @property def target_temperature(self): """Return the temperature we try to reach.""" - return self._target_temp + return self._tado_zone_data.target_temp @property def is_away_mode_on(self): """Return true if away mode is on.""" - return self._is_away + return self._tado_zone_data.is_away @property def operation_list(self): @@ -198,16 +198,9 @@ class TadoWaterHeater(WaterHeaterDevice): elif operation_mode == MODE_AUTO: mode = CONST_MODE_SMART_SCHEDULE elif operation_mode == MODE_HEAT: - mode = self._default_overlay + mode = CONST_MODE_HEAT - self._current_operation = mode - self._overlay_mode = None - - # Set a target temperature if we don't have any - if mode == CONST_OVERLAY_TADO_MODE and self._target_temp is None: - self._target_temp = self.min_temp - - self._control_heater() + self._control_heater(hvac_mode=mode) def set_temperature(self, **kwargs): """Set new target temperature.""" @@ -215,88 +208,75 @@ class TadoWaterHeater(WaterHeaterDevice): if not self._supports_temperature_control or temperature is None: return - self._current_operation = self._default_overlay - self._overlay_mode = None - self._target_temp = temperature - self._control_heater() - - def update(self): - """Handle update callbacks.""" - _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id) - data = self._tado.data["zone"][self.zone_id] - - if "tadoMode" in data: - mode = data["tadoMode"] - self._is_away = mode == "AWAY" - - if "setting" in data: - power = data["setting"]["power"] - if power == "OFF": - self._current_operation = CONST_MODE_OFF - # There is no overlay, the mode will always be - # "SMART_SCHEDULE" - self._overlay_mode = CONST_MODE_SMART_SCHEDULE - self._device_is_active = False - else: - self._device_is_active = True - - # temperature setting will not exist when device is off - if ( - "temperature" in data["setting"] - and data["setting"]["temperature"] is not None + if self._current_tado_hvac_mode not in ( + CONST_MODE_OFF, + CONST_MODE_AUTO, + CONST_MODE_SMART_SCHEDULE, ): - setting = float(data["setting"]["temperature"]["celsius"]) - self._target_temp = setting + self._control_heater(target_temp=temperature) + return - overlay = False - overlay_data = None - termination = CONST_MODE_SMART_SCHEDULE + self._control_heater(target_temp=temperature, hvac_mode=CONST_MODE_HEAT) - if "overlay" in data: - overlay_data = data["overlay"] - overlay = overlay_data is not None + @callback + def _async_update_callback(self): + """Load tado data and update state.""" + self._async_update_data() + self.async_write_ha_state() - if overlay: - termination = overlay_data["termination"]["type"] + @callback + def _async_update_data(self): + """Load tado data.""" + _LOGGER.debug("Updating water_heater platform for zone %d", self.zone_id) + self._tado_zone_data = self._tado.data["zone"][self.zone_id] + self._current_tado_hvac_mode = self._tado_zone_data.current_hvac_mode - if self._device_is_active: - # If you set mode manually to off, there will be an overlay - # and a termination, but we want to see the mode "OFF" - self._overlay_mode = termination - self._current_operation = termination - - def _control_heater(self): + def _control_heater(self, hvac_mode=None, target_temp=None): """Send new target temperature.""" - if self._current_operation == CONST_MODE_SMART_SCHEDULE: + + if hvac_mode: + self._current_tado_hvac_mode = hvac_mode + + if target_temp: + self._target_temp = target_temp + + # Set a target temperature if we don't have any + if self._target_temp is None: + self._target_temp = self.min_temp + + if self._current_tado_hvac_mode == CONST_MODE_SMART_SCHEDULE: _LOGGER.debug( "Switching to SMART_SCHEDULE for zone %s (%d)", self.zone_name, self.zone_id, ) self._tado.reset_zone_overlay(self.zone_id) - self._overlay_mode = self._current_operation return - if self._current_operation == CONST_MODE_OFF: + if self._current_tado_hvac_mode == CONST_MODE_OFF: _LOGGER.debug( "Switching to OFF for zone %s (%d)", self.zone_name, self.zone_id ) self._tado.set_zone_off(self.zone_id, CONST_OVERLAY_MANUAL, TYPE_HOT_WATER) - self._overlay_mode = self._current_operation return + # Fallback to Smart Schedule at next Schedule switch if we have fallback enabled + overlay_mode = ( + CONST_OVERLAY_TADO_MODE if self._tado.fallback else CONST_OVERLAY_MANUAL + ) + _LOGGER.debug( "Switching to %s for zone %s (%d) with temperature %s", - self._current_operation, + self._current_tado_hvac_mode, self.zone_name, self.zone_id, self._target_temp, ) self._tado.set_zone_overlay( - self.zone_id, - self._current_operation, - self._target_temp, - None, - TYPE_HOT_WATER, + zone_id=self.zone_id, + overlay_mode=overlay_mode, + temperature=self._target_temp, + duration=None, + device_type=TYPE_HOT_WATER, ) - self._overlay_mode = self._current_operation + self._overlay_mode = self._current_tado_hvac_mode diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f8ff29221ac..0f0580f8c09 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -616,6 +616,9 @@ python-miio==0.4.8 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.tado +python-tado==0.5.0 + # homeassistant.components.twitch python-twitch-client==0.6.0 diff --git a/tests/components/tado/__init__.py b/tests/components/tado/__init__.py new file mode 100644 index 00000000000..11d199f01a1 --- /dev/null +++ b/tests/components/tado/__init__.py @@ -0,0 +1 @@ +"""Tests for the tado integration.""" diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py new file mode 100644 index 00000000000..602f4d8424f --- /dev/null +++ b/tests/components/tado/test_climate.py @@ -0,0 +1,59 @@ +"""The sensor tests for the tado platform.""" + +from .util import async_init_integration + + +async def test_air_con(hass): + """Test creation of aircon climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.air_conditioning") + assert state.state == "cool" + + expected_attributes = { + "current_humidity": 60.9, + "current_temperature": 24.8, + "fan_mode": "auto", + "fan_modes": ["auto", "high", "medium", "low"], + "friendly_name": "Air Conditioning", + "hvac_action": "cooling", + "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], + "max_temp": 31.0, + "min_temp": 16.0, + "preset_mode": "home", + "preset_modes": ["away", "home"], + "supported_features": 25, + "target_temp_step": 1, + "temperature": 17.8, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_heater(hass): + """Test creation of heater climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.baseboard_heater") + assert state.state == "heat" + + expected_attributes = { + "current_humidity": 45.2, + "current_temperature": 20.6, + "friendly_name": "Baseboard Heater", + "hvac_action": "idle", + "hvac_modes": ["off", "auto", "heat"], + "max_temp": 31.0, + "min_temp": 16.0, + "preset_mode": "home", + "preset_modes": ["away", "home"], + "supported_features": 17, + "target_temp_step": 1, + "temperature": 20.5, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/test_sensor.py b/tests/components/tado/test_sensor.py new file mode 100644 index 00000000000..2ea2c0508ee --- /dev/null +++ b/tests/components/tado/test_sensor.py @@ -0,0 +1,96 @@ +"""The sensor tests for the tado platform.""" + +from .util import async_init_integration + + +async def test_air_con_create_sensors(hass): + """Test creation of aircon sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.air_conditioning_power") + assert state.state == "ON" + + state = hass.states.get("sensor.air_conditioning_link") + assert state.state == "ONLINE" + + state = hass.states.get("sensor.air_conditioning_link") + assert state.state == "ONLINE" + + state = hass.states.get("sensor.air_conditioning_tado_mode") + assert state.state == "HOME" + + state = hass.states.get("sensor.air_conditioning_temperature") + assert state.state == "24.76" + + state = hass.states.get("sensor.air_conditioning_ac") + assert state.state == "ON" + + state = hass.states.get("sensor.air_conditioning_overlay") + assert state.state == "True" + + state = hass.states.get("sensor.air_conditioning_humidity") + assert state.state == "60.9" + + state = hass.states.get("sensor.air_conditioning_open_window") + assert state.state == "False" + + +async def test_heater_create_sensors(hass): + """Test creation of heater sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.baseboard_heater_power") + assert state.state == "ON" + + state = hass.states.get("sensor.baseboard_heater_link") + assert state.state == "ONLINE" + + state = hass.states.get("sensor.baseboard_heater_link") + assert state.state == "ONLINE" + + state = hass.states.get("sensor.baseboard_heater_tado_mode") + assert state.state == "HOME" + + state = hass.states.get("sensor.baseboard_heater_temperature") + assert state.state == "20.65" + + state = hass.states.get("sensor.baseboard_heater_early_start") + assert state.state == "False" + + state = hass.states.get("sensor.baseboard_heater_overlay") + assert state.state == "True" + + state = hass.states.get("sensor.baseboard_heater_humidity") + assert state.state == "45.2" + + state = hass.states.get("sensor.baseboard_heater_open_window") + assert state.state == "False" + + +async def test_water_heater_create_sensors(hass): + """Test creation of water heater sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.water_heater_tado_mode") + assert state.state == "HOME" + + state = hass.states.get("sensor.water_heater_link") + assert state.state == "ONLINE" + + state = hass.states.get("sensor.water_heater_overlay") + assert state.state == "False" + + state = hass.states.get("sensor.water_heater_power") + assert state.state == "ON" + + +async def test_home_create_sensors(hass): + """Test creation of home sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("sensor.home_name_tado_bridge_status") + assert state.state == "True" diff --git a/tests/components/tado/test_water_heater.py b/tests/components/tado/test_water_heater.py new file mode 100644 index 00000000000..03dfaaef0df --- /dev/null +++ b/tests/components/tado/test_water_heater.py @@ -0,0 +1,47 @@ +"""The sensor tests for the tado platform.""" + +from .util import async_init_integration + + +async def test_water_heater_create_sensors(hass): + """Test creation of water heater.""" + + await async_init_integration(hass) + + state = hass.states.get("water_heater.water_heater") + assert state.state == "auto" + + expected_attributes = { + "current_temperature": None, + "friendly_name": "Water Heater", + "max_temp": 31.0, + "min_temp": 16.0, + "operation_list": ["auto", "heat", "off"], + "operation_mode": "auto", + "supported_features": 3, + "target_temp_high": None, + "target_temp_low": None, + "temperature": 65.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + state = hass.states.get("water_heater.second_water_heater") + assert state.state == "heat" + + expected_attributes = { + "current_temperature": None, + "friendly_name": "Second Water Heater", + "max_temp": 31.0, + "min_temp": 16.0, + "operation_list": ["auto", "heat", "off"], + "operation_mode": "heat", + "supported_features": 3, + "target_temp_high": None, + "target_temp_low": None, + "temperature": 30.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py new file mode 100644 index 00000000000..7ee4c17058d --- /dev/null +++ b/tests/components/tado/util.py @@ -0,0 +1,86 @@ +"""Tests for the tado integration.""" + +import requests_mock + +from homeassistant.components.tado import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import load_fixture + + +async def async_init_integration( + hass: HomeAssistant, skip_setup: bool = False, +): + """Set up the tado integration in Home Assistant.""" + + token_fixture = "tado/token.json" + devices_fixture = "tado/devices.json" + me_fixture = "tado/me.json" + zones_fixture = "tado/zones.json" + # Water Heater 2 + zone_4_state_fixture = "tado/tadov2.water_heater.heating.json" + zone_4_capabilities_fixture = "tado/water_heater_zone_capabilities.json" + + # Smart AC + zone_3_state_fixture = "tado/smartac3.cool_mode.json" + zone_3_capabilities_fixture = "tado/zone_capabilities.json" + + # Water Heater + zone_2_state_fixture = "tado/tadov2.water_heater.auto_mode.json" + zone_2_capabilities_fixture = "tado/water_heater_zone_capabilities.json" + + zone_1_state_fixture = "tado/tadov2.heating.manual_mode.json" + zone_1_capabilities_fixture = "tado/tadov2.zone_capabilities.json" + + with requests_mock.mock() as m: + m.post("https://auth.tado.com/oauth/token", text=load_fixture(token_fixture)) + m.get( + "https://my.tado.com/api/v2/me", text=load_fixture(me_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/devices", + text=load_fixture(devices_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones", + text=load_fixture(zones_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/4/capabilities", + text=load_fixture(zone_4_capabilities_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/3/capabilities", + text=load_fixture(zone_3_capabilities_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/2/capabilities", + text=load_fixture(zone_2_capabilities_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/1/capabilities", + text=load_fixture(zone_1_capabilities_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/4/state", + text=load_fixture(zone_4_state_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/3/state", + text=load_fixture(zone_3_state_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/2/state", + text=load_fixture(zone_2_state_fixture), + ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/1/state", + text=load_fixture(zone_1_state_fixture), + ) + if not skip_setup: + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_USERNAME: "mock", CONF_PASSWORD: "mock"}} + ) + await hass.async_block_till_done() diff --git a/tests/fixtures/tado/ac_issue_32294.heat_mode.json b/tests/fixtures/tado/ac_issue_32294.heat_mode.json new file mode 100644 index 00000000000..098afd018aa --- /dev/null +++ b/tests/fixtures/tado/ac_issue_32294.heat_mode.json @@ -0,0 +1,60 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 71.28, + "timestamp": "2020-02-29T22:51:05.016Z", + "celsius": 21.82, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-02-29T22:51:05.016Z", + "percentage": 40.4, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": null, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-02-29T22:50:34.850Z", + "type": "POWER", + "value": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-01T00:00:00.000Z" + }, + "preparation": null, + "overlayType": null, + "nextScheduleChange": { + "start": "2020-03-01T00:00:00Z", + "setting": { + "type": "AIR_CONDITIONING", + "mode": "HEAT", + "power": "ON", + "temperature": { + "fahrenheit": 59.0, + "celsius": 15.0 + } + } + }, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "HEAT", + "power": "ON", + "temperature": { + "fahrenheit": 77.0, + "celsius": 25.0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/devices.json b/tests/fixtures/tado/devices.json new file mode 100644 index 00000000000..5fc43adc903 --- /dev/null +++ b/tests/fixtures/tado/devices.json @@ -0,0 +1,22 @@ +[ + { + "deviceType" : "WR02", + "currentFwVersion" : "59.4", + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "serialNo" : "WR1", + "commandTableUploadState" : "FINISHED", + "connectionState" : { + "timestamp" : "2020-03-23T18:30:07.377Z", + "value" : true + }, + "shortSerialNo" : "WR1" + } +] diff --git a/tests/fixtures/tado/hvac_action_heat.json b/tests/fixtures/tado/hvac_action_heat.json new file mode 100644 index 00000000000..9cbf1fd5f82 --- /dev/null +++ b/tests/fixtures/tado/hvac_action_heat.json @@ -0,0 +1,67 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 16.11, + "fahrenheit": 61.00 + }, + "fanSpeed": "AUTO" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 16.11, + "fahrenheit": 61.00 + }, + "fanSpeed": "AUTO" + }, + "termination": { + "type": "TADO_MODE", + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": null, + "nextTimeBlock": { + "start": "2020-03-07T04:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-06T17:38:30.302Z", + "type": "POWER", + "value": "OFF" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 21.40, + "fahrenheit": 70.52, + "timestamp": "2020-03-06T18:06:09.546Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 50.40, + "timestamp": "2020-03-06T18:06:09.546Z" + } + } +} diff --git a/tests/fixtures/tado/me.json b/tests/fixtures/tado/me.json new file mode 100644 index 00000000000..4707b3f04d4 --- /dev/null +++ b/tests/fixtures/tado/me.json @@ -0,0 +1,28 @@ +{ + "id" : "5", + "mobileDevices" : [ + { + "name" : "nick Android", + "deviceMetadata" : { + "platform" : "Android", + "locale" : "en", + "osVersion" : "10", + "model" : "OnePlus_GM1917" + }, + "settings" : { + "geoTrackingEnabled" : false + }, + "id" : 1 + } + ], + "homes" : [ + { + "name" : "home name", + "id" : 1 + } + ], + "name" : "name", + "locale" : "en_US", + "email" : "user@domain.tld", + "username" : "user@domain.tld" +} diff --git a/tests/fixtures/tado/smartac3.auto_mode.json b/tests/fixtures/tado/smartac3.auto_mode.json new file mode 100644 index 00000000000..254b409ddd9 --- /dev/null +++ b/tests/fixtures/tado/smartac3.auto_mode.json @@ -0,0 +1,57 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 76.64, + "timestamp": "2020-03-05T03:55:38.160Z", + "celsius": 24.8, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T03:55:38.160Z", + "percentage": 62.5, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null, + "type": "TADO_MODE" + }, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "AUTO", + "power": "ON" + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T03:56:38.627Z", + "type": "POWER", + "value": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "AUTO", + "power": "ON" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.cool_mode.json b/tests/fixtures/tado/smartac3.cool_mode.json new file mode 100644 index 00000000000..a7db2cc75bc --- /dev/null +++ b/tests/fixtures/tado/smartac3.cool_mode.json @@ -0,0 +1,67 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 76.57, + "timestamp": "2020-03-05T03:57:38.850Z", + "celsius": 24.76, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T03:57:38.850Z", + "percentage": 60.9, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null, + "type": "TADO_MODE" + }, + "setting": { + "fanSpeed": "AUTO", + "type": "AIR_CONDITIONING", + "mode": "COOL", + "power": "ON", + "temperature": { + "fahrenheit": 64.0, + "celsius": 17.78 + } + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T04:01:07.162Z", + "type": "POWER", + "value": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "fanSpeed": "AUTO", + "type": "AIR_CONDITIONING", + "mode": "COOL", + "power": "ON", + "temperature": { + "fahrenheit": 64.0, + "celsius": 17.78 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.dry_mode.json b/tests/fixtures/tado/smartac3.dry_mode.json new file mode 100644 index 00000000000..d04612d1105 --- /dev/null +++ b/tests/fixtures/tado/smartac3.dry_mode.json @@ -0,0 +1,57 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 77.02, + "timestamp": "2020-03-05T04:02:07.396Z", + "celsius": 25.01, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T04:02:07.396Z", + "percentage": 62.0, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null, + "type": "TADO_MODE" + }, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "DRY", + "power": "ON" + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T04:02:40.867Z", + "type": "POWER", + "value": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "DRY", + "power": "ON" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.fan_mode.json b/tests/fixtures/tado/smartac3.fan_mode.json new file mode 100644 index 00000000000..6907c31c517 --- /dev/null +++ b/tests/fixtures/tado/smartac3.fan_mode.json @@ -0,0 +1,57 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 77.02, + "timestamp": "2020-03-05T04:02:07.396Z", + "celsius": 25.01, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T04:02:07.396Z", + "percentage": 62.0, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null, + "type": "TADO_MODE" + }, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "FAN", + "power": "ON" + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T04:03:44.328Z", + "type": "POWER", + "value": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "type": "AIR_CONDITIONING", + "mode": "FAN", + "power": "ON" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.heat_mode.json b/tests/fixtures/tado/smartac3.heat_mode.json new file mode 100644 index 00000000000..06b5a350d83 --- /dev/null +++ b/tests/fixtures/tado/smartac3.heat_mode.json @@ -0,0 +1,67 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 76.57, + "timestamp": "2020-03-05T03:57:38.850Z", + "celsius": 24.76, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T03:57:38.850Z", + "percentage": 60.9, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null, + "type": "TADO_MODE" + }, + "setting": { + "fanSpeed": "AUTO", + "type": "AIR_CONDITIONING", + "mode": "HEAT", + "power": "ON", + "temperature": { + "fahrenheit": 61.0, + "celsius": 16.11 + } + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T03:59:36.390Z", + "type": "POWER", + "value": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "fanSpeed": "AUTO", + "type": "AIR_CONDITIONING", + "mode": "HEAT", + "power": "ON", + "temperature": { + "fahrenheit": 61.0, + "celsius": 16.11 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.hvac_off.json b/tests/fixtures/tado/smartac3.hvac_off.json new file mode 100644 index 00000000000..83e9d1a83d5 --- /dev/null +++ b/tests/fixtures/tado/smartac3.hvac_off.json @@ -0,0 +1,55 @@ +{ + "tadoMode": "AWAY", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 70.59, + "timestamp": "2020-03-05T01:21:44.089Z", + "celsius": 21.44, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T01:21:44.089Z", + "percentage": 48.2, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null, + "type": "MANUAL" + }, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-02-29T05:34:10.318Z", + "type": "POWER", + "value": "OFF" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T04:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + } +} diff --git a/tests/fixtures/tado/smartac3.manual_off.json b/tests/fixtures/tado/smartac3.manual_off.json new file mode 100644 index 00000000000..a9538f30dbe --- /dev/null +++ b/tests/fixtures/tado/smartac3.manual_off.json @@ -0,0 +1,55 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 77.02, + "timestamp": "2020-03-05T04:02:07.396Z", + "celsius": 25.01, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T04:02:07.396Z", + "percentage": 62.0, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null, + "type": "MANUAL" + }, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T04:05:08.804Z", + "type": "POWER", + "value": "OFF" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.offline.json b/tests/fixtures/tado/smartac3.offline.json new file mode 100644 index 00000000000..fda1e6468eb --- /dev/null +++ b/tests/fixtures/tado/smartac3.offline.json @@ -0,0 +1,71 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 77.09, + "timestamp": "2020-03-03T21:23:57.846Z", + "celsius": 25.05, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-03T21:23:57.846Z", + "percentage": 61.6, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "OFFLINE", + "reason": { + "code": "disconnectedDevice", + "title": "There is a disconnected device." + } + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": { + "termination": { + "typeSkillBasedApp": "TADO_MODE", + "projectedExpiry": null, + "type": "TADO_MODE" + }, + "setting": { + "fanSpeed": "AUTO", + "type": "AIR_CONDITIONING", + "mode": "COOL", + "power": "ON", + "temperature": { + "fahrenheit": 64.0, + "celsius": 17.78 + } + }, + "type": "MANUAL" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-02-29T18:42:26.683Z", + "type": "POWER", + "value": "OFF" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": "MANUAL", + "nextScheduleChange": null, + "setting": { + "fanSpeed": "AUTO", + "type": "AIR_CONDITIONING", + "mode": "COOL", + "power": "ON", + "temperature": { + "fahrenheit": 64.0, + "celsius": 17.78 + } + } +} diff --git a/tests/fixtures/tado/smartac3.smart_mode.json b/tests/fixtures/tado/smartac3.smart_mode.json new file mode 100644 index 00000000000..357a1a96658 --- /dev/null +++ b/tests/fixtures/tado/smartac3.smart_mode.json @@ -0,0 +1,50 @@ +{ + "tadoMode": "HOME", + "sensorDataPoints": { + "insideTemperature": { + "fahrenheit": 75.97, + "timestamp": "2020-03-05T03:50:24.769Z", + "celsius": 24.43, + "type": "TEMPERATURE", + "precision": { + "fahrenheit": 0.1, + "celsius": 0.1 + } + }, + "humidity": { + "timestamp": "2020-03-05T03:50:24.769Z", + "percentage": 60.0, + "type": "PERCENTAGE" + } + }, + "link": { + "state": "ONLINE" + }, + "openWindow": null, + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "overlay": null, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-05T03:52:22.253Z", + "type": "POWER", + "value": "OFF" + } + }, + "nextTimeBlock": { + "start": "2020-03-05T08:00:00.000Z" + }, + "preparation": null, + "overlayType": null, + "nextScheduleChange": null, + "setting": { + "fanSpeed": "MIDDLE", + "type": "AIR_CONDITIONING", + "mode": "COOL", + "power": "ON", + "temperature": { + "fahrenheit": 68.0, + "celsius": 20.0 + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/smartac3.turning_off.json b/tests/fixtures/tado/smartac3.turning_off.json new file mode 100644 index 00000000000..0c16f85811a --- /dev/null +++ b/tests/fixtures/tado/smartac3.turning_off.json @@ -0,0 +1,55 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "AIR_CONDITIONING", + "power": "OFF" + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": null, + "nextTimeBlock": { + "start": "2020-03-07T04:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-06T19:05:21.835Z", + "type": "POWER", + "value": "ON" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 21.40, + "fahrenheit": 70.52, + "timestamp": "2020-03-06T19:06:13.185Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 49.20, + "timestamp": "2020-03-06T19:06:13.185Z" + } + } +} diff --git a/tests/fixtures/tado/tadov2.heating.auto_mode.json b/tests/fixtures/tado/tadov2.heating.auto_mode.json new file mode 100644 index 00000000000..34464051f1e --- /dev/null +++ b/tests/fixtures/tado/tadov2.heating.auto_mode.json @@ -0,0 +1,58 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "HEATING", + "power": "ON", + "temperature": { + "celsius": 20.00, + "fahrenheit": 68.00 + } + }, + "overlayType": null, + "overlay": null, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-10T17:00:00Z", + "setting": { + "type": "HEATING", + "power": "ON", + "temperature": { + "celsius": 21.00, + "fahrenheit": 69.80 + } + } + }, + "nextTimeBlock": { + "start": "2020-03-10T17:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": { + "heatingPower": { + "type": "PERCENTAGE", + "percentage": 0.00, + "timestamp": "2020-03-10T07:47:45.978Z" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 20.65, + "fahrenheit": 69.17, + "timestamp": "2020-03-10T07:44:11.947Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 45.20, + "timestamp": "2020-03-10T07:44:11.947Z" + } + } +} diff --git a/tests/fixtures/tado/tadov2.heating.manual_mode.json b/tests/fixtures/tado/tadov2.heating.manual_mode.json new file mode 100644 index 00000000000..a62499d7dd4 --- /dev/null +++ b/tests/fixtures/tado/tadov2.heating.manual_mode.json @@ -0,0 +1,73 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "HEATING", + "power": "ON", + "temperature": { + "celsius": 20.50, + "fahrenheit": 68.90 + } + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "HEATING", + "power": "ON", + "temperature": { + "celsius": 20.50, + "fahrenheit": 68.90 + } + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-10T17:00:00Z", + "setting": { + "type": "HEATING", + "power": "ON", + "temperature": { + "celsius": 21.00, + "fahrenheit": 69.80 + } + } + }, + "nextTimeBlock": { + "start": "2020-03-10T17:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": { + "heatingPower": { + "type": "PERCENTAGE", + "percentage": 0.00, + "timestamp": "2020-03-10T07:47:45.978Z" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 20.65, + "fahrenheit": 69.17, + "timestamp": "2020-03-10T07:44:11.947Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 45.20, + "timestamp": "2020-03-10T07:44:11.947Z" + } + } +} diff --git a/tests/fixtures/tado/tadov2.heating.off_mode.json b/tests/fixtures/tado/tadov2.heating.off_mode.json new file mode 100644 index 00000000000..e22805abc73 --- /dev/null +++ b/tests/fixtures/tado/tadov2.heating.off_mode.json @@ -0,0 +1,67 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "HEATING", + "power": "OFF", + "temperature": null + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "HEATING", + "power": "OFF", + "temperature": null + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-10T17:00:00Z", + "setting": { + "type": "HEATING", + "power": "ON", + "temperature": { + "celsius": 21.00, + "fahrenheit": 69.80 + } + } + }, + "nextTimeBlock": { + "start": "2020-03-10T17:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": { + "heatingPower": { + "type": "PERCENTAGE", + "percentage": 0.00, + "timestamp": "2020-03-10T07:47:45.978Z" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 20.65, + "fahrenheit": 69.17, + "timestamp": "2020-03-10T07:44:11.947Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 45.20, + "timestamp": "2020-03-10T07:44:11.947Z" + } + } +} \ No newline at end of file diff --git a/tests/fixtures/tado/tadov2.water_heater.auto_mode.json b/tests/fixtures/tado/tadov2.water_heater.auto_mode.json new file mode 100644 index 00000000000..7df4e3f5ea6 --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.auto_mode.json @@ -0,0 +1,33 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "HOT_WATER", + "power": "ON", + "temperature": { + "celsius": 65.00, + "fahrenheit": 149.00 + } + }, + "overlayType": null, + "overlay": null, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-10T22:00:00Z", + "setting": { + "type": "HOT_WATER", + "power": "OFF", + "temperature": null + } + }, + "nextTimeBlock": { + "start": "2020-03-10T22:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": {}, + "sensorDataPoints": {} +} diff --git a/tests/fixtures/tado/tadov2.water_heater.heating.json b/tests/fixtures/tado/tadov2.water_heater.heating.json new file mode 100644 index 00000000000..8eecc79d63c --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.heating.json @@ -0,0 +1,51 @@ +{ + "activityDataPoints" : {}, + "preparation" : null, + "openWindow" : null, + "tadoMode" : "HOME", + "nextScheduleChange" : { + "setting" : { + "temperature" : { + "fahrenheit" : 149, + "celsius" : 65 + }, + "type" : "HOT_WATER", + "power" : "ON" + }, + "start" : "2020-03-26T05:00:00Z" + }, + "nextTimeBlock" : { + "start" : "2020-03-26T05:00:00.000Z" + }, + "overlay" : { + "setting" : { + "temperature" : { + "celsius" : 30, + "fahrenheit" : 86 + }, + "type" : "HOT_WATER", + "power" : "ON" + }, + "termination" : { + "type" : "TADO_MODE", + "projectedExpiry" : "2020-03-26T05:00:00Z", + "typeSkillBasedApp" : "TADO_MODE" + }, + "type" : "MANUAL" + }, + "geolocationOverride" : false, + "geolocationOverrideDisableTime" : null, + "sensorDataPoints" : {}, + "overlayType" : "MANUAL", + "link" : { + "state" : "ONLINE" + }, + "setting" : { + "type" : "HOT_WATER", + "temperature" : { + "fahrenheit" : 86, + "celsius" : 30 + }, + "power" : "ON" + } +} diff --git a/tests/fixtures/tado/tadov2.water_heater.manual_mode.json b/tests/fixtures/tado/tadov2.water_heater.manual_mode.json new file mode 100644 index 00000000000..21972a55d6d --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.manual_mode.json @@ -0,0 +1,48 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "HOT_WATER", + "power": "ON", + "temperature": { + "celsius": 55.00, + "fahrenheit": 131.00 + } + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "HOT_WATER", + "power": "ON", + "temperature": { + "celsius": 55.00, + "fahrenheit": 131.00 + } + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-10T22:00:00Z", + "setting": { + "type": "HOT_WATER", + "power": "OFF", + "temperature": null + } + }, + "nextTimeBlock": { + "start": "2020-03-10T22:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": {}, + "sensorDataPoints": {} +} diff --git a/tests/fixtures/tado/tadov2.water_heater.off_mode.json b/tests/fixtures/tado/tadov2.water_heater.off_mode.json new file mode 100644 index 00000000000..12698db601b --- /dev/null +++ b/tests/fixtures/tado/tadov2.water_heater.off_mode.json @@ -0,0 +1,42 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "HOT_WATER", + "power": "OFF", + "temperature": null + }, + "overlayType": "MANUAL", + "overlay": { + "type": "MANUAL", + "setting": { + "type": "HOT_WATER", + "power": "OFF", + "temperature": null + }, + "termination": { + "type": "MANUAL", + "typeSkillBasedApp": "MANUAL", + "projectedExpiry": null + } + }, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-10T22:00:00Z", + "setting": { + "type": "HOT_WATER", + "power": "OFF", + "temperature": null + } + }, + "nextTimeBlock": { + "start": "2020-03-10T22:00:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": {}, + "sensorDataPoints": {} +} diff --git a/tests/fixtures/tado/tadov2.zone_capabilities.json b/tests/fixtures/tado/tadov2.zone_capabilities.json new file mode 100644 index 00000000000..a908b699e64 --- /dev/null +++ b/tests/fixtures/tado/tadov2.zone_capabilities.json @@ -0,0 +1,19 @@ +{ + "type" : "HEATING", + "HEAT" : { + "temperatures" : { + "celsius" : { + "max" : 31, + "step" : 1, + "min" : 16 + }, + "fahrenheit" : { + "step" : 1, + "max" : 88, + "min" : 61 + } + } + }, + "AUTO" : {}, + "FAN" : {} +} diff --git a/tests/fixtures/tado/token.json b/tests/fixtures/tado/token.json new file mode 100644 index 00000000000..1e0089a1c9a --- /dev/null +++ b/tests/fixtures/tado/token.json @@ -0,0 +1,8 @@ +{ + "expires_in" : 599, + "scope" : "home.user", + "token_type" : "bearer", + "refresh_token" : "refresh", + "access_token" : "access", + "jti" : "jti" +} diff --git a/tests/fixtures/tado/water_heater_zone_capabilities.json b/tests/fixtures/tado/water_heater_zone_capabilities.json new file mode 100644 index 00000000000..f3f0daa6c09 --- /dev/null +++ b/tests/fixtures/tado/water_heater_zone_capabilities.json @@ -0,0 +1,17 @@ +{ + "canSetTemperature" : true, + "DRY" : {}, + "type" : "HOT_WATER", + "temperatures" : { + "celsius" : { + "min" : 16, + "max" : 31, + "step" : 1 + }, + "fahrenheit" : { + "step" : 1, + "max" : 88, + "min" : 61 + } + } +} diff --git a/tests/fixtures/tado/zone_capabilities.json b/tests/fixtures/tado/zone_capabilities.json new file mode 100644 index 00000000000..8435094ecca --- /dev/null +++ b/tests/fixtures/tado/zone_capabilities.json @@ -0,0 +1,46 @@ +{ + "type" : "AIR_CONDITIONING", + "HEAT" : { + "fanSpeeds" : [ + "AUTO", + "HIGH", + "MIDDLE", + "LOW" + ], + "temperatures" : { + "celsius" : { + "max" : 31, + "step" : 1, + "min" : 16 + }, + "fahrenheit" : { + "step" : 1, + "max" : 88, + "min" : 61 + } + } + }, + "AUTO" : {}, + "DRY" : {}, + "FAN" : {}, + "COOL" : { + "temperatures" : { + "celsius" : { + "min" : 16, + "step" : 1, + "max" : 31 + }, + "fahrenheit" : { + "min" : 61, + "max" : 88, + "step" : 1 + } + }, + "fanSpeeds" : [ + "AUTO", + "HIGH", + "MIDDLE", + "LOW" + ] + } +} diff --git a/tests/fixtures/tado/zone_state.json b/tests/fixtures/tado/zone_state.json new file mode 100644 index 00000000000..c206dc9d081 --- /dev/null +++ b/tests/fixtures/tado/zone_state.json @@ -0,0 +1,55 @@ +{ + "openWindow" : null, + "nextScheduleChange" : null, + "geolocationOverrideDisableTime" : null, + "sensorDataPoints" : { + "insideTemperature" : { + "celsius" : 22.43, + "type" : "TEMPERATURE", + "precision" : { + "fahrenheit" : 0.1, + "celsius" : 0.1 + }, + "timestamp" : "2020-03-23T18:30:07.377Z", + "fahrenheit" : 72.37 + }, + "humidity" : { + "timestamp" : "2020-03-23T18:30:07.377Z", + "percentage" : 60.2, + "type" : "PERCENTAGE" + } + }, + "overlay" : { + "type" : "MANUAL", + "termination" : { + "projectedExpiry" : null, + "typeSkillBasedApp" : "MANUAL", + "type" : "MANUAL" + }, + "setting" : { + "power" : "OFF", + "type" : "AIR_CONDITIONING" + } + }, + "geolocationOverride" : false, + "overlayType" : "MANUAL", + "activityDataPoints" : { + "acPower" : { + "type" : "POWER", + "timestamp" : "2020-03-11T15:08:23.604Z", + "value" : "OFF" + } + }, + "tadoMode" : "HOME", + "link" : { + "state" : "ONLINE" + }, + "setting" : { + "power" : "OFF", + "type" : "AIR_CONDITIONING" + }, + "nextTimeBlock" : { + "start" : "2020-03-24T03:00:00.000Z" + }, + "preparation" : null +} diff --git a/tests/fixtures/tado/zones.json b/tests/fixtures/tado/zones.json new file mode 100644 index 00000000000..8d7265ade50 --- /dev/null +++ b/tests/fixtures/tado/zones.json @@ -0,0 +1,179 @@ +[ + { + "deviceTypes" : [ + "WR02" + ], + "type" : "HEATING", + "reportAvailable" : false, + "dazzleMode" : { + "enabled" : true, + "supported" : true + }, + "name" : "Baseboard Heater", + "supportsDazzle" : true, + "id" : 1, + "devices" : [ + { + "duties" : [ + "ZONE_UI", + "ZONE_DRIVER", + "ZONE_LEADER" + ], + "currentFwVersion" : "59.4", + "deviceType" : "WR02", + "serialNo" : "WR4", + "shortSerialNo" : "WR4", + "commandTableUploadState" : "FINISHED", + "connectionState" : { + "value" : true, + "timestamp" : "2020-03-23T18:30:07.377Z" + }, + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + } + } + ], + "dateCreated" : "2019-11-28T15:58:48.968Z", + "dazzleEnabled" : true + }, + { + "type" : "HOT_WATER", + "reportAvailable" : false, + "deviceTypes" : [ + "WR02" + ], + "devices" : [ + { + "connectionState" : { + "value" : true, + "timestamp" : "2020-03-23T18:30:07.377Z" + }, + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "duties" : [ + "ZONE_UI", + "ZONE_DRIVER", + "ZONE_LEADER" + ], + "currentFwVersion" : "59.4", + "deviceType" : "WR02", + "serialNo" : "WR4", + "shortSerialNo" : "WR4", + "commandTableUploadState" : "FINISHED" + } + ], + "dazzleEnabled" : true, + "dateCreated" : "2019-11-28T15:58:48.968Z", + "name" : "Water Heater", + "dazzleMode" : { + "enabled" : true, + "supported" : true + }, + "id" : 2, + "supportsDazzle" : true + }, + { + "dazzleMode" : { + "supported" : true, + "enabled" : true + }, + "name" : "Air Conditioning", + "id" : 3, + "supportsDazzle" : true, + "devices" : [ + { + "deviceType" : "WR02", + "shortSerialNo" : "WR4", + "serialNo" : "WR4", + "commandTableUploadState" : "FINISHED", + "duties" : [ + "ZONE_UI", + "ZONE_DRIVER", + "ZONE_LEADER" + ], + "currentFwVersion" : "59.4", + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "connectionState" : { + "timestamp" : "2020-03-23T18:30:07.377Z", + "value" : true + } + } + ], + "dazzleEnabled" : true, + "dateCreated" : "2019-11-28T15:58:48.968Z", + "openWindowDetection" : { + "timeoutInSeconds" : 900, + "enabled" : true, + "supported" : true + }, + "deviceTypes" : [ + "WR02" + ], + "reportAvailable" : false, + "type" : "AIR_CONDITIONING" + }, + { + "type" : "HOT_WATER", + "reportAvailable" : false, + "deviceTypes" : [ + "WR02" + ], + "devices" : [ + { + "connectionState" : { + "value" : true, + "timestamp" : "2020-03-23T18:30:07.377Z" + }, + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "duties" : [ + "ZONE_UI", + "ZONE_DRIVER", + "ZONE_LEADER" + ], + "currentFwVersion" : "59.4", + "deviceType" : "WR02", + "serialNo" : "WR4", + "shortSerialNo" : "WR4", + "commandTableUploadState" : "FINISHED" + } + ], + "dazzleEnabled" : true, + "dateCreated" : "2019-11-28T15:58:48.968Z", + "name" : "Second Water Heater", + "dazzleMode" : { + "enabled" : true, + "supported" : true + }, + "id" : 4, + "supportsDazzle" : true + } +] From 0186ce78968ba95fc83c0ccf9630134cc1120367 Mon Sep 17 00:00:00 2001 From: Santobert Date: Mon, 30 Mar 2020 17:13:22 +0200 Subject: [PATCH 321/431] Filter the history of device_tracker by location attributes (#33356) * Add include_location_attributes * Add check for different state * Fix things * Drop filter * significant changes only * Change default behavior * Remove trailing commas --- homeassistant/components/history/__init__.py | 22 +++++--- tests/components/history/test_init.py | 56 ++++++++++++++++++++ 2 files changed, 71 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/history/__init__.py b/homeassistant/components/history/__init__.py index 7fcbf519bf3..7540740a737 100644 --- a/homeassistant/components/history/__init__.py +++ b/homeassistant/components/history/__init__.py @@ -39,7 +39,7 @@ CONFIG_SCHEMA = vol.Schema( extra=vol.ALLOW_EXTRA, ) -SIGNIFICANT_DOMAINS = ("thermostat", "climate", "water_heater") +SIGNIFICANT_DOMAINS = ("climate", "device_tracker", "thermostat", "water_heater") IGNORE_DOMAINS = ("zone", "scene") @@ -50,6 +50,7 @@ def get_significant_states( entity_ids=None, filters=None, include_start_time_state=True, + significant_changes_only=True, ): """ Return states changes during UTC period start_time - end_time. @@ -61,13 +62,16 @@ def get_significant_states( timer_start = time.perf_counter() with session_scope(hass=hass) as session: - query = session.query(States).filter( - ( - States.domain.in_(SIGNIFICANT_DOMAINS) - | (States.last_changed == States.last_updated) + if significant_changes_only: + query = session.query(States).filter( + ( + States.domain.in_(SIGNIFICANT_DOMAINS) + | (States.last_changed == States.last_updated) + ) + & (States.last_updated > start_time) ) - & (States.last_updated > start_time) - ) + else: + query = session.query(States).filter(States.last_updated > start_time) if filters: query = filters.apply(query, entity_ids) @@ -327,6 +331,9 @@ class HistoryPeriodView(HomeAssistantView): if entity_ids: entity_ids = entity_ids.lower().split(",") include_start_time_state = "skip_initial_state" not in request.query + significant_changes_only = ( + request.query.get("significant_changes_only", "1") != "0" + ) hass = request.app["hass"] @@ -338,6 +345,7 @@ class HistoryPeriodView(HomeAssistantView): entity_ids, self.filters, include_start_time_state, + significant_changes_only, ) result = list(result.values()) if _LOGGER.isEnabledFor(logging.DEBUG): diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 51f1e3cb2ac..64b438a29fc 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -522,6 +522,62 @@ class TestComponentHistory(unittest.TestCase): ) assert list(hist.keys()) == entity_ids + def test_get_significant_states_only(self): + """Test significant states when significant_states_only is set.""" + self.init_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + self.hass.states.set(entity_id, state, **kwargs) + wait_recording_done(self.hass) + return self.hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=start + ): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0] + ): + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1] + ): + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2] + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states( + self.hass, start, significant_changes_only=True + ) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = history.get_significant_states( + self.hass, start, significant_changes_only=False + ) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + def check_significant_states(self, zero, four, states, config): """Check if significant states are retrieved.""" filters = history.Filters() From 01bf4daf373f1d634520a0749bebed99a904031b Mon Sep 17 00:00:00 2001 From: da-anda Date: Mon, 30 Mar 2020 17:27:02 +0200 Subject: [PATCH 322/431] Fix detection of zone master in soundtouch media_player (#33157) --- .../components/soundtouch/media_player.py | 31 ++++++++++++++----- .../soundtouch/test_media_player.py | 19 +++++++----- 2 files changed, 36 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/soundtouch/media_player.py b/homeassistant/components/soundtouch/media_player.py index 1d82c38d088..2f64a2d3605 100644 --- a/homeassistant/components/soundtouch/media_player.py +++ b/homeassistant/components/soundtouch/media_player.py @@ -437,15 +437,25 @@ class SoundTouchDevice(MediaPlayerDevice): # slaves for some reason. To compensate for this shortcoming we have to fetch # the zone info from the master when the current device is a slave until this is # fixed in the SoundTouch API or libsoundtouch, or of course until somebody has a - # better idea on how to fix this - if zone_status.is_master: + # better idea on how to fix this. + # In addition to this shortcoming, libsoundtouch seems to report the "is_master" + # property wrong on some slaves, so the only reliable way to detect if the current + # devices is the master, is by comparing the master_id of the zone with the device_id + if zone_status.master_id == self._device.config.device_id: return self._build_zone_info(self.entity_id, zone_status.slaves) - master_instance = self._get_instance_by_ip(zone_status.master_ip) - master_zone_status = master_instance.device.zone_status() - return self._build_zone_info( - master_instance.entity_id, master_zone_status.slaves - ) + # The master device has to be searched by it's ID and not IP since libsoundtouch / BOSE API + # do not return the IP of the master for some slave objects/responses + master_instance = self._get_instance_by_id(zone_status.master_id) + if master_instance is not None: + master_zone_status = master_instance.device.zone_status() + return self._build_zone_info( + master_instance.entity_id, master_zone_status.slaves + ) + + # We should never end up here since this means we haven't found a master device to get the + # correct zone info from. In this case, assume current device is master + return self._build_zone_info(self.entity_id, zone_status.slaves) def _get_instance_by_ip(self, ip_address): """Search and return a SoundTouchDevice instance by it's IP address.""" @@ -454,6 +464,13 @@ class SoundTouchDevice(MediaPlayerDevice): return instance return None + def _get_instance_by_id(self, instance_id): + """Search and return a SoundTouchDevice instance by it's ID (aka MAC address).""" + for instance in self.hass.data[DATA_SOUNDTOUCH]: + if instance and instance.device.config.device_id == instance_id: + return instance + return None + def _build_zone_info(self, master, zone_slaves): """Build the exposed zone attributes.""" slaves = [] diff --git a/tests/components/soundtouch/test_media_player.py b/tests/components/soundtouch/test_media_player.py index e69cec12ba3..ea580656b24 100644 --- a/tests/components/soundtouch/test_media_player.py +++ b/tests/components/soundtouch/test_media_player.py @@ -33,6 +33,8 @@ from homeassistant.setup import async_setup_component DEVICE_1_IP = "192.168.0.1" DEVICE_2_IP = "192.168.0.2" +DEVICE_1_ID = 1 +DEVICE_2_ID = 2 def get_config(host=DEVICE_1_IP, port=8090, name="soundtouch"): @@ -60,20 +62,22 @@ def one_device_fixture(): def two_zones_fixture(): """Mock one master and one slave.""" device_1 = MockDevice( + DEVICE_1_ID, MockZoneStatus( is_master=True, - master_id=1, + master_id=DEVICE_1_ID, master_ip=DEVICE_1_IP, slaves=[MockZoneSlave(DEVICE_2_IP)], - ) + ), ) device_2 = MockDevice( + DEVICE_2_ID, MockZoneStatus( is_master=False, - master_id=1, + master_id=DEVICE_1_ID, master_ip=DEVICE_1_IP, slaves=[MockZoneSlave(DEVICE_2_IP)], - ) + ), ) devices = {DEVICE_1_IP: device_1, DEVICE_2_IP: device_2} device_patch = patch( @@ -112,9 +116,9 @@ async def setup_soundtouch(hass, config): class MockDevice(STD): """Mock device.""" - def __init__(self, zone_status=None): + def __init__(self, id=None, zone_status=None): """Init the class.""" - self._config = MockConfig() + self._config = MockConfig(id) self._zone_status = zone_status or MockZoneStatus() def zone_status(self, refresh=True): @@ -125,9 +129,10 @@ class MockDevice(STD): class MockConfig(Config): """Mock config.""" - def __init__(self): + def __init__(self, id=None): """Init class.""" self._name = "name" + self._id = id or DEVICE_1_ID class MockZoneStatus(ZoneStatus): From bcd1eb952c19fa2d3dd4c4392de5e713c9718f42 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 30 Mar 2020 13:18:39 -0400 Subject: [PATCH 323/431] =?UTF-8?q?RFC=20-=20Add=20a=203rd=20state=20to=20?= =?UTF-8?q?the=20HA=20shutdown=20sequence=20for=20writing=E2=80=A6=20(#333?= =?UTF-8?q?58)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * add third stage to hass shutdown * use 3rd stage in storage * update core state * add writing data to multi stop ignore * update core test * review comment * update name based on feedback --- homeassistant/const.py | 1 + homeassistant/core.py | 9 ++++++++- homeassistant/helpers/restore_state.py | 9 +++++++-- homeassistant/helpers/storage.py | 4 ++-- tests/helpers/test_storage.py | 4 ++-- tests/test_core.py | 8 +++++++- 6 files changed, 27 insertions(+), 8 deletions(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 74095a2583b..2f1cc75e4a5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -184,6 +184,7 @@ EVENT_CORE_CONFIG_UPDATE = "core_config_updated" EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close" EVENT_HOMEASSISTANT_START = "homeassistant_start" EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" +EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write" EVENT_LOGBOOK_ENTRY = "logbook_entry" EVENT_PLATFORM_DISCOVERED = "platform_discovered" EVENT_SCRIPT_STARTED = "script_started" diff --git a/homeassistant/core.py b/homeassistant/core.py index fd894fd6c05..9265c57bbf3 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -47,6 +47,7 @@ from homeassistant.const import ( EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_REGISTERED, @@ -151,6 +152,7 @@ class CoreState(enum.Enum): starting = "STARTING" running = "RUNNING" stopping = "STOPPING" + writing_data = "WRITING_DATA" def __str__(self) -> str: """Return the event.""" @@ -412,7 +414,7 @@ class HomeAssistant: # regardless of the state of the loop. if self.state == CoreState.not_running: # just ignore return - if self.state == CoreState.stopping: + if self.state == CoreState.stopping or self.state == CoreState.writing_data: _LOGGER.info("async_stop called twice: ignored") return if self.state == CoreState.starting: @@ -426,6 +428,11 @@ class HomeAssistant: await self.async_block_till_done() # stage 2 + self.state = CoreState.writing_data + self.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) + await self.async_block_till_done() + + # stage 3 self.state = CoreState.not_running self.bus.async_fire(EVENT_HOMEASSISTANT_CLOSE) await self.async_block_till_done() diff --git a/homeassistant/helpers/restore_state.py b/homeassistant/helpers/restore_state.py index d57d3ad9920..0757770d2f7 100644 --- a/homeassistant/helpers/restore_state.py +++ b/homeassistant/helpers/restore_state.py @@ -4,7 +4,10 @@ from datetime import datetime, timedelta import logging from typing import Any, Awaitable, Dict, List, Optional, Set, cast -from homeassistant.const import EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP +from homeassistant.const import ( + EVENT_HOMEASSISTANT_FINAL_WRITE, + EVENT_HOMEASSISTANT_START, +) from homeassistant.core import ( CoreState, HomeAssistant, @@ -184,7 +187,9 @@ class RestoreStateData: async_track_time_interval(self.hass, _async_dump_states, STATE_DUMP_INTERVAL) # Dump states when stopping hass - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_dump_states) + self.hass.bus.async_listen_once( + EVENT_HOMEASSISTANT_FINAL_WRITE, _async_dump_states + ) @callback def async_restore_entity_added(self, entity_id: str) -> None: diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 1cad8eec473..5885aa01e6f 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -5,7 +5,7 @@ import logging import os from typing import Any, Callable, Dict, List, Optional, Type, Union -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from homeassistant.loader import bind_hass @@ -153,7 +153,7 @@ class Store: """Ensure that we write if we quit before delay has passed.""" if self._unsub_stop_listener is None: self._unsub_stop_listener = self.hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, self._async_callback_stop_write + EVENT_HOMEASSISTANT_FINAL_WRITE, self._async_callback_stop_write ) @callback diff --git a/tests/helpers/test_storage.py b/tests/helpers/test_storage.py index 8c8d370e4b4..dcadd4d4369 100644 --- a/tests/helpers/test_storage.py +++ b/tests/helpers/test_storage.py @@ -6,7 +6,7 @@ from unittest.mock import Mock, patch import pytest -from homeassistant.const import EVENT_HOMEASSISTANT_STOP +from homeassistant.const import EVENT_HOMEASSISTANT_FINAL_WRITE from homeassistant.helpers import storage from homeassistant.util import dt @@ -85,7 +85,7 @@ async def test_saving_on_stop(hass, hass_storage): store.async_delay_save(lambda: MOCK_DATA, 1) assert store.key not in hass_storage - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) + hass.bus.async_fire(EVENT_HOMEASSISTANT_FINAL_WRITE) await hass.async_block_till_done() assert hass_storage[store.key] == { "version": MOCK_VERSION, diff --git a/tests/test_core.py b/tests/test_core.py index f5a6f4718cd..5e6bb090821 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -21,6 +21,7 @@ from homeassistant.const import ( EVENT_CALL_SERVICE, EVENT_CORE_CONFIG_UPDATE, EVENT_HOMEASSISTANT_CLOSE, + EVENT_HOMEASSISTANT_FINAL_WRITE, EVENT_HOMEASSISTANT_STOP, EVENT_SERVICE_REGISTERED, EVENT_SERVICE_REMOVED, @@ -151,10 +152,14 @@ def test_stage_shutdown(): """Simulate a shutdown, test calling stuff.""" hass = get_test_home_assistant() test_stop = [] + test_final_write = [] test_close = [] test_all = [] hass.bus.listen(EVENT_HOMEASSISTANT_STOP, lambda event: test_stop.append(event)) + hass.bus.listen( + EVENT_HOMEASSISTANT_FINAL_WRITE, lambda event: test_final_write.append(event) + ) hass.bus.listen(EVENT_HOMEASSISTANT_CLOSE, lambda event: test_close.append(event)) hass.bus.listen("*", lambda event: test_all.append(event)) @@ -162,7 +167,8 @@ def test_stage_shutdown(): assert len(test_stop) == 1 assert len(test_close) == 1 - assert len(test_all) == 1 + assert len(test_final_write) == 1 + assert len(test_all) == 2 class TestHomeAssistant(unittest.TestCase): From 3e0ccd2e86df00706ff6c3fdb4c93f2563459055 Mon Sep 17 00:00:00 2001 From: Bas Nijholt Date: Mon, 30 Mar 2020 19:45:24 +0200 Subject: [PATCH 324/431] Add KEF services for DSP (#31967) * add services for DSP * add homeassistant/components/kef/const.py * add services.yaml * fix set_mode * fix services * media_player.py fixes * bump aiokef to 0.2.9 * update requirements_all.txt * add basic sensor.py * add DSP settings as attributes * add message about kef.update_dsp * remove sensor.py * fix pylint issues * update_dsp inside async_added_to_hass * use {...} instead of dict(...) * get DSP settings when connecting to HA or once on update * simplify condition * do not get mode twice * remove async_added_to_hass * use async_register_entity_service * remove entity_id from schema and prepend _value * invalidate self._dsp after setting a DSP setting * schedule update_dsp every hour * subscribe and unsubscribe on adding and removing to HA * don't pass hass and set _update_dsp_task_remover to None after removing --- homeassistant/components/kef/manifest.json | 2 +- homeassistant/components/kef/media_player.py | 142 ++++++++++++++++++- homeassistant/components/kef/services.yaml | 97 +++++++++++++ requirements_all.txt | 2 +- 4 files changed, 238 insertions(+), 5 deletions(-) create mode 100644 homeassistant/components/kef/services.yaml diff --git a/homeassistant/components/kef/manifest.json b/homeassistant/components/kef/manifest.json index 135f8e1cf54..4af0626ace9 100644 --- a/homeassistant/components/kef/manifest.json +++ b/homeassistant/components/kef/manifest.json @@ -4,5 +4,5 @@ "documentation": "https://www.home-assistant.io/integrations/kef", "dependencies": [], "codeowners": ["@basnijholt"], - "requirements": ["aiokef==0.2.7", "getmac==0.8.1"] + "requirements": ["aiokef==0.2.9", "getmac==0.8.1"] } diff --git a/homeassistant/components/kef/media_player.py b/homeassistant/components/kef/media_player.py index d4a1d7a4df3..2a227212006 100644 --- a/homeassistant/components/kef/media_player.py +++ b/homeassistant/components/kef/media_player.py @@ -1,11 +1,13 @@ """Platform for the KEF Wireless Speakers.""" +import asyncio from datetime import timedelta from functools import partial import ipaddress import logging from aiokef import AsyncKefSpeaker +from aiokef.aiokef import DSP_OPTION_MAPPING from getmac import get_mac_address import voluptuous as vol @@ -31,7 +33,8 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.helpers import config_validation as cv +from homeassistant.helpers import config_validation as cv, entity_platform +from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -55,6 +58,17 @@ CONF_INVERSE_SPEAKER_MODE = "inverse_speaker_mode" CONF_SUPPORTS_ON = "supports_on" CONF_STANDBY_TIME = "standby_time" +SERVICE_MODE = "set_mode" +SERVICE_DESK_DB = "set_desk_db" +SERVICE_WALL_DB = "set_wall_db" +SERVICE_TREBLE_DB = "set_treble_db" +SERVICE_HIGH_HZ = "set_high_hz" +SERVICE_LOW_HZ = "set_low_hz" +SERVICE_SUB_DB = "set_sub_db" +SERVICE_UPDATE_DSP = "update_dsp" + +DSP_SCAN_INTERVAL = 3600 + PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -118,6 +132,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= inverse_speaker_mode, supports_on, sources, + speaker_type, ioloop=hass.loop, unique_id=unique_id, ) @@ -128,6 +143,36 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DOMAIN][host] = media_player async_add_entities([media_player], update_before_add=True) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_MODE, + { + vol.Optional("desk_mode"): cv.boolean, + vol.Optional("wall_mode"): cv.boolean, + vol.Optional("phase_correction"): cv.boolean, + vol.Optional("high_pass"): cv.boolean, + vol.Optional("sub_polarity"): vol.In(["-", "+"]), + vol.Optional("bass_extension"): vol.In(["Less", "Standard", "Extra"]), + }, + "set_mode", + ) + platform.async_register_entity_service(SERVICE_UPDATE_DSP, {}, "update_dsp") + + def add_service(name, which, option): + platform.async_register_entity_service( + name, + {vol.Required(option): vol.In(DSP_OPTION_MAPPING[which])}, + f"set_{which}", + ) + + add_service(SERVICE_DESK_DB, "desk_db", "db_value") + add_service(SERVICE_WALL_DB, "wall_db", "db_value") + add_service(SERVICE_TREBLE_DB, "treble_db", "db_value") + add_service(SERVICE_HIGH_HZ, "high_hz", "hz_value") + add_service(SERVICE_LOW_HZ, "low_hz", "hz_value") + add_service(SERVICE_SUB_DB, "sub_db", "db_value") + class KefMediaPlayer(MediaPlayerDevice): """Kef Player Object.""" @@ -143,6 +188,7 @@ class KefMediaPlayer(MediaPlayerDevice): inverse_speaker_mode, supports_on, sources, + speaker_type, ioloop, unique_id, ): @@ -160,12 +206,15 @@ class KefMediaPlayer(MediaPlayerDevice): ) self._unique_id = unique_id self._supports_on = supports_on + self._speaker_type = speaker_type self._state = None self._muted = None self._source = None self._volume = None self._is_online = None + self._dsp = None + self._update_dsp_task_remover = None @property def name(self): @@ -190,6 +239,9 @@ class KefMediaPlayer(MediaPlayerDevice): state = await self._speaker.get_state() self._source = state.source self._state = STATE_ON if state.is_on else STATE_OFF + if self._dsp is None: + # Only do this when necessary because it is a slow operation + await self.update_dsp() else: self._muted = None self._source = None @@ -291,11 +343,11 @@ class KefMediaPlayer(MediaPlayerDevice): async def async_media_play(self): """Send play command.""" - await self._speaker.play_pause() + await self._speaker.set_play_pause() async def async_media_pause(self): """Send pause command.""" - await self._speaker.play_pause() + await self._speaker.set_play_pause() async def async_media_previous_track(self): """Send previous track command.""" @@ -304,3 +356,87 @@ class KefMediaPlayer(MediaPlayerDevice): async def async_media_next_track(self): """Send next track command.""" await self._speaker.next_track() + + async def update_dsp(self) -> None: + """Update the DSP settings.""" + if self._speaker_type == "LS50" and self._state == STATE_OFF: + # The LSX is able to respond when off the LS50 has to be on. + return + + (mode, *rest) = await asyncio.gather( + self._speaker.get_mode(), + self._speaker.get_desk_db(), + self._speaker.get_wall_db(), + self._speaker.get_treble_db(), + self._speaker.get_high_hz(), + self._speaker.get_low_hz(), + self._speaker.get_sub_db(), + ) + keys = ["desk_db", "wall_db", "treble_db", "high_hz", "low_hz", "sub_db"] + self._dsp = dict(zip(keys, rest), **mode._asdict()) + + async def async_added_to_hass(self): + """Subscribe to DSP updates.""" + self._update_dsp_task_remover = async_track_time_interval( + self.hass, self.update_dsp, DSP_SCAN_INTERVAL + ) + + async def async_will_remove_from_hass(self): + """Unsubscribe to DSP updates.""" + self._update_dsp_task_remover() + self._update_dsp_task_remover = None + + @property + def device_state_attributes(self): + """Return the DSP settings of the KEF device.""" + return self._dsp or {} + + async def set_mode( + self, + desk_mode=None, + wall_mode=None, + phase_correction=None, + high_pass=None, + sub_polarity=None, + bass_extension=None, + ): + """Set the speaker mode.""" + await self._speaker.set_mode( + desk_mode=desk_mode, + wall_mode=wall_mode, + phase_correction=phase_correction, + high_pass=high_pass, + sub_polarity=sub_polarity, + bass_extension=bass_extension, + ) + self._dsp = None + + async def set_desk_db(self, db_value): + """Set desk_db of the KEF speakers.""" + await self._speaker.set_desk_db(db_value) + self._dsp = None + + async def set_wall_db(self, db_value): + """Set wall_db of the KEF speakers.""" + await self._speaker.set_wall_db(db_value) + self._dsp = None + + async def set_treble_db(self, db_value): + """Set treble_db of the KEF speakers.""" + await self._speaker.set_treble_db(db_value) + self._dsp = None + + async def set_high_hz(self, hz_value): + """Set high_hz of the KEF speakers.""" + await self._speaker.set_high_hz(hz_value) + self._dsp = None + + async def set_low_hz(self, hz_value): + """Set low_hz of the KEF speakers.""" + await self._speaker.set_low_hz(hz_value) + self._dsp = None + + async def set_sub_db(self, db_value): + """Set sub_db of the KEF speakers.""" + await self._speaker.set_sub_db(db_value) + self._dsp = None diff --git a/homeassistant/components/kef/services.yaml b/homeassistant/components/kef/services.yaml new file mode 100644 index 00000000000..2226d3b6c2d --- /dev/null +++ b/homeassistant/components/kef/services.yaml @@ -0,0 +1,97 @@ +update_dsp: + description: Update all DSP settings. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + +set_mode: + description: Set the mode of the speaker. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + desk_mode: + description: > + "Desk mode" (true or false) + example: true + wall_mode: + description: > + "Wall mode" (true or false) + example: true + phase_correction: + description: > + "Phase correction" (true or false) + example: true + high_pass: + description: > + "High-pass mode" (true or false) + example: true + sub_polarity: + description: > + "Sub polarity" ("-" or "+") + example: "+" + bass_extension: + description: > + "Bass extension" selector ("Less", "Standard", or "Extra") + example: "Extra" + +set_desk_db: + description: Set the "Desk mode" slider of the speaker in dB. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + db_value: + description: Value of the slider (-6 to 0 with steps of 0.5) + example: 0.0 + +set_wall_db: + description: Set the "Wall mode" slider of the speaker in dB. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + db_value: + description: Value of the slider (-6 to 0 with steps of 0.5) + example: 0.0 + +set_treble_db: + description: Set desk the "Treble trim" slider of the speaker in dB. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + db_value: + description: Value of the slider (-2 to 2 with steps of 0.5) + example: 0.0 + +set_high_hz: + description: Set the "High-pass mode" slider of the speaker in Hz. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + hz_value: + description: Value of the slider (50 to 120 with steps of 5) + example: 95 + +set_low_hz: + description: Set the "Sub out low-pass frequency" slider of the speaker in Hz. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + hz_value: + description: Value of the slider (40 to 250 with steps of 5) + example: 80 + +set_sub_db: + description: Set the "Sub gain" slider of the speaker in dB. + fields: + entity_id: + description: The entity_id of the KEF speaker. + example: media_player.kef_lsx + db_value: + description: Value of the slider (-10 to 10 with steps of 1) + example: 0 diff --git a/requirements_all.txt b/requirements_all.txt index 94ce70f6e4e..3e7e5fa5653 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -184,7 +184,7 @@ aioimaplib==0.7.15 aiokafka==0.5.1 # homeassistant.components.kef -aiokef==0.2.7 +aiokef==0.2.9 # homeassistant.components.lifx aiolifx==0.6.7 From 952aa02e371f938280c2698be6eeb6b4c3915663 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 30 Mar 2020 20:33:43 +0200 Subject: [PATCH 325/431] Add ability to specify group when creating user (#33373) * Add abbility to specify group when creating user * Fix tests * Not default admin and tests --- homeassistant/auth/__init__.py | 6 ++-- homeassistant/components/config/auth.py | 18 +++++------ homeassistant/components/onboarding/views.py | 3 +- tests/auth/test_init.py | 27 ++++++++++++++-- tests/components/config/test_auth.py | 33 ++++++++++++++++++-- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/homeassistant/auth/__init__.py b/homeassistant/auth/__init__.py index 710b4af1cd8..26bd10535d0 100644 --- a/homeassistant/auth/__init__.py +++ b/homeassistant/auth/__init__.py @@ -215,12 +215,14 @@ class AuthManager: return user - async def async_create_user(self, name: str) -> models.User: + async def async_create_user( + self, name: str, group_ids: Optional[List[str]] = None + ) -> models.User: """Create a user.""" kwargs: Dict[str, Any] = { "name": name, "is_active": True, - "group_ids": [GROUP_ID_ADMIN], + "group_ids": group_ids or [], } if await self._user_should_be_owner(): diff --git a/homeassistant/components/config/auth.py b/homeassistant/components/config/auth.py index 361367ffb4d..d5bbb60e27d 100644 --- a/homeassistant/components/config/auth.py +++ b/homeassistant/components/config/auth.py @@ -13,11 +13,6 @@ SCHEMA_WS_DELETE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( {vol.Required("type"): WS_TYPE_DELETE, vol.Required("user_id"): str} ) -WS_TYPE_CREATE = "config/auth/create" -SCHEMA_WS_CREATE = websocket_api.BASE_COMMAND_MESSAGE_SCHEMA.extend( - {vol.Required("type"): WS_TYPE_CREATE, vol.Required("name"): str} -) - async def async_setup(hass): """Enable the Home Assistant views.""" @@ -27,9 +22,7 @@ async def async_setup(hass): hass.components.websocket_api.async_register_command( WS_TYPE_DELETE, websocket_delete, SCHEMA_WS_DELETE ) - hass.components.websocket_api.async_register_command( - WS_TYPE_CREATE, websocket_create, SCHEMA_WS_CREATE - ) + hass.components.websocket_api.async_register_command(websocket_create) hass.components.websocket_api.async_register_command(websocket_update) return True @@ -70,9 +63,16 @@ async def websocket_delete(hass, connection, msg): @websocket_api.require_admin @websocket_api.async_response +@websocket_api.websocket_command( + { + vol.Required("type"): "config/auth/create", + vol.Required("name"): str, + vol.Optional("group_ids"): [str], + } +) async def websocket_create(hass, connection, msg): """Create a user.""" - user = await hass.auth.async_create_user(msg["name"]) + user = await hass.auth.async_create_user(msg["name"], msg.get("group_ids")) connection.send_message( websocket_api.result_message(msg["id"], {"user": _user_info(user)}) diff --git a/homeassistant/components/onboarding/views.py b/homeassistant/components/onboarding/views.py index 8eac430ac49..fa859861fb7 100644 --- a/homeassistant/components/onboarding/views.py +++ b/homeassistant/components/onboarding/views.py @@ -3,6 +3,7 @@ import asyncio import voluptuous as vol +from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.http.data_validator import RequestDataValidator from homeassistant.components.http.view import HomeAssistantView from homeassistant.core import callback @@ -99,7 +100,7 @@ class UserOnboardingView(_BaseOnboardingView): provider = _async_get_hass_provider(hass) await provider.async_initialize() - user = await hass.auth.async_create_user(data["name"]) + user = await hass.auth.async_create_user(data["name"], [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 82c0c0dbdbd..edcd01d51e1 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -899,8 +899,8 @@ async def test_async_remove_user(hass): assert events[0].data["user_id"] == user.id -async def test_new_users_admin(mock_hass): - """Test newly created users are admin.""" +async def test_new_users(mock_hass): + """Test newly created users.""" manager = await auth.auth_manager_from_config( mock_hass, [ @@ -911,7 +911,17 @@ async def test_new_users_admin(mock_hass): "username": "test-user", "password": "test-pass", "name": "Test Name", - } + }, + { + "username": "test-user-2", + "password": "test-pass", + "name": "Test Name", + }, + { + "username": "test-user-3", + "password": "test-pass", + "name": "Test Name", + }, ], } ], @@ -920,7 +930,18 @@ async def test_new_users_admin(mock_hass): ensure_auth_manager_loaded(manager) user = await manager.async_create_user("Hello") + # first user in the system is owner and admin + assert user.is_owner assert user.is_admin + 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"]) + assert user.is_admin + assert user.groups[0].id == "system-admin" user_cred = await manager.async_get_or_create_user( auth_models.Credentials( diff --git a/tests/components/config/test_auth.py b/tests/components/config/test_auth.py index b07df39a8fe..53defb4cd6e 100644 --- a/tests/components/config/test_auth.py +++ b/tests/components/config/test_auth.py @@ -156,8 +156,35 @@ async def test_create(hass, hass_ws_client, hass_access_token): assert len(await hass.auth.async_get_users()) == 1 + await client.send_json({"id": 5, "type": "config/auth/create", "name": "Paulus"}) + + result = await client.receive_json() + assert result["success"], result + assert len(await hass.auth.async_get_users()) == 2 + data_user = result["result"]["user"] + user = await hass.auth.async_get_user(data_user["id"]) + assert user is not None + assert user.name == data_user["name"] + assert user.is_active + assert user.groups == [] + assert not user.is_admin + assert not user.is_owner + assert not user.system_generated + + +async def test_create_user_group(hass, hass_ws_client, hass_access_token): + """Test create user with a group.""" + client = await hass_ws_client(hass, hass_access_token) + + assert len(await hass.auth.async_get_users()) == 1 + await client.send_json( - {"id": 5, "type": auth_config.WS_TYPE_CREATE, "name": "Paulus"} + { + "id": 5, + "type": "config/auth/create", + "name": "Paulus", + "group_ids": ["system-admin"], + } ) result = await client.receive_json() @@ -168,6 +195,8 @@ async def test_create(hass, hass_ws_client, hass_access_token): assert user is not None assert user.name == data_user["name"] assert user.is_active + assert user.groups[0].id == "system-admin" + assert user.is_admin assert not user.is_owner assert not user.system_generated @@ -176,7 +205,7 @@ async def test_create_requires_admin(hass, hass_ws_client, hass_read_only_access """Test create command requires an admin.""" client = await hass_ws_client(hass, hass_read_only_access_token) - await client.send_json({"id": 5, "type": auth_config.WS_TYPE_CREATE, "name": "YO"}) + await client.send_json({"id": 5, "type": "config/auth/create", "name": "YO"}) result = await client.receive_json() assert not result["success"], result From 531207e005dff1bcb8b63c1947627523f05b6dae Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 30 Mar 2020 23:20:14 +0200 Subject: [PATCH 326/431] Updated frontend to 20200330.0 (#33449) --- homeassistant/components/frontend/manifest.json | 4 ++-- homeassistant/package_constraints.txt | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index e5f6c3e2a26..e1ae4bca255 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==20200318.1" + "home-assistant-frontend==20200330.0" ], "dependencies": [ "api", @@ -20,4 +20,4 @@ "@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 26571ab7a27..35423033cf9 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200318.1 +home-assistant-frontend==20200330.0 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 3e7e5fa5653..7dcc3da041d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200318.1 +home-assistant-frontend==20200330.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0f0580f8c09..3bf647c911e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200318.1 +home-assistant-frontend==20200330.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 9508c5140306b47e5b821951d41bdc047c982e48 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 30 Mar 2020 23:26:59 +0200 Subject: [PATCH 327/431] Fix change of entity_id for discovered MQTT entities (#33444) --- homeassistant/components/mqtt/__init__.py | 12 +++-- homeassistant/components/mqtt/discovery.py | 5 ++ .../mqtt/test_alarm_control_panel.py | 16 +++++-- tests/components/mqtt/test_binary_sensor.py | 16 +++++-- tests/components/mqtt/test_camera.py | 16 +++++-- tests/components/mqtt/test_climate.py | 16 +++++-- .../mqtt/{common.py => test_common.py} | 48 ++++++++++++++++++- tests/components/mqtt/test_cover.py | 18 +++++-- tests/components/mqtt/test_fan.py | 18 +++++-- tests/components/mqtt/test_legacy_vacuum.py | 16 +++++-- tests/components/mqtt/test_light.py | 18 +++++-- tests/components/mqtt/test_light_json.py | 18 +++++-- tests/components/mqtt/test_light_template.py | 18 +++++-- tests/components/mqtt/test_lock.py | 18 +++++-- tests/components/mqtt/test_sensor.py | 18 +++++-- tests/components/mqtt/test_state_vacuum.py | 18 +++++-- tests/components/mqtt/test_switch.py | 18 +++++-- 17 files changed, 246 insertions(+), 61 deletions(-) rename tests/components/mqtt/{common.py => test_common.py} (90%) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 90dd21ae307..ee14fe432b5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -56,7 +56,7 @@ from .const import ( DEFAULT_QOS, PROTOCOL_311, ) -from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash +from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics @@ -1181,10 +1181,12 @@ class MqttDiscoveryUpdate(Entity): self._discovery_data = discovery_data self._discovery_update = discovery_update self._remove_signal = None + self._removed_from_hass = False async def async_added_to_hass(self) -> None: """Subscribe to discovery updates.""" await super().async_added_to_hass() + self._removed_from_hass = False discovery_hash = ( self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None ) @@ -1217,6 +1219,8 @@ class MqttDiscoveryUpdate(Entity): await self._discovery_update(payload) if discovery_hash: + # Set in case the entity has been removed and is re-added + set_discovery_hash(self.hass, discovery_hash) self._remove_signal = async_dispatcher_connect( self.hass, MQTT_DISCOVERY_UPDATED.format(discovery_hash), @@ -1225,7 +1229,7 @@ class MqttDiscoveryUpdate(Entity): async def async_removed_from_registry(self) -> None: """Clear retained discovery topic in broker.""" - if self._discovery_data: + if not self._removed_from_hass: discovery_topic = self._discovery_data[ATTR_DISCOVERY_TOPIC] publish( self.hass, discovery_topic, "", retain=True, @@ -1237,9 +1241,9 @@ class MqttDiscoveryUpdate(Entity): def _cleanup_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" - if self._discovery_data: + if self._discovery_data and not self._removed_from_hass: clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) - self._discovery_data = None + self._removed_from_hass = True if self._remove_signal: self._remove_signal() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index ac20ba7a4a8..3bcd8594ebe 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -64,6 +64,11 @@ def clear_discovery_hash(hass, discovery_hash): del hass.data[ALREADY_DISCOVERED][discovery_hash] +def set_discovery_hash(hass, discovery_hash): + """Clear entry in ALREADY_DISCOVERED list.""" + hass.data[ALREADY_DISCOVERED][discovery_hash] = {} + + class MQTTConfig(dict): """Dummy class to allow adding attributes.""" diff --git a/tests/components/mqtt/test_alarm_control_panel.py b/tests/components/mqtt/test_alarm_control_panel.py index 93036335e16..45c123fa2fe 100644 --- a/tests/components/mqtt/test_alarm_control_panel.py +++ b/tests/components/mqtt/test_alarm_control_panel.py @@ -13,7 +13,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -25,7 +25,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -481,8 +482,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update( + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, alarm_control_panel.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, alarm_control_panel.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_binary_sensor.py b/tests/components/mqtt/test_binary_sensor.py index 7b104089073..a73919844c1 100644 --- a/tests/components/mqtt/test_binary_sensor.py +++ b/tests/components/mqtt/test_binary_sensor.py @@ -15,7 +15,7 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -27,7 +27,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -495,8 +496,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update( + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, binary_sensor.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, binary_sensor.DOMAIN, DEFAULT_CONFIG ) diff --git a/tests/components/mqtt/test_camera.py b/tests/components/mqtt/test_camera.py index 96ea9b3005f..21f552b4163 100644 --- a/tests/components/mqtt/test_camera.py +++ b/tests/components/mqtt/test_camera.py @@ -5,7 +5,7 @@ from homeassistant.components import camera, mqtt from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -17,7 +17,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -194,8 +195,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update( + await help_test_entity_id_update_subscriptions( hass, mqtt_mock, camera.DOMAIN, DEFAULT_CONFIG, ["test_topic"] ) + + +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, camera.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_climate.py b/tests/components/mqtt/test_climate.py index 29f97af7725..ce21aa53d27 100644 --- a/tests/components/mqtt/test_climate.py +++ b/tests/components/mqtt/test_climate.py @@ -25,7 +25,7 @@ from homeassistant.components.climate.const import ( ) from homeassistant.const import STATE_OFF -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -37,7 +37,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -885,7 +886,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { CLIMATE_DOMAIN: { @@ -895,11 +896,18 @@ async def test_entity_id_update(hass, mqtt_mock): "availability_topic": "avty-topic", } } - await help_test_entity_id_update( + await help_test_entity_id_update_subscriptions( hass, mqtt_mock, CLIMATE_DOMAIN, config, ["test-topic", "avty-topic"] ) +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, CLIMATE_DOMAIN, DEFAULT_CONFIG + ) + + async def test_precision_default(hass, mqtt_mock): """Test that setting precision to tenths works as intended.""" assert await async_setup_component(hass, CLIMATE_DOMAIN, DEFAULT_CONFIG) diff --git a/tests/components/mqtt/common.py b/tests/components/mqtt/test_common.py similarity index 90% rename from tests/components/mqtt/common.py rename to tests/components/mqtt/test_common.py index 702a38928a2..2f437174299 100644 --- a/tests/components/mqtt/common.py +++ b/tests/components/mqtt/test_common.py @@ -440,7 +440,9 @@ async def help_test_entity_device_info_update(hass, mqtt_mock, domain, config): assert device.name == "Milk" -async def help_test_entity_id_update(hass, mqtt_mock, domain, config, topics=None): +async def help_test_entity_id_update_subscriptions( + hass, mqtt_mock, domain, config, topics=None +): """Test MQTT subscriptions are managed when entity_id is updated.""" # Add unique_id to config config = copy.deepcopy(config) @@ -473,3 +475,47 @@ async def help_test_entity_id_update(hass, mqtt_mock, domain, config, topics=Non assert state is not None for topic in topics: mock_mqtt.async_subscribe.assert_any_call(topic, ANY, ANY, ANY) + + +async def help_test_entity_id_update_discovery_update( + hass, mqtt_mock, domain, config, topic=None +): + """Test MQTT discovery update after entity_id is updated.""" + # Add unique_id to config + config = copy.deepcopy(config) + config[domain]["unique_id"] = "TOTALLY_UNIQUE" + + if topic is None: + # Add default topic to config + config[domain]["availability_topic"] = "avty-topic" + topic = "avty-topic" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + ent_registry = mock_registry(hass, {}) + + data = json.dumps(config[domain]) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "online") + state = hass.states.get(f"{domain}.test") + assert state.state != STATE_UNAVAILABLE + + async_fire_mqtt_message(hass, topic, "offline") + state = hass.states.get(f"{domain}.test") + assert state.state == STATE_UNAVAILABLE + + ent_registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") + await hass.async_block_till_done() + + config[domain]["availability_topic"] = f"{topic}_2" + data = json.dumps(config[domain]) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(domain)) == 1 + + async_fire_mqtt_message(hass, f"{topic}_2", "online") + state = hass.states.get(f"{domain}.milk") + assert state.state != STATE_UNAVAILABLE diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 2e5e232cdd5..7749c419ca0 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -22,7 +22,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -34,7 +34,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -1749,6 +1750,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, cover.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, cover.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, cover.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index 0ecc6a25d6f..460c99618bd 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -3,7 +3,7 @@ from homeassistant.components import fan from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -15,7 +15,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -496,6 +497,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, fan.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, fan.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, fan.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_legacy_vacuum.py b/tests/components/mqtt/test_legacy_vacuum.py index 1bbefa35478..14ab79b2d20 100644 --- a/tests/components/mqtt/test_legacy_vacuum.py +++ b/tests/components/mqtt/test_legacy_vacuum.py @@ -19,7 +19,7 @@ from homeassistant.components.vacuum import ( from homeassistant.const import CONF_NAME, CONF_PLATFORM, STATE_OFF, STATE_ON from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -31,7 +31,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -606,7 +607,7 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" config = { vacuum.DOMAIN: { @@ -618,6 +619,13 @@ async def test_entity_id_update(hass, mqtt_mock): "availability_topic": "avty-topic", } } - await help_test_entity_id_update( + await help_test_entity_id_update_subscriptions( hass, mqtt_mock, vacuum.DOMAIN, config, ["test-topic", "avty-topic"] ) + + +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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index ba4078f5374..bc4f5fc3393 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -162,7 +162,7 @@ from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -174,7 +174,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -1345,6 +1346,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, light.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, light.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index c07cec47ecc..f71791e019f 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -101,7 +101,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -113,7 +113,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -1121,6 +1122,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, light.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, light.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_light_template.py b/tests/components/mqtt/test_light_template.py index 4f89ca77847..c9612a7ded7 100644 --- a/tests/components/mqtt/test_light_template.py +++ b/tests/components/mqtt/test_light_template.py @@ -38,7 +38,7 @@ from homeassistant.const import ( import homeassistant.core as ha from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -50,7 +50,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -944,6 +945,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, light.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, light.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, light.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_lock.py b/tests/components/mqtt/test_lock.py index 4c34db6ea20..151021a45f8 100644 --- a/tests/components/mqtt/test_lock.py +++ b/tests/components/mqtt/test_lock.py @@ -3,7 +3,7 @@ from homeassistant.components import lock from homeassistant.const import ATTR_ASSUMED_STATE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -15,7 +15,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -386,6 +387,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, lock.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, lock.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, lock.DOMAIN, DEFAULT_CONFIG + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 0455c5f9c7c..061e53250cb 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -11,7 +11,7 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -23,7 +23,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -392,9 +393,18 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, sensor.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, sensor.DOMAIN, DEFAULT_CONFIG + ) async def test_entity_device_info_with_hub(hass, mqtt_mock): diff --git a/tests/components/mqtt/test_state_vacuum.py b/tests/components/mqtt/test_state_vacuum.py index 1b1150985a2..ecb38ef5774 100644 --- a/tests/components/mqtt/test_state_vacuum.py +++ b/tests/components/mqtt/test_state_vacuum.py @@ -30,7 +30,7 @@ from homeassistant.const import ( ) from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -42,7 +42,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -441,6 +442,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) + + +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, vacuum.DOMAIN, DEFAULT_CONFIG_2 + ) diff --git a/tests/components/mqtt/test_switch.py b/tests/components/mqtt/test_switch.py index 5f5c69d5a22..d8ca8031390 100644 --- a/tests/components/mqtt/test_switch.py +++ b/tests/components/mqtt/test_switch.py @@ -7,7 +7,7 @@ from homeassistant.const import ATTR_ASSUMED_STATE, STATE_OFF, STATE_ON import homeassistant.core as ha from homeassistant.setup import async_setup_component -from .common import ( +from .test_common import ( help_test_availability_without_topic, help_test_custom_availability_payload, help_test_default_availability_payload, @@ -19,7 +19,8 @@ from .common import ( 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, + help_test_entity_id_update_discovery_update, + help_test_entity_id_update_subscriptions, help_test_setting_attribute_via_mqtt_json_message, help_test_setting_attribute_with_template, help_test_unique_id, @@ -353,6 +354,15 @@ async def test_entity_device_info_remove(hass, mqtt_mock): ) -async def test_entity_id_update(hass, mqtt_mock): +async def test_entity_id_update_subscriptions(hass, mqtt_mock): """Test MQTT subscriptions are managed when entity_id is updated.""" - await help_test_entity_id_update(hass, mqtt_mock, switch.DOMAIN, DEFAULT_CONFIG) + await help_test_entity_id_update_subscriptions( + hass, mqtt_mock, switch.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, switch.DOMAIN, DEFAULT_CONFIG + ) From 0e3c1dc0311e3fdd90b74f0edabf3bcf2c902a8b Mon Sep 17 00:00:00 2001 From: evoblicec <56120188+evoblicec@users.noreply.github.com> Date: Tue, 31 Mar 2020 00:05:18 +0200 Subject: [PATCH 328/431] =?UTF-8?q?Add=20new=20mapped=20weather=20conditio?= =?UTF-8?q?n=20classes=20to=20M=C3=A9t=C3=A9o=20France=20(#33450)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Update Meteo_France Weather constants Updating Meteo_France integration with more weather "condition" returned by the web service. Adding: - "Nuit claire" (note the lower 'c') mapped to "clear-night" - "Brume" mapped to "fog" * Black formatting update --- homeassistant/components/meteo_france/const.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/meteo_france/const.py b/homeassistant/components/meteo_france/const.py index b7647f5d97b..2edbf980f36 100644 --- a/homeassistant/components/meteo_france/const.py +++ b/homeassistant/components/meteo_france/const.py @@ -83,9 +83,14 @@ SENSOR_TYPES = { } CONDITION_CLASSES = { - "clear-night": ["Nuit Claire"], + "clear-night": ["Nuit Claire", "Nuit claire"], "cloudy": ["Très nuageux"], - "fog": ["Brume ou bancs de brouillard", "Brouillard", "Brouillard givrant"], + "fog": [ + "Brume ou bancs de brouillard", + "Brume", + "Brouillard", + "Brouillard givrant", + ], "hail": ["Risque de grêle"], "lightning": ["Risque d'orages", "Orages"], "lightning-rainy": ["Pluie orageuses", "Pluies orageuses", "Averses orageuses"], From 98f68f4798ae91668d63caefb484f690628adba5 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Mon, 30 Mar 2020 18:13:47 -0500 Subject: [PATCH 329/431] Add Internet Printing Protocol (IPP) integration (#32859) * Create __init__.py * Create manifest.json * Update zeroconf.py * more work on integration * more work on integration. * add more sensor tests. * Update const.py * Update sensor.py * more work on ipp. * Update test_config_flow.py * more work on ipp. * Update config_flow.py * Update config_flow.py --- CODEOWNERS | 1 + .../components/ipp/.translations/en.json | 32 ++ homeassistant/components/ipp/__init__.py | 190 +++++++++++ homeassistant/components/ipp/config_flow.py | 144 +++++++++ homeassistant/components/ipp/const.py | 25 ++ homeassistant/components/ipp/manifest.json | 11 + homeassistant/components/ipp/sensor.py | 178 ++++++++++ homeassistant/components/ipp/strings.json | 32 ++ homeassistant/generated/config_flows.py | 1 + homeassistant/generated/zeroconf.py | 6 + requirements_all.txt | 3 + requirements_test_all.txt | 3 + tests/components/ipp/__init__.py | 95 ++++++ tests/components/ipp/test_config_flow.py | 306 ++++++++++++++++++ tests/components/ipp/test_init.py | 42 +++ tests/components/ipp/test_sensor.py | 96 ++++++ tests/fixtures/ipp/get-printer-attributes.bin | Bin 0 -> 9143 bytes 17 files changed, 1165 insertions(+) create mode 100644 homeassistant/components/ipp/.translations/en.json create mode 100644 homeassistant/components/ipp/__init__.py create mode 100644 homeassistant/components/ipp/config_flow.py create mode 100644 homeassistant/components/ipp/const.py create mode 100644 homeassistant/components/ipp/manifest.json create mode 100644 homeassistant/components/ipp/sensor.py create mode 100644 homeassistant/components/ipp/strings.json create mode 100644 tests/components/ipp/__init__.py create mode 100644 tests/components/ipp/test_config_flow.py create mode 100644 tests/components/ipp/test_init.py create mode 100644 tests/components/ipp/test_sensor.py create mode 100644 tests/fixtures/ipp/get-printer-attributes.bin diff --git a/CODEOWNERS b/CODEOWNERS index 8f335bfcc4d..4598c6f049d 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -187,6 +187,7 @@ homeassistant/components/intesishome/* @jnimmo homeassistant/components/ios/* @robbiet480 homeassistant/components/iperf3/* @rohankapoorcom homeassistant/components/ipma/* @dgomes @abmantis +homeassistant/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/izone/* @Swamp-Ig diff --git a/homeassistant/components/ipp/.translations/en.json b/homeassistant/components/ipp/.translations/en.json new file mode 100644 index 00000000000..df84cbefa29 --- /dev/null +++ b/homeassistant/components/ipp/.translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "This printer is already configured.", + "connection_error": "Failed to connect to printer.", + "connection_upgrade": "Failed to connect to printer due to connection upgrade being required." + }, + "error": { + "connection_error": "Failed to connect to printer.", + "connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relative path to the printer", + "host": "Host or IP address", + "port": "Port", + "ssl": "Printer supports communication over SSL/TLS", + "verify_ssl": "Printer uses a proper SSL certificate" + }, + "description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.", + "title": "Link your printer" + }, + "zeroconf_confirm": { + "description": "Do you want to add the printer named `{name}` to Home Assistant?", + "title": "Discovered printer" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/__init__.py b/homeassistant/components/ipp/__init__.py new file mode 100644 index 00000000000..447665a3676 --- /dev/null +++ b/homeassistant/components/ipp/__init__.py @@ -0,0 +1,190 @@ +"""The Internet Printing Protocol (IPP) integration.""" +import asyncio +from datetime import timedelta +import logging +from typing import Any, Dict + +from pyipp import IPP, IPPError, Printer as IPPPrinter + +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + ATTR_NAME, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + CONF_BASE_PATH, + DOMAIN, +) + +PLATFORMS = [SENSOR_DOMAIN] +SCAN_INTERVAL = timedelta(seconds=60) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: Dict) -> bool: + """Set up the IPP component.""" + hass.data.setdefault(DOMAIN, {}) + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up IPP from a config entry.""" + + # Create IPP instance for this entry + coordinator = IPPDataUpdateCoordinator( + hass, + host=entry.data[CONF_HOST], + port=entry.data[CONF_PORT], + base_path=entry.data[CONF_BASE_PATH], + tls=entry.data[CONF_SSL], + verify_ssl=entry.data[CONF_VERIFY_SSL], + ) + await coordinator.async_refresh() + + if not coordinator.last_update_success: + raise ConfigEntryNotReady + + hass.data[DOMAIN][entry.entry_id] = coordinator + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class IPPDataUpdateCoordinator(DataUpdateCoordinator): + """Class to manage fetching IPP data from single endpoint.""" + + def __init__( + self, + hass: HomeAssistant, + *, + host: str, + port: int, + base_path: str, + tls: bool, + verify_ssl: bool, + ): + """Initialize global IPP data updater.""" + self.ipp = IPP( + host=host, + port=port, + base_path=base_path, + tls=tls, + verify_ssl=verify_ssl, + session=async_get_clientsession(hass, verify_ssl), + ) + + super().__init__( + hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL, + ) + + async def _async_update_data(self) -> IPPPrinter: + """Fetch data from IPP.""" + try: + return await self.ipp.printer() + except IPPError as error: + raise UpdateFailed(f"Invalid response from API: {error}") + + +class IPPEntity(Entity): + """Defines a base IPP entity.""" + + def __init__( + self, + *, + entry_id: str, + coordinator: IPPDataUpdateCoordinator, + name: str, + icon: str, + enabled_default: bool = True, + ) -> None: + """Initialize the IPP entity.""" + self._enabled_default = enabled_default + self._entry_id = entry_id + self._icon = icon + self._name = name + self._unsub_dispatcher = None + self.coordinator = coordinator + + @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.coordinator.last_update_success + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + return self._enabled_default + + @property + def should_poll(self) -> bool: + """Return the polling requirement of the entity.""" + return False + + async def async_added_to_hass(self) -> None: + """Connect to dispatcher listening for entity data notifications.""" + self.coordinator.async_add_listener(self.async_write_ha_state) + + async def async_will_remove_from_hass(self) -> None: + """Disconnect from update signal.""" + self.coordinator.async_remove_listener(self.async_write_ha_state) + + async def async_update(self) -> None: + """Update an IPP entity.""" + await self.coordinator.async_request_refresh() + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this IPP device.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.uuid)}, + ATTR_NAME: self.coordinator.data.info.name, + ATTR_MANUFACTURER: self.coordinator.data.info.manufacturer, + ATTR_MODEL: self.coordinator.data.info.model, + ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + } diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py new file mode 100644 index 00000000000..395a5f0db58 --- /dev/null +++ b/homeassistant/components/ipp/config_flow.py @@ -0,0 +1,144 @@ +"""Config flow to configure the IPP integration.""" +import logging +from typing import Any, Dict, Optional + +from pyipp import IPP, IPPConnectionError, IPPConnectionUpgradeRequired +import voluptuous as vol + +from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SSL, + CONF_VERIFY_SSL, +) +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ConfigType, HomeAssistantType + +from .const import CONF_BASE_PATH, CONF_UUID +from .const import DOMAIN # pylint: disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + session = async_get_clientsession(hass) + ipp = IPP( + host=data[CONF_HOST], + port=data[CONF_PORT], + base_path=data[CONF_BASE_PATH], + tls=data[CONF_SSL], + verify_ssl=data[CONF_VERIFY_SSL], + session=session, + ) + + printer = await ipp.printer() + + return {CONF_UUID: printer.info.uuid} + + +class IPPFlowHandler(ConfigFlow, domain=DOMAIN): + """Handle an IPP config flow.""" + + VERSION = 1 + CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Set up the instance.""" + self.discovery_info = {} + + async def async_step_user( + self, user_input: Optional[ConfigType] = None + ) -> Dict[str, Any]: + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() + + try: + info = await validate_input(self.hass, user_input) + except IPPConnectionUpgradeRequired: + return self._show_setup_form({"base": "connection_upgrade"}) + except IPPConnectionError: + return self._show_setup_form({"base": "connection_error"}) + user_input[CONF_UUID] = info[CONF_UUID] + + await self.async_set_unique_id(user_input[CONF_UUID]) + self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) + + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) + + async def async_step_zeroconf(self, discovery_info: ConfigType) -> Dict[str, Any]: + """Handle zeroconf discovery.""" + # Hostname is format: EPSON123456.local. + host = discovery_info["hostname"].rstrip(".") + port = discovery_info["port"] + name, _ = host.rsplit(".") + tls = discovery_info["type"] == "_ipps._tcp.local." + + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context.update({"title_placeholders": {"name": name}}) + + self.discovery_info.update( + { + CONF_HOST: host, + CONF_PORT: port, + CONF_SSL: tls, + CONF_VERIFY_SSL: False, + CONF_BASE_PATH: "/" + + discovery_info["properties"].get("rp", "ipp/print"), + CONF_NAME: name, + CONF_UUID: discovery_info["properties"].get("UUID"), + } + ) + + try: + info = await validate_input(self.hass, self.discovery_info) + except IPPConnectionUpgradeRequired: + return self.async_abort(reason="connection_upgrade") + except IPPConnectionError: + return self.async_abort(reason="connection_error") + + self.discovery_info[CONF_UUID] = info[CONF_UUID] + + await self.async_set_unique_id(self.discovery_info[CONF_UUID]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.discovery_info[CONF_HOST]} + ) + + return await self.async_step_zeroconf_confirm() + + async def async_step_zeroconf_confirm( + self, user_input: ConfigType = None + ) -> Dict[str, Any]: + """Handle a confirmation flow initiated by zeroconf.""" + if user_input is None: + return self.async_show_form( + step_id="zeroconf_confirm", + description_placeholders={"name": self.discovery_info[CONF_NAME]}, + errors={}, + ) + + return self.async_create_entry( + title=self.discovery_info[CONF_NAME], data=self.discovery_info, + ) + + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=631): int, + vol.Required(CONF_BASE_PATH, default="/ipp/print"): str, + vol.Required(CONF_SSL, default=False): bool, + vol.Required(CONF_VERIFY_SSL, default=False): bool, + } + ), + errors=errors or {}, + ) diff --git a/homeassistant/components/ipp/const.py b/homeassistant/components/ipp/const.py new file mode 100644 index 00000000000..7caf60b7edd --- /dev/null +++ b/homeassistant/components/ipp/const.py @@ -0,0 +1,25 @@ +"""Constants for the IPP integration.""" + +# Integration domain +DOMAIN = "ipp" + +# Attributes +ATTR_COMMAND_SET = "command_set" +ATTR_IDENTIFIERS = "identifiers" +ATTR_INFO = "info" +ATTR_LOCATION = "location" +ATTR_MANUFACTURER = "manufacturer" +ATTR_MARKER_TYPE = "marker_type" +ATTR_MARKER_LOW_LEVEL = "marker_low_level" +ATTR_MARKER_HIGH_LEVEL = "marker_high_level" +ATTR_MODEL = "model" +ATTR_SERIAL = "serial" +ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_STATE_MESSAGE = "state_message" +ATTR_STATE_REASON = "state_reason" +ATTR_URI_SUPPORTED = "uri_supported" + +# Config Keys +CONF_BASE_PATH = "base_path" +CONF_TLS = "tls" +CONF_UUID = "uuid" diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json new file mode 100644 index 00000000000..beb6679e308 --- /dev/null +++ b/homeassistant/components/ipp/manifest.json @@ -0,0 +1,11 @@ +{ + "domain": "ipp", + "name": "Internet Printing Protocol (IPP)", + "documentation": "https://www.home-assistant.io/integrations/ipp", + "requirements": ["pyipp==0.8.1"], + "dependencies": [], + "codeowners": ["@ctalkington"], + "config_flow": true, + "quality_scale": "platinum", + "zeroconf": ["_ipps._tcp.local.", "_ipp._tcp.local."] +} diff --git a/homeassistant/components/ipp/sensor.py b/homeassistant/components/ipp/sensor.py new file mode 100644 index 00000000000..1ce162500c5 --- /dev/null +++ b/homeassistant/components/ipp/sensor.py @@ -0,0 +1,178 @@ +"""Support for IPP sensors.""" +from datetime import timedelta +from typing import Any, Callable, Dict, List, Optional, Union + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, UNIT_PERCENTAGE +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.dt import utcnow + +from . import IPPDataUpdateCoordinator, IPPEntity +from .const import ( + ATTR_COMMAND_SET, + ATTR_INFO, + ATTR_LOCATION, + ATTR_MARKER_HIGH_LEVEL, + ATTR_MARKER_LOW_LEVEL, + ATTR_MARKER_TYPE, + ATTR_SERIAL, + ATTR_STATE_MESSAGE, + ATTR_STATE_REASON, + ATTR_URI_SUPPORTED, + DOMAIN, +) + + +async def async_setup_entry( + hass: HomeAssistantType, + entry: ConfigEntry, + async_add_entities: Callable[[List[Entity], bool], None], +) -> None: + """Set up IPP sensor based on a config entry.""" + coordinator: IPPDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] + + sensors = [] + + sensors.append(IPPPrinterSensor(entry.entry_id, coordinator)) + sensors.append(IPPUptimeSensor(entry.entry_id, coordinator)) + + for marker_index in range(len(coordinator.data.markers)): + sensors.append(IPPMarkerSensor(entry.entry_id, coordinator, marker_index)) + + async_add_entities(sensors, True) + + +class IPPSensor(IPPEntity): + """Defines an IPP sensor.""" + + def __init__( + self, + *, + coordinator: IPPDataUpdateCoordinator, + enabled_default: bool = True, + entry_id: str, + icon: str, + key: str, + name: str, + unit_of_measurement: Optional[str] = None, + ) -> None: + """Initialize IPP sensor.""" + self._unit_of_measurement = unit_of_measurement + self._key = key + + super().__init__( + entry_id=entry_id, + coordinator=coordinator, + name=name, + icon=icon, + enabled_default=enabled_default, + ) + + @property + def unique_id(self) -> str: + """Return the unique ID for this sensor.""" + return f"{self.coordinator.data.info.uuid}_{self._key}" + + @property + def unit_of_measurement(self) -> str: + """Return the unit this state is expressed in.""" + return self._unit_of_measurement + + +class IPPMarkerSensor(IPPSensor): + """Defines an IPP marker sensor.""" + + def __init__( + self, entry_id: str, coordinator: IPPDataUpdateCoordinator, marker_index: int + ) -> None: + """Initialize IPP marker sensor.""" + self.marker_index = marker_index + + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:water", + key=f"marker_{marker_index}", + name=f"{coordinator.data.info.name} {coordinator.data.markers[marker_index].name}", + unit_of_measurement=UNIT_PERCENTAGE, + ) + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return { + ATTR_MARKER_HIGH_LEVEL: self.coordinator.data.markers[ + self.marker_index + ].high_level, + ATTR_MARKER_LOW_LEVEL: self.coordinator.data.markers[ + self.marker_index + ].low_level, + ATTR_MARKER_TYPE: self.coordinator.data.markers[ + self.marker_index + ].marker_type, + } + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.markers[self.marker_index].level + + +class IPPPrinterSensor(IPPSensor): + """Defines an IPP printer sensor.""" + + def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None: + """Initialize IPP printer sensor.""" + super().__init__( + coordinator=coordinator, + entry_id=entry_id, + icon="mdi:printer", + key="printer", + name=coordinator.data.info.name, + unit_of_measurement=None, + ) + + @property + def device_state_attributes(self) -> Optional[Dict[str, Any]]: + """Return the state attributes of the entity.""" + return { + ATTR_INFO: self.coordinator.data.info.printer_info, + ATTR_SERIAL: self.coordinator.data.info.serial, + ATTR_LOCATION: self.coordinator.data.info.location, + ATTR_STATE_MESSAGE: self.coordinator.data.state.message, + ATTR_STATE_REASON: self.coordinator.data.state.reasons, + ATTR_COMMAND_SET: self.coordinator.data.info.command_set, + ATTR_URI_SUPPORTED: self.coordinator.data.info.printer_uri_supported, + } + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + return self.coordinator.data.state.printer_state + + +class IPPUptimeSensor(IPPSensor): + """Defines a IPP uptime sensor.""" + + def __init__(self, entry_id: str, coordinator: IPPDataUpdateCoordinator) -> None: + """Initialize IPP uptime sensor.""" + super().__init__( + coordinator=coordinator, + enabled_default=False, + entry_id=entry_id, + icon="mdi:clock-outline", + key="uptime", + name=f"{coordinator.data.info.name} Uptime", + ) + + @property + def state(self) -> Union[None, str, int, float]: + """Return the state of the sensor.""" + uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) + return uptime.replace(microsecond=0).isoformat() + + @property + def device_class(self) -> Optional[str]: + """Return the class of this sensor.""" + return DEVICE_CLASS_TIMESTAMP diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json new file mode 100644 index 00000000000..afd82d1f454 --- /dev/null +++ b/homeassistant/components/ipp/strings.json @@ -0,0 +1,32 @@ +{ + "config": { + "title": "Internet Printing Protocol (IPP)", + "flow_title": "Printer: {name}", + "step": { + "user": { + "title": "Link your printer", + "description": "Set up your printer via Internet Printing Protocol (IPP) to integrate with Home Assistant.", + "data": { + "host": "Host or IP address", + "port": "Port", + "base_path": "Relative path to the printer", + "ssl": "Printer supports communication over SSL/TLS", + "verify_ssl": "Printer uses a proper SSL certificate" + } + }, + "zeroconf_confirm": { + "description": "Do you want to add the printer named `{name}` to Home Assistant?", + "title": "Discovered printer" + } + }, + "error": { + "connection_error": "Failed to connect to printer.", + "connection_upgrade": "Failed to connect to printer. Please try again with SSL/TLS option checked." + }, + "abort": { + "already_configured": "This printer is already configured.", + "connection_error": "Failed to connect to printer.", + "connection_upgrade": "Failed to connect to printer due to connection upgrade being required." + } + } +} diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 05bc4a7ba4a..2b96c63f4d7 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -54,6 +54,7 @@ FLOWS = [ "ifttt", "ios", "ipma", + "ipp", "iqvia", "izone", "konnected", diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 968a73588e7..46b3a9943f8 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -25,6 +25,12 @@ ZEROCONF = { "_hap._tcp.local.": [ "homekit_controller" ], + "_ipp._tcp.local.": [ + "ipp" + ], + "_ipps._tcp.local.": [ + "ipp" + ], "_printer._tcp.local.": [ "brother" ], diff --git a/requirements_all.txt b/requirements_all.txt index 7dcc3da041d..2a66e1dd7cb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1335,6 +1335,9 @@ pyintesishome==1.7.1 # homeassistant.components.ipma pyipma==2.0.5 +# homeassistant.components.ipp +pyipp==0.8.1 + # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 3bf647c911e..b7f1f73c2f9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -518,6 +518,9 @@ pyicloud==0.9.6.1 # homeassistant.components.ipma pyipma==2.0.5 +# homeassistant.components.ipp +pyipp==0.8.1 + # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py new file mode 100644 index 00000000000..6bf162725e1 --- /dev/null +++ b/tests/components/ipp/__init__.py @@ -0,0 +1,95 @@ +"""Tests for the IPP integration.""" +import os + +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.core import HomeAssistant + +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker + +ATTR_HOSTNAME = "hostname" +ATTR_PROPERTIES = "properties" + +IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local." +IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local." + +ZEROCONF_NAME = "EPSON123456" +ZEROCONF_HOST = "1.2.3.4" +ZEROCONF_HOSTNAME = "EPSON123456.local." +ZEROCONF_PORT = 631 + + +MOCK_USER_INPUT = { + CONF_HOST: "EPSON123456.local", + CONF_PORT: 361, + CONF_SSL: False, + CONF_VERIFY_SSL: False, + CONF_BASE_PATH: "/ipp/print", +} + +MOCK_ZEROCONF_IPP_SERVICE_INFO = { + CONF_TYPE: IPP_ZEROCONF_SERVICE_TYPE, + CONF_NAME: ZEROCONF_NAME, + CONF_HOST: ZEROCONF_HOST, + ATTR_HOSTNAME: ZEROCONF_HOSTNAME, + CONF_PORT: ZEROCONF_PORT, + ATTR_PROPERTIES: {"rp": "ipp/print"}, +} + +MOCK_ZEROCONF_IPPS_SERVICE_INFO = { + CONF_TYPE: IPPS_ZEROCONF_SERVICE_TYPE, + CONF_NAME: ZEROCONF_NAME, + CONF_HOST: ZEROCONF_HOST, + ATTR_HOSTNAME: ZEROCONF_HOSTNAME, + CONF_PORT: ZEROCONF_PORT, + ATTR_PROPERTIES: {"rp": "ipp/print"}, +} + + +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() + + +async def init_integration( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the IPP integration in Home Assistant.""" + + fixture = "ipp/get-printer-attributes.bin" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", + content=load_fixture_binary(fixture), + headers={"Content-Type": "application/ipp"}, + ) + + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="cfe92100-67c4-11d4-a45f-f8d027761251", + data={ + CONF_HOST: "EPSON123456.local", + CONF_PORT: 631, + CONF_SSL: False, + CONF_VERIFY_SSL: True, + CONF_BASE_PATH: "/ipp/print", + CONF_UUID: "cfe92100-67c4-11d4-a45f-f8d027761251", + }, + ) + + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py new file mode 100644 index 00000000000..505ba618505 --- /dev/null +++ b/tests/components/ipp/test_config_flow.py @@ -0,0 +1,306 @@ +"""Tests for the IPP config flow.""" +import aiohttp +from pyipp import IPPConnectionUpgradeRequired + +from homeassistant import data_entry_flow +from homeassistant.components.ipp import config_flow +from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SSL +from homeassistant.core import HomeAssistant + +from . import ( + MOCK_USER_INPUT, + MOCK_ZEROCONF_IPP_SERVICE_INFO, + MOCK_ZEROCONF_IPPS_SERVICE_INFO, + init_integration, + load_fixture_binary, +) + +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_show_user_form(hass: HomeAssistant) -> None: + """Test that the user set up form is served.""" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + +async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: + """Test that the zeroconf confirmation form is served.""" + flow = config_flow.IPPFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + flow.discovery_info = {CONF_NAME: "EPSON123456"} + + result = await flow.async_step_zeroconf_confirm() + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + + +async def test_show_zeroconf_form( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the zeroconf confirmation form is served.""" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + flow = config_flow.IPPFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await flow.async_step_zeroconf(discovery_info) + + assert flow.discovery_info[CONF_HOST] == "EPSON123456.local" + assert flow.discovery_info[CONF_NAME] == "EPSON123456" + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + + +async def test_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on IPP connection error.""" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError + ) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "connection_error"} + + +async def test_zeroconf_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP connection error.""" + aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + +async def test_zeroconf_confirm_connection_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP connection error.""" + aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={ + "source": SOURCE_ZEROCONF, + CONF_HOST: "EPSON123456.local", + CONF_NAME: "EPSON123456", + }, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_error" + + +async def test_user_connection_upgrade_required( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show the user form if connection upgrade required by server.""" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", exc=IPPConnectionUpgradeRequired + ) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["step_id"] == "user" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {"base": "connection_upgrade"} + + +async def test_zeroconf_connection_upgrade_required( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP connection error.""" + aioclient_mock.post( + "http://EPSON123456.local/ipp/print", exc=IPPConnectionUpgradeRequired + ) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "connection_upgrade" + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort user flow if printer already configured.""" + await init_integration(hass, aioclient_mock) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if printer already configured.""" + await init_integration(hass, aioclient_mock) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_zeroconf_with_uuid_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if printer already configured.""" + await init_integration(hass, aioclient_mock) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + discovery_info["properties"]["UUID"] = "cfe92100-67c4-11d4-a45f-f8d027761251" + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_full_user_flow_implementation( + hass: HomeAssistant, aioclient_mock +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": SOURCE_USER}, + ) + + 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={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "EPSON123456.local" + + assert result["data"] + assert result["data"][CONF_HOST] == "EPSON123456.local" + assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" + + +async def test_full_zeroconf_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + flow = config_flow.IPPFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await flow.async_step_zeroconf(discovery_info) + + assert flow.discovery_info + assert flow.discovery_info[CONF_HOST] == "EPSON123456.local" + assert flow.discovery_info[CONF_NAME] == "EPSON123456" + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "EPSON123456.local"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "EPSON123456" + + assert result["data"] + assert result["data"][CONF_HOST] == "EPSON123456.local" + assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" + assert not result["data"][CONF_SSL] + + +async def test_full_zeroconf_tls_flow_implementation( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + aioclient_mock.post( + "https://EPSON123456.local:631/ipp/print", + content=load_fixture_binary("ipp/get-printer-attributes.bin"), + headers={"Content-Type": "application/ipp"}, + ) + + flow = config_flow.IPPFlowHandler() + flow.hass = hass + flow.context = {"source": SOURCE_ZEROCONF} + + discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy() + result = await flow.async_step_zeroconf(discovery_info) + + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + + result = await flow.async_step_zeroconf_confirm( + user_input={CONF_HOST: "EPSON123456.local"} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "EPSON123456" + + assert result["data"] + assert result["data"][CONF_HOST] == "EPSON123456.local" + assert result["data"][CONF_NAME] == "EPSON123456" + assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" + assert result["data"][CONF_SSL] diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py new file mode 100644 index 00000000000..7d3d0692e28 --- /dev/null +++ b/tests/components/ipp/test_init.py @@ -0,0 +1,42 @@ +"""Tests for the IPP integration.""" +import aiohttp + +from homeassistant.components.ipp.const import DOMAIN +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_RETRY, +) +from homeassistant.core import HomeAssistant + +from tests.components.ipp import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_config_entry_not_ready( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the IPP configuration entry not ready.""" + aioclient_mock.post( + "http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError + ) + + entry = await init_integration(hass, aioclient_mock) + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the IPP configuration entry unloading.""" + entry = await init_integration(hass, aioclient_mock) + + assert hass.data[DOMAIN] + assert entry.entry_id in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.entry_id not in hass.data[DOMAIN] + assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/ipp/test_sensor.py b/tests/components/ipp/test_sensor.py new file mode 100644 index 00000000000..b7db606d870 --- /dev/null +++ b/tests/components/ipp/test_sensor.py @@ -0,0 +1,96 @@ +"""Tests for the IPP sensor platform.""" +from datetime import datetime + +from asynctest import patch + +from homeassistant.components.ipp.const import DOMAIN +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.const import ATTR_ICON, ATTR_UNIT_OF_MEASUREMENT, UNIT_PERCENTAGE +from homeassistant.core import HomeAssistant +from homeassistant.util import dt as dt_util + +from tests.components.ipp import init_integration +from tests.test_util.aiohttp import AiohttpClientMocker + + +async def test_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the IPP sensors.""" + entry = await init_integration(hass, aioclient_mock, skip_setup=True) + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "cfe92100-67c4-11d4-a45f-f8d027761251_uptime", + suggested_object_id="epson_xp_6000_series_uptime", + disabled_by=None, + ) + + test_time = datetime(2019, 11, 11, 9, 10, 32, tzinfo=dt_util.UTC) + with patch("homeassistant.components.ipp.sensor.utcnow", return_value=test_time): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get("sensor.epson_xp_6000_series") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:printer" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + state = hass.states.get("sensor.epson_xp_6000_series_black_ink") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:water" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.state == "58" + + state = hass.states.get("sensor.epson_xp_6000_series_photo_black_ink") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:water" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.state == "98" + + state = hass.states.get("sensor.epson_xp_6000_series_cyan_ink") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:water" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.state == "91" + + state = hass.states.get("sensor.epson_xp_6000_series_yellow_ink") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:water" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.state == "95" + + state = hass.states.get("sensor.epson_xp_6000_series_magenta_ink") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:water" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is UNIT_PERCENTAGE + assert state.state == "73" + + state = hass.states.get("sensor.epson_xp_6000_series_uptime") + assert state + assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + assert state.state == "2019-10-26T15:37:00+00:00" + + entry = registry.async_get("sensor.epson_xp_6000_series_uptime") + assert entry + assert entry.unique_id == "cfe92100-67c4-11d4-a45f-f8d027761251_uptime" + + +async def test_disabled_by_default_sensors( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the disabled by default IPP sensors.""" + await init_integration(hass, aioclient_mock) + registry = await hass.helpers.entity_registry.async_get_registry() + + state = hass.states.get("sensor.epson_xp_6000_series_uptime") + assert state is None + + entry = registry.async_get("sensor.epson_xp_6000_series_uptime") + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" diff --git a/tests/fixtures/ipp/get-printer-attributes.bin b/tests/fixtures/ipp/get-printer-attributes.bin new file mode 100644 index 0000000000000000000000000000000000000000..24b903efc5d6fda33b8693ecb2238244d439313a GIT binary patch literal 9143 zcmeHNOK%(36~3}0>tQ>xWjVIvq>-!uO&Sh0!-qtXRa?p=Wf?ZbN^+c{McL7CNKQPj z%nW5J1&S<-qKl&41VtA830-tmpu7Hn{DdsJ=%)Xmi+<^nBX^v}vwWYV!o*A$i8aT^thG4(vx{ep2oAW9t%uS{1e=P%%pq$=FKp$`PcJ-^F?)z17hx81;6HFde(Y z;p^-z$1`+0Py@rUB~Smjr~BB#&>peYx5rb(YlxO@=`BMYa4*|x)6|1N_nL)tzON{T zjr9wfn0G7{V+1zrmfn|g{mmx+h?%kL0O$K#^d|s!0O&ZUffUWuS7d>??^w;QaccP3 zTT_vh^k!cv$mvbXqJeH2zSC55&5R=VGuvB9;3lZC++0BbZ}Dw(R8#CCCq`d(^rqW& z0!K2NS?n$^z$<*&r;efN%{;)^xIo+k!tPlox+f`eGnZB}`TllufaQ*iqxZ20Y@@b$HWsA0)VQ;veVdE@t z%)Vpx_=!ilya0{u(%*E3y*Y+1KCL7rW9VJ^g8snAd#``W*zE71GI#hW(#Jj3G=j5% zN|2(=th2kr*m(F54vELFvQ*Q?e=(2!%MyHzuhqIhG0gfa=9!?aTxqPDQ;k-`I z)ASq*r=Yb(r@)?I(~0Hf(B-geeW_(wx=otA1{j2M`~eYPgJee#);n7f+qtcUyi+OS zJ-^2x^q9>K;m7TIh+t&K9DHeY&$-4p5o**KnLIW2u4q8YUp zIF(SBr368IzOx)kLoQm5?Py)kvG@Tj5if>I!j@gn(RAM*0f*CkBU%US#ttOM4Garv zGrF4931sn_!tp|*fR&rL5=Ms!jUvLH<7RB0@1Si2Twra(G^sHi0c>0o6?QGuQeADG zaW5J<#(@i-=vmR2u0oC+J4{%S(0RKO&78@)Torf@4CJ zSP8E@@|m$a`?^=!z~IJQhnE@GMZC)A6NFz4hE;payazv-z^fH5<(;_Z+AlBVJ)EB~ zZ!mKy?|U;7c(=c}ly_p-@r%D+%KPw-6XsPuzm#|L*AsY;Ke&|l-M>xX{qgRlyc75P z>EAEqz09b`>SPudIugi-e;ya&g-GRdHuZ;J@%a%03mc;-Ght(iXp2IGyGd6rbrHZy z9nNG?reEV@Uu^SXVin~sQz zZ^dCD!Z{VmCy{U`Qem$rL*~Tp!f+}K8i_&NL<80}_C_d{L0F$;2Ll_#s%z|lpiM;k z7ZATGh?7ac1=Mc|>Y?0VA~{MFVO;{Eu-itb=b@Y+N)&tSR)mW^sX#qsOCk`C5mQqY zb_cR|k?T>?kepIPiI83A6T{tScUz9uLv9gBZO6mG4WiO}s_UAD#!CYmjuz;Fm)VLZo=A*_!)L4uf*P=!#YJ7W*-E<)rU&53UQhHRV_yh}U?DbKE$aw@ByLMZq z7{tymF?`xicEXT3gjL|an zGP@N~wDeQdG$f-nb?;eiuUKsy9n?#Yo>hv~qeqRWnbdCOQL2)|zs83t0v&Fps&xSN zs`Y9k_583_dRBQ_{IDTy%CfwAq@(WSmmWPmDmD)H>&Ml@Ql)w%MqM2x+Q%g1Lk{#1@L{c=6^;;>vg1U@5|DwU(6oXO;6xm10+S5z=(>8M`J z7qYo*F29|Xvw5^+B~!x6ijNvfs`34tlusS(Zl?}v&*jwNQFc>K9bw8$F7=|2syxf4 z_Ky^4OWI6*prmuDXUB?^MYn)*j-)6OhAP9ChBX$$ZaZv+3u$CK4`ZPlzNBiJ4tXOP z{Kz@++0;}Sx)6?GI>PuEE90O;pryXlCDm^6wAdU!MKwM(%kgk#w3zq1LEsiQHk9p5 zTG=e5mGpL|Sdcdwht;EM{kVL*e^B4pJ*Yo9cz(DWZ^TwD9VY6!8+--pu{x3C7J7<; zrO0fdWE@I~BF@WaGvsX6*iy9mo+}}RCa#q~nbnx9=NSp@UBh(#=u$Vc%En+BRao9C zdKVJGtWeMF!FfQou$!JOina`P!za`=e4#iMd#~xhI(N86+#Jun$u_@mcX;Tuum=F0feXLc1}Ry0VpsSsznhhoFXh75#%0i!GfDm1IL5f z4IVyV*{E{9grU*~eYVt~687q@scJ7*4g2LTnCBmzsdm)4_6#=@9tfiT;tzDwbY4Z> zg#_n~aAY2g_-t++P8S~)ES+F)MeG+hB3UIkPa@7AMveWb(Tp0!YV3av zsDjVFLvH2NpCxt?~QQsp+`gMoMD;XMMoxO*bvKBpE=+aoL zkPwEp5u+fOB?-?~ z9!->8b~`el7KM5g8{$eCd)ZdQmj=3+@IGgr@w*W78EpHj+=q{kpFQ0;K4?H}$NTl# zL7D&$*{X#HK*Kdr366qi7+lgPPa(f}FrXU5lKl3Q;2^jl`eE4Gh6_WepX7h-Rl9|F zkQ{{+MzGga!;1i0Fp@|JFY!vo=A(s>^$#UjBbiZ!J&eQec=;pgvUiN~ba?Z%Hp750sBTz*$aI&lK0!{$7P0*Mv^AIrg>R7pdaYP1b#%gQKZ zRY_-e1*=u<5BPfaKUuNuVOiF5!iyXB)weWoYyifwadj@w=q0 z>#d#4);7i#fWCkz`E7!zeVCN0;a1v_G9~UQs6i1{&)<E0?{$ySw; z3>EkdpP!4l23#30A9V!Ky5*dzC#HUkHU5vTOUY+OnooZ-eB=b zo>v69_`LGsyeg=hV#a|B3xfr&G-rGfz4(}R4SQ%?dAa22^iBp<-jT8+s!$WpqO1IH zp(PqTL&x!;7(0kEgJTEw;RhwkC@I*1hM(=wQ|fcKgfh}A6#2V63rWOu3sghE`;n;1 z+_0Sh0Z0fLQwE6re4Nz7;auPc!-$wIs3-Je1$fa46VGWd#twW$9J(ZywG?@~ux5cd zMj+Cu>Yb6n$NVf242oE*9Ea?V*HLZb6CO%ZzV#q-AV~y%6GDc}*kiUH;e}uE2R{}` zI&l;d9@BjX&u;apb;S+%SKF(`8W4>@mr`oNrS!C1-+Nx(s~&CaHyYoi-i~f7@$!+# zcvQmS$ Date: Tue, 31 Mar 2020 00:02:51 +0000 Subject: [PATCH 330/431] [ci skip] Translation update --- .../airvisual/.translations/it.json | 2 +- .../components/cover/.translations/de.json | 6 ++- .../components/directv/.translations/de.json | 3 +- .../components/doorbird/.translations/de.json | 6 ++- .../components/doorbird/.translations/it.json | 34 +++++++++++++++ .../components/elkm1/.translations/de.json | 11 ++++- .../components/elkm1/.translations/es.json | 16 ++++++++ .../components/elkm1/.translations/it.json | 28 +++++++++++++ .../components/elkm1/.translations/no.json | 28 +++++++++++++ .../components/elkm1/.translations/ru.json | 10 +++++ .../components/freebox/.translations/de.json | 1 + .../components/griddy/.translations/de.json | 4 ++ .../konnected/.translations/de.json | 3 +- .../konnected/.translations/en.json | 1 - .../konnected/.translations/no.json | 3 +- .../konnected/.translations/zh-Hant.json | 1 + .../monoprice/.translations/de.json | 8 +++- .../monoprice/.translations/it.json | 41 +++++++++++++++++++ .../components/myq/.translations/it.json | 22 ++++++++++ .../components/nexia/.translations/de.json | 3 ++ .../components/nexia/.translations/it.json | 22 ++++++++++ .../components/nuheat/.translations/de.json | 4 +- .../components/nuheat/.translations/it.json | 25 +++++++++++ .../pvpc_hourly_pricing/.translations/de.json | 4 ++ .../pvpc_hourly_pricing/.translations/it.json | 18 ++++++++ .../components/rachio/.translations/de.json | 10 +++++ .../components/tesla/.translations/it.json | 1 + .../components/vizio/.translations/de.json | 1 + 28 files changed, 306 insertions(+), 10 deletions(-) create mode 100644 homeassistant/components/doorbird/.translations/it.json create mode 100644 homeassistant/components/elkm1/.translations/es.json create mode 100644 homeassistant/components/elkm1/.translations/it.json create mode 100644 homeassistant/components/elkm1/.translations/no.json create mode 100644 homeassistant/components/elkm1/.translations/ru.json create mode 100644 homeassistant/components/monoprice/.translations/it.json create mode 100644 homeassistant/components/myq/.translations/it.json create mode 100644 homeassistant/components/nexia/.translations/it.json create mode 100644 homeassistant/components/nuheat/.translations/it.json create mode 100644 homeassistant/components/pvpc_hourly_pricing/.translations/it.json diff --git a/homeassistant/components/airvisual/.translations/it.json b/homeassistant/components/airvisual/.translations/it.json index 9db76248a36..7d309fdb22a 100644 --- a/homeassistant/components/airvisual/.translations/it.json +++ b/homeassistant/components/airvisual/.translations/it.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "Questa chiave API \u00e8 gi\u00e0 in uso." + "already_configured": "Queste coordinate sono gi\u00e0 state registrate." }, "error": { "invalid_api_key": "Chiave API non valida" diff --git a/homeassistant/components/cover/.translations/de.json b/homeassistant/components/cover/.translations/de.json index c40a7d074f5..9a9f0be21e2 100644 --- a/homeassistant/components/cover/.translations/de.json +++ b/homeassistant/components/cover/.translations/de.json @@ -2,7 +2,11 @@ "device_automation": { "action_type": { "close": "Schlie\u00dfe {entity_name}", - "open": "\u00d6ffne {entity_name}" + "close_tilt": "{entity_name} gekippt schlie\u00dfen", + "open": "\u00d6ffne {entity_name}", + "open_tilt": "{entity_name} gekippt \u00f6ffnen", + "set_position": "Position von {entity_name} setzen", + "set_tilt_position": "Neigeposition von {entity_name} einstellen" }, "condition_type": { "is_closed": "{entity_name} ist geschlossen", diff --git a/homeassistant/components/directv/.translations/de.json b/homeassistant/components/directv/.translations/de.json index 4fecc58dafb..b6074c732f6 100644 --- a/homeassistant/components/directv/.translations/de.json +++ b/homeassistant/components/directv/.translations/de.json @@ -15,7 +15,8 @@ "one": "eins", "other": "andere" }, - "description": "M\u00f6chten Sie {name} einrichten?" + "description": "M\u00f6chten Sie {name} einrichten?", + "title": "Stellen Sie eine Verbindung zum DirecTV-Empf\u00e4nger her" }, "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/de.json b/homeassistant/components/doorbird/.translations/de.json index 4582c469cb9..8676359e5ca 100644 --- a/homeassistant/components/doorbird/.translations/de.json +++ b/homeassistant/components/doorbird/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dieser DoorBird ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", "invalid_auth": "Ung\u00fcltige Authentifizierung", @@ -23,7 +26,8 @@ "init": { "data": { "events": "Durch Kommas getrennte Liste von Ereignissen." - } + }, + "description": "F\u00fcgen Sie f\u00fcr jedes Ereignis, das Sie verfolgen m\u00f6chten, einen durch Kommas getrennten Ereignisnamen hinzu. Nachdem Sie sie hier eingegeben haben, verwenden Sie die DoorBird-App, um sie einem bestimmten Ereignis zuzuweisen. Weitere Informationen finden Sie in der Dokumentation unter https://www.home-assistant.io/integrations/doorbird/#events. Beispiel: jemand_hat_den_knopf_gedr\u00fcckt, bewegung" } } } diff --git a/homeassistant/components/doorbird/.translations/it.json b/homeassistant/components/doorbird/.translations/it.json new file mode 100644 index 00000000000..6d1a80424bf --- /dev/null +++ b/homeassistant/components/doorbird/.translations/it.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Questo DoorBird \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "host": "Host (indirizzo IP)", + "name": "Nome del dispositivo", + "password": "Password", + "username": "Nome utente" + }, + "title": "Connetti a DoorBird" + } + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Elenco di eventi separati da virgole." + }, + "description": "Aggiungere un nome di evento separato da virgola per ogni evento che si desidera monitorare. Dopo averli inseriti qui, usa l'applicazione DoorBird per assegnarli a un evento specifico. Consultare la documentazione su https://www.home-assistant.io/integrations/doorbird/#events. Esempio: qualcuno_premuto_il_pulsante, movimento" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/de.json b/homeassistant/components/elkm1/.translations/de.json index 3afcef8c464..40e6cff4460 100644 --- a/homeassistant/components/elkm1/.translations/de.json +++ b/homeassistant/components/elkm1/.translations/de.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "Ein ElkM1 mit dieser Adresse ist bereits konfiguriert", + "already_configured": "Ein ElkM1 mit diesem Pr\u00e4fix ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", "invalid_auth": "Ung\u00fcltige Authentifizierung", @@ -8,9 +12,14 @@ "step": { "user": { "data": { + "address": "Die IP-Adresse, die Domain oder der serielle Port bei einer seriellen Verbindung.", + "password": "Passwort (Nur sicher).", + "prefix": "Ein eindeutiges Pr\u00e4fix (leer lassen, wenn Sie nur einen ElkM1 haben).", "protocol": "Protokoll", - "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit." + "temperature_unit": "Die von ElkM1 verwendete Temperatureinheit.", + "username": "Benutzername (Nur sicher)." }, + "description": "Die Adresszeichenfolge muss in der Form 'adresse[:port]' f\u00fcr 'sicher' und 'nicht sicher' vorliegen. Beispiel: '192.168.1.1'. Der Port ist optional und standardm\u00e4\u00dfig 2101 f\u00fcr \"nicht sicher\" und 2601 f\u00fcr \"sicher\". F\u00fcr das serielle Protokoll muss die Adresse die Form 'tty[:baud]' haben. Beispiel: '/dev/ttyS1'. Der Baudrate ist optional und standardm\u00e4\u00dfig 115200.", "title": "Stellen Sie eine Verbindung zur Elk-M1-Steuerung her" } }, diff --git a/homeassistant/components/elkm1/.translations/es.json b/homeassistant/components/elkm1/.translations/es.json new file mode 100644 index 00000000000..6602ff3da2e --- /dev/null +++ b/homeassistant/components/elkm1/.translations/es.json @@ -0,0 +1,16 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "protocol": "Protocolo" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/it.json b/homeassistant/components/elkm1/.translations/it.json new file mode 100644 index 00000000000..c3f1941d8b5 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/it.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un ElkM1 con questo indirizzo \u00e8 gi\u00e0 configurato", + "already_configured": "Un ElkM1 con questo prefisso \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "address": "L'indirizzo IP o il dominio o la porta seriale se ci si connette tramite seriale.", + "password": "Password (solo sicura).", + "prefix": "Un prefisso univoco (lasciare vuoto se si dispone di un solo ElkM1).", + "protocol": "Protocollo", + "temperature_unit": "L'unit\u00e0 di temperatura utilizzata da ElkM1.", + "username": "Nome utente (solo sicuro)." + }, + "description": "La stringa di indirizzi deve essere nella forma \"address[:port]\" per \"secure\" e \"non secure\". Esempio: '192.168.1.1.1'. La porta \u00e8 facoltativa e il valore predefinito \u00e8 2101 per 'non sicuro' e 2601 per 'sicuro'. Per il protocollo seriale, l'indirizzo deve essere nella forma 'tty[:baud]'. Esempio: '/dev/ttyS1'. Il baud \u00e8 opzionale e il valore predefinito \u00e8 115200.", + "title": "Collegamento al controllo Elk-M1" + } + }, + "title": "Controllo Elk-M1" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/no.json b/homeassistant/components/elkm1/.translations/no.json new file mode 100644 index 00000000000..86a4e67801b --- /dev/null +++ b/homeassistant/components/elkm1/.translations/no.json @@ -0,0 +1,28 @@ +{ + "config": { + "abort": { + "address_already_configured": "En ElkM1 med denne adressen er allerede konfigurert", + "already_configured": "En ElkM1 med dette prefikset er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "address": "IP-adressen eller domenet eller seriell port hvis du kobler til via seriell.", + "password": "Passord (bare sikkert).", + "prefix": "Et unikt prefiks (la v\u00e6re tomt hvis du bare har en ElkM1).", + "protocol": "protokoll", + "temperature_unit": "Temperaturenheten ElkM1 bruker.", + "username": "Brukernavn (bare sikkert)." + }, + "description": "Adressestrengen m\u00e5 v\u00e6re i formen 'adresse [: port]' for 'sikker' og 'ikke-sikker'. Eksempel: '192.168.1.1'. Porten er valgfri og er standard til 2101 for 'ikke-sikker' og 2601 for 'sikker'. For den serielle protokollen m\u00e5 adressen v\u00e6re i formen 'tty [: baud]'. Eksempel: '/ dev / ttyS1'. Baud er valgfri og er standard til 115200.", + "title": "Koble til Elk-M1-kontroll" + } + }, + "title": "Elk-M1 kontroll" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/ru.json b/homeassistant/components/elkm1/.translations/ru.json new file mode 100644 index 00000000000..1575b47ed68 --- /dev/null +++ b/homeassistant/components/elkm1/.translations/ru.json @@ -0,0 +1,10 @@ +{ + "config": { + "step": { + "user": { + "title": "Elk-M1 Control" + } + }, + "title": "Elk-M1 Control" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/de.json b/homeassistant/components/freebox/.translations/de.json index 7b8b417634a..72caccf49dc 100644 --- a/homeassistant/components/freebox/.translations/de.json +++ b/homeassistant/components/freebox/.translations/de.json @@ -10,6 +10,7 @@ }, "step": { "link": { + "description": "Klicken Sie auf \"Senden\" und ber\u00fchren Sie dann den Pfeil nach rechts auf dem Router, um Freebox bei Home Assistant zu registrieren. \n\n ![Position der Schaltfl\u00e4che am Router]\n (/static/images/config_freebox.png)", "title": "Link Freebox Router" }, "user": { diff --git a/homeassistant/components/griddy/.translations/de.json b/homeassistant/components/griddy/.translations/de.json index 44ef7989afe..f2012615267 100644 --- a/homeassistant/components/griddy/.translations/de.json +++ b/homeassistant/components/griddy/.translations/de.json @@ -9,6 +9,10 @@ }, "step": { "user": { + "data": { + "loadzone": "Ladezone (Abwicklungspunkt)" + }, + "description": "Ihre Ladezone befindet sich in Ihrem Griddy-Konto unter \"Konto > Messger\u00e4t > Ladezone\".", "title": "Richten Sie Ihre Griddy Ladezone ein" } }, diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json index ab29e9d1f08..ffd8f3219fe 100644 --- a/homeassistant/components/konnected/.translations/de.json +++ b/homeassistant/components/konnected/.translations/de.json @@ -91,11 +91,12 @@ "data": { "activation": "Ausgabe, wenn eingeschaltet", "momentary": "Impulsdauer (ms) (optional)", + "more_states": "Konfigurieren Sie zus\u00e4tzliche Zust\u00e4nde f\u00fcr diese Zone", "name": "Name (optional)", "pause": "Pause zwischen Impulsen (ms) (optional)", "repeat": "Zeit zum Wiederholen (-1 = unendlich) (optional)" }, - "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone}" + "description": "Bitte w\u00e4hlen Sie die Ausgabeoptionen f\u00fcr {zone} : Status {state}" } }, "title": "Konnected Alarm Panel-Optionen" diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json index bc86a5ca549..ae41b64ad98 100644 --- a/homeassistant/components/konnected/.translations/en.json +++ b/homeassistant/components/konnected/.translations/en.json @@ -33,7 +33,6 @@ "abort": { "not_konn_panel": "Not a recognized Konnected.io device" }, - "error": {}, "step": { "options_binary": { "data": { diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json index 9e3b3bdb7c9..71c0fa1de6e 100644 --- a/homeassistant/components/konnected/.translations/no.json +++ b/homeassistant/components/konnected/.translations/no.json @@ -91,11 +91,12 @@ "data": { "activation": "Utgang n\u00e5r den er p\u00e5", "momentary": "Pulsvarighet (ms) (valgfritt)", + "more_states": "Konfigurere flere tilstander for denne sonen", "name": "Navn (valgfritt)", "pause": "Pause mellom pulser (ms) (valgfritt)", "repeat": "Tider \u00e5 gjenta (-1 = uendelig) (valgfritt)" }, - "description": "Velg outputalternativer for {zone}", + "description": "Velg outputalternativer for {zone} : state {state}", "title": "Konfigurere Valgbare Utgang" } }, diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json index 4c1bec691db..f3aa89fe877 100644 --- a/homeassistant/components/konnected/.translations/zh-Hant.json +++ b/homeassistant/components/konnected/.translations/zh-Hant.json @@ -91,6 +91,7 @@ "data": { "activation": "\u958b\u555f\u6642\u8f38\u51fa", "momentary": "\u6301\u7e8c\u6642\u9593\uff08ms\uff09\uff08\u9078\u9805\uff09", + "more_states": "\u8a2d\u5b9a\u6b64\u5340\u57df\u7684\u9644\u52a0\u72c0\u614b", "name": "\u540d\u7a31\uff08\u9078\u9805\uff09", "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" diff --git a/homeassistant/components/monoprice/.translations/de.json b/homeassistant/components/monoprice/.translations/de.json index 176b1f5c1ac..ea2b8cdc6c4 100644 --- a/homeassistant/components/monoprice/.translations/de.json +++ b/homeassistant/components/monoprice/.translations/de.json @@ -1,11 +1,16 @@ { "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", "unknown": "Unerwarteter Fehler" }, "step": { "user": { "data": { + "port": "Serielle Schnittstelle", "source_1": "Name der Quelle #1", "source_2": "Name der Quelle #2", "source_3": "Name der Quelle #3", @@ -15,7 +20,8 @@ }, "title": "Stellen Sie eine Verbindung zum Ger\u00e4t her" } - } + }, + "title": "Monoprice 6-Zonen-Verst\u00e4rker" }, "options": { "step": { diff --git a/homeassistant/components/monoprice/.translations/it.json b/homeassistant/components/monoprice/.translations/it.json new file mode 100644 index 00000000000..c3c8770d2ad --- /dev/null +++ b/homeassistant/components/monoprice/.translations/it.json @@ -0,0 +1,41 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "port": "Porta seriale", + "source_1": "Nome della fonte n. 1", + "source_2": "Nome della fonte n. 2", + "source_3": "Nome della fonte n. 3", + "source_4": "Nome della fonte n. 4", + "source_5": "Nome della fonte n. 5", + "source_6": "Nome della fonte n. 6" + }, + "title": "Connettersi al dispositivo" + } + }, + "title": "Amplificatore a 6 zone Monoprice" + }, + "options": { + "step": { + "init": { + "data": { + "source_1": "Nome della fonte n. 1", + "source_2": "Nome della fonte n. 2", + "source_3": "Nome della fonte n. 3", + "source_4": "Nome della fonte n. 4", + "source_5": "Nome della fonte n. 5", + "source_6": "Nome della fonte n. 6" + }, + "title": "Configurare le sorgenti" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/myq/.translations/it.json b/homeassistant/components/myq/.translations/it.json new file mode 100644 index 00000000000..4f495e670f1 --- /dev/null +++ b/homeassistant/components/myq/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Connettersi al gateway MyQ" + } + }, + "title": "MyQ" + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/.translations/de.json b/homeassistant/components/nexia/.translations/de.json index 123cfa26a67..bda92cc7fe3 100644 --- a/homeassistant/components/nexia/.translations/de.json +++ b/homeassistant/components/nexia/.translations/de.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Dieses Nexia Home ist bereits konfiguriert" + }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", "invalid_auth": "Ung\u00fcltige Authentifizierung", diff --git a/homeassistant/components/nexia/.translations/it.json b/homeassistant/components/nexia/.translations/it.json new file mode 100644 index 00000000000..5fdd9a6095e --- /dev/null +++ b/homeassistant/components/nexia/.translations/it.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Questo Nexia Home \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Connettersi a mynexia.com" + } + }, + "title": "Nexia" + } +} \ No newline at end of file diff --git a/homeassistant/components/nuheat/.translations/de.json b/homeassistant/components/nuheat/.translations/de.json index 358b5a76254..adbc63b8157 100644 --- a/homeassistant/components/nuheat/.translations/de.json +++ b/homeassistant/components/nuheat/.translations/de.json @@ -15,7 +15,9 @@ "password": "Passwort", "serial_number": "Seriennummer des Thermostats.", "username": "Benutzername" - } + }, + "description": "Sie m\u00fcssen die numerische Seriennummer oder ID Ihres Thermostats erhalten, indem Sie sich bei https://MyNuHeat.com anmelden und Ihre Thermostate ausw\u00e4hlen.", + "title": "Stellen Sie eine Verbindung zu NuHeat her" } }, "title": "NuHeat" diff --git a/homeassistant/components/nuheat/.translations/it.json b/homeassistant/components/nuheat/.translations/it.json new file mode 100644 index 00000000000..a98f24a9651 --- /dev/null +++ b/homeassistant/components/nuheat/.translations/it.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Il termostato \u00e8 gi\u00e0 configurato" + }, + "error": { + "cannot_connect": "Impossibile connettersi, si prega di riprovare", + "invalid_auth": "Autenticazione non valida", + "invalid_thermostat": "Il numero di serie del termostato non \u00e8 valido.", + "unknown": "Errore imprevisto" + }, + "step": { + "user": { + "data": { + "password": "Password", + "serial_number": "Numero di serie del termostato.", + "username": "Nome utente" + }, + "description": "\u00c8 necessario ottenere il numero di serie o l'ID numerico del termostato accedendo a https://MyNuHeat.com e selezionando il termostato.", + "title": "Connettersi al NuHeat" + } + }, + "title": "NuHeat" + } +} \ No newline at end of file diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/de.json b/homeassistant/components/pvpc_hourly_pricing/.translations/de.json index f8bf787b685..2e80e3da6e6 100644 --- a/homeassistant/components/pvpc_hourly_pricing/.translations/de.json +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/de.json @@ -1,11 +1,15 @@ { "config": { + "abort": { + "already_configured": "Die Integration ist bereits mit einem vorhandenen Sensor mit diesem Tarif konfiguriert" + }, "step": { "user": { "data": { "name": "Sensorname", "tariff": "Vertragstarif (1, 2 oder 3 Perioden)" }, + "description": "Dieser Sensor verwendet die offizielle API, um [st\u00fcndliche Strompreise (PVPC)] (https://www.esios.ree.es/es/pvpc) in Spanien zu erhalten. \nWeitere Informationen finden Sie in den [Integrations-Dokumentation] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nW\u00e4hlen Sie den vertraglich vereinbarten Tarif basierend auf der Anzahl der Abrechnungsperioden pro Tag aus: \n - 1 Periode: Normal \n - 2 Perioden: Diskriminierung (Nachttarif) \n - 3 Perioden: Elektroauto (Nachttarif von 3 Perioden)", "title": "Tarifauswahl" } }, diff --git a/homeassistant/components/pvpc_hourly_pricing/.translations/it.json b/homeassistant/components/pvpc_hourly_pricing/.translations/it.json new file mode 100644 index 00000000000..5e0c6acef50 --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/.translations/it.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "already_configured": "L'integrazione \u00e8 gi\u00e0 configurata con un sensore esistente con quella tariffa" + }, + "step": { + "user": { + "data": { + "name": "Nome del sensore", + "tariff": "Tariffa contrattuale (1, 2 o 3 periodi)" + }, + "description": "Questo sensore utilizza l'API ufficiale per ottenere [prezzi orari dell'elettricit\u00e0 (PVPC)](https://www.esios.ree.es/es/pvpc) in Spagna.\nPer una spiegazione pi\u00f9 precisa, visitare la [documentazione di integrazione](https://www.home-assistant.io/integrations/pvpc_hourly_pricing/).\n\nSelezionare la tariffa contrattuale in base al numero di periodi di fatturazione al giorno:\n- 1 periodo: normale\n- 2 periodi: discriminazione (tariffa notturna)\n- 3 periodi: auto elettrica (tariffa notturna di 3 periodi)", + "title": "Selezione della tariffa" + } + }, + "title": "Prezzo orario dell'elettricit\u00e0 in Spagna (PVPC)" + } +} \ No newline at end of file diff --git a/homeassistant/components/rachio/.translations/de.json b/homeassistant/components/rachio/.translations/de.json index 79c15a43dc4..05bf5fbe4dd 100644 --- a/homeassistant/components/rachio/.translations/de.json +++ b/homeassistant/components/rachio/.translations/de.json @@ -13,9 +13,19 @@ "data": { "api_key": "Der API-Schl\u00fcssel f\u00fcr das Rachio-Konto." }, + "description": "Sie ben\u00f6tigen den API-Schl\u00fcssel von https://app.rach.io/. W\u00e4hlen Sie \"Kontoeinstellungen\" und klicken Sie dann auf \"API-SCHL\u00dcSSEL ERHALTEN\".", "title": "Stellen Sie eine Verbindung zu Ihrem Rachio-Ger\u00e4t her" } }, "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Wie lange, in Minuten, um eine Station einzuschalten, wenn der Schalter aktiviert ist." + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/tesla/.translations/it.json b/homeassistant/components/tesla/.translations/it.json index 0e254cf2843..e9bf5e2d4fe 100644 --- a/homeassistant/components/tesla/.translations/it.json +++ b/homeassistant/components/tesla/.translations/it.json @@ -22,6 +22,7 @@ "step": { "init": { "data": { + "enable_wake_on_start": "Forza il risveglio delle auto all'avvio", "scan_interval": "Secondi tra le scansioni" } } diff --git a/homeassistant/components/vizio/.translations/de.json b/homeassistant/components/vizio/.translations/de.json index f7c0916ef7b..a3b69526943 100644 --- a/homeassistant/components/vizio/.translations/de.json +++ b/homeassistant/components/vizio/.translations/de.json @@ -71,6 +71,7 @@ "timeout": "API Request Timeout (Sekunden)", "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, + "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", "title": "Aktualisieren Sie die Vizo SmartCast-Optionen" } }, From a9cbd355cab2ce8de2dded0f9a62080fac859d5c Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Mar 2020 02:20:37 +0200 Subject: [PATCH 331/431] Correct FortiOS integration name in manifest (#33454) --- homeassistant/components/fortios/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/fortios/manifest.json b/homeassistant/components/fortios/manifest.json index dd0fdae9619..4073f1bbb36 100644 --- a/homeassistant/components/fortios/manifest.json +++ b/homeassistant/components/fortios/manifest.json @@ -1,6 +1,6 @@ { "domain": "fortios", - "name": "Home Assistant Device Tracker to support FortiOS", + "name": "FortiOS", "documentation": "https://www.home-assistant.io/integrations/fortios/", "requirements": ["fortiosapi==0.10.8"], "dependencies": [], From 7330e30fd3e6c2fc4b2951b5bafd9636023659d1 Mon Sep 17 00:00:00 2001 From: Balazs Sandor Date: Tue, 31 Mar 2020 02:33:07 +0200 Subject: [PATCH 332/431] bump version zigpy-cc (#33448) --- 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 8bb7c3c2149..ea1bc1bbb2f 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -6,7 +6,7 @@ "requirements": [ "bellows-homeassistant==0.14.0", "zha-quirks==0.0.37", - "zigpy-cc==0.3.0", + "zigpy-cc==0.3.1", "zigpy-deconz==0.7.0", "zigpy-homeassistant==0.16.0", "zigpy-xbee-homeassistant==0.10.0", diff --git a/requirements_all.txt b/requirements_all.txt index 2a66e1dd7cb..3063b5d1030 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2185,7 +2185,7 @@ zhong_hong_hvac==1.0.9 ziggo-mediabox-xl==1.1.0 # homeassistant.components.zha -zigpy-cc==0.3.0 +zigpy-cc==0.3.1 # homeassistant.components.zha zigpy-deconz==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index b7f1f73c2f9..09542dddd54 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -798,7 +798,7 @@ zeroconf==0.24.5 zha-quirks==0.0.37 # homeassistant.components.zha -zigpy-cc==0.3.0 +zigpy-cc==0.3.1 # homeassistant.components.zha zigpy-deconz==0.7.0 From d0dad4bfd658b6dc779cbba10a539f7c3c7abbe3 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Mon, 30 Mar 2020 20:34:23 -0400 Subject: [PATCH 333/431] Fix uncaught exceptions in ZHA tests (#33442) --- homeassistant/components/zha/core/gateway.py | 1 - tests/ignore_uncaught_exceptions.py | 29 -------------------- 2 files changed, 30 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 14a6a5c839e..21f2f636128 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -474,7 +474,6 @@ class ZHAGateway: """Update the devices in the store.""" for device in self.devices.values(): self.zha_storage.async_update_device(device) - await self.zha_storage.async_save() async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): """Handle device joined and basic information discovered (async).""" diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index b0feb2bddb3..58531b251e0 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -121,35 +121,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.yr.test_sensor", "test_default_setup"), ("tests.components.yr.test_sensor", "test_custom_setup"), ("tests.components.yr.test_sensor", "test_forecast_setup"), - ("tests.components.zha.test_api", "test_device_clusters"), - ("tests.components.zha.test_api", "test_device_cluster_attributes"), - ("tests.components.zha.test_api", "test_device_cluster_commands"), - ("tests.components.zha.test_api", "test_list_devices"), - ("tests.components.zha.test_api", "test_device_not_found"), - ("tests.components.zha.test_api", "test_list_groups"), - ("tests.components.zha.test_api", "test_get_group"), - ("tests.components.zha.test_api", "test_get_group_not_found"), - ("tests.components.zha.test_api", "test_list_groupable_devices"), - ("tests.components.zha.test_api", "test_add_group"), - ("tests.components.zha.test_api", "test_remove_group"), - ("tests.components.zha.test_binary_sensor", "test_binary_sensor"), - ("tests.components.zha.test_cover", "test_cover"), - ("tests.components.zha.test_device_action", "test_get_actions"), - ("tests.components.zha.test_device_action", "test_action"), - ("tests.components.zha.test_device_tracker", "test_device_tracker"), - ("tests.components.zha.test_device_trigger", "test_triggers"), - ("tests.components.zha.test_device_trigger", "test_no_triggers"), - ("tests.components.zha.test_device_trigger", "test_if_fires_on_event"), - ("tests.components.zha.test_device_trigger", "test_exception_no_triggers"), - ("tests.components.zha.test_device_trigger", "test_exception_bad_trigger"), - ("tests.components.zha.test_discover", "test_devices"), - ("tests.components.zha.test_discover", "test_device_override"), - ("tests.components.zha.test_fan", "test_fan"), - ("tests.components.zha.test_gateway", "test_gateway_group_methods"), - ("tests.components.zha.test_light", "test_light"), - ("tests.components.zha.test_lock", "test_lock"), - ("tests.components.zha.test_sensor", "test_sensor"), - ("tests.components.zha.test_switch", "test_switch"), ("tests.components.zwave.test_init", "test_power_schemes"), ( "tests.helpers.test_entity_platform", From 6208d8c911226f690abca34bdb3e446957d4b7ae Mon Sep 17 00:00:00 2001 From: Marcel Steinbach Date: Tue, 31 Mar 2020 02:47:03 +0200 Subject: [PATCH 334/431] Add HomeKit support for slat tilting (#33388) * Add HomeKit support for slat tilting * Reset tilt-specific attribute, not position attribute Co-Authored-By: J. Nick Koston * Add explanation why we fix HomeKit's targets We have to assume that the device has worse precision than HomeKit. If it reports back a state that is only _close_ to HK's requested state, we'll "fix" what HomeKit requested so that it won't appear out of sync. Co-authored-by: J. Nick Koston --- homeassistant/components/homekit/const.py | 4 + .../components/homekit/type_covers.py | 73 ++++++++++++++++++- tests/components/homekit/test_type_covers.py | 70 ++++++++++++++++++ 3 files changed, 143 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index 82ec296da4b..ac421913f6f 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -1,10 +1,12 @@ """Constants used be the HomeKit component.""" # #### Misc #### DEBOUNCE_TIMEOUT = 0.5 +DEVICE_PRECISION_LEEWAY = 6 DOMAIN = "homekit" HOMEKIT_FILE = ".homekit.state" HOMEKIT_NOTIFY_ID = 4663548 + # #### Attributes #### ATTR_DISPLAY_NAME = "display_name" ATTR_VALUE = "value" @@ -106,6 +108,7 @@ CHAR_CURRENT_POSITION = "CurrentPosition" CHAR_CURRENT_HUMIDITY = "CurrentRelativeHumidity" CHAR_CURRENT_SECURITY_STATE = "SecuritySystemCurrentState" CHAR_CURRENT_TEMPERATURE = "CurrentTemperature" +CHAR_CURRENT_TILT_ANGLE = "CurrentHorizontalTiltAngle" CHAR_CURRENT_VISIBILITY_STATE = "CurrentVisibilityState" CHAR_FIRMWARE_REVISION = "FirmwareRevision" CHAR_HEATING_THRESHOLD_TEMPERATURE = "HeatingThresholdTemperature" @@ -141,6 +144,7 @@ CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" CHAR_TARGET_POSITION = "TargetPosition" CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState" CHAR_TARGET_TEMPERATURE = "TargetTemperature" +CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle" CHAR_TEMP_DISPLAY_UNITS = "TemperatureDisplayUnits" CHAR_VALVE_TYPE = "ValveType" CHAR_VOLUME = "Volume" diff --git a/homeassistant/components/homekit/type_covers.py b/homeassistant/components/homekit/type_covers.py index d77ea22dc96..97940952171 100644 --- a/homeassistant/components/homekit/type_covers.py +++ b/homeassistant/components/homekit/type_covers.py @@ -5,8 +5,11 @@ from pyhap.const import CATEGORY_GARAGE_DOOR_OPENER, CATEGORY_WINDOW_COVERING from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, ) from homeassistant.const import ( @@ -15,6 +18,7 @@ from homeassistant.const import ( SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, SERVICE_SET_COVER_POSITION, + SERVICE_SET_COVER_TILT_POSITION, SERVICE_STOP_COVER, STATE_CLOSED, STATE_CLOSING, @@ -27,9 +31,12 @@ from .accessories import HomeAccessory, debounce from .const import ( CHAR_CURRENT_DOOR_STATE, CHAR_CURRENT_POSITION, + CHAR_CURRENT_TILT_ANGLE, CHAR_POSITION_STATE, CHAR_TARGET_DOOR_STATE, CHAR_TARGET_POSITION, + CHAR_TARGET_TILT_ANGLE, + DEVICE_PRECISION_LEEWAY, SERV_GARAGE_DOOR_OPENER, SERV_WINDOW_COVERING, ) @@ -94,9 +101,28 @@ class WindowCovering(HomeAccessory): def __init__(self, *args): """Initialize a WindowCovering accessory object.""" super().__init__(*args, category=CATEGORY_WINDOW_COVERING) - self._homekit_target = None - serv_cover = self.add_preload_service(SERV_WINDOW_COVERING) + self._homekit_target = None + self._homekit_target_tilt = None + + serv_cover = self.add_preload_service( + SERV_WINDOW_COVERING, + chars=[CHAR_TARGET_TILT_ANGLE, CHAR_CURRENT_TILT_ANGLE], + ) + + features = self.hass.states.get(self.entity_id).attributes.get( + ATTR_SUPPORTED_FEATURES, 0 + ) + + self._supports_tilt = features & SUPPORT_SET_TILT_POSITION + if self._supports_tilt: + self.char_target_tilt = serv_cover.configure_char( + CHAR_TARGET_TILT_ANGLE, setter_callback=self.set_tilt + ) + self.char_current_tilt = serv_cover.configure_char( + CHAR_CURRENT_TILT_ANGLE, value=0 + ) + self.char_current_position = serv_cover.configure_char( CHAR_CURRENT_POSITION, value=0 ) @@ -107,6 +133,20 @@ class WindowCovering(HomeAccessory): CHAR_POSITION_STATE, value=2 ) + @debounce + def set_tilt(self, value): + """Set tilt to value if call came from HomeKit.""" + self._homekit_target_tilt = value + _LOGGER.info("%s: Set tilt to %d", self.entity_id, value) + + # HomeKit sends values between -90 and 90. + # We'll have to normalize to [0,100] + value = round((value + 90) / 180.0 * 100.0) + + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_TILT_POSITION: value} + + self.call_service(DOMAIN, SERVICE_SET_COVER_TILT_POSITION, params, value) + @debounce def move_cover(self, value): """Move cover to value if call came from HomeKit.""" @@ -117,14 +157,20 @@ class WindowCovering(HomeAccessory): self.call_service(DOMAIN, SERVICE_SET_COVER_POSITION, params, value) def update_state(self, new_state): - """Update cover position after state changed.""" + """Update cover position and tilt after state changed.""" current_position = new_state.attributes.get(ATTR_CURRENT_POSITION) if isinstance(current_position, (float, int)): current_position = int(current_position) self.char_current_position.set_value(current_position) + + # We have to assume that the device has worse precision than HomeKit. + # If it reports back a state that is only _close_ to HK's requested + # state, we'll "fix" what HomeKit requested so that it won't appear + # out of sync. if ( self._homekit_target is None - or abs(current_position - self._homekit_target) < 6 + or abs(current_position - self._homekit_target) + < DEVICE_PRECISION_LEEWAY ): self.char_target_position.set_value(current_position) self._homekit_target = None @@ -135,6 +181,25 @@ class WindowCovering(HomeAccessory): else: self.char_position_state.set_value(2) + # update tilt + current_tilt = new_state.attributes.get(ATTR_CURRENT_TILT_POSITION) + if isinstance(current_tilt, (float, int)): + # HomeKit sends values between -90 and 90. + # We'll have to normalize to [0,100] + current_tilt = (current_tilt / 100.0 * 180.0) - 90.0 + current_tilt = int(current_tilt) + self.char_current_tilt.set_value(current_tilt) + + # We have to assume that the device has worse precision than HomeKit. + # If it reports back a state that is only _close_ to HK's requested + # state, we'll "fix" what HomeKit requested so that it won't appear + # out of sync. + if self._homekit_target_tilt is None or abs( + current_tilt - self._homekit_target_tilt < DEVICE_PRECISION_LEEWAY + ): + self.char_target_tilt.set_value(current_tilt) + self._homekit_target_tilt = None + @TYPES.register("WindowCoveringBasic") class WindowCoveringBasic(HomeAccessory): diff --git a/tests/components/homekit/test_type_covers.py b/tests/components/homekit/test_type_covers.py index 87d4fbdcc2b..eb7429aa47e 100644 --- a/tests/components/homekit/test_type_covers.py +++ b/tests/components/homekit/test_type_covers.py @@ -5,8 +5,11 @@ import pytest from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + ATTR_CURRENT_TILT_POSITION, ATTR_POSITION, + ATTR_TILT_POSITION, DOMAIN, + SUPPORT_SET_TILT_POSITION, SUPPORT_STOP, ) from homeassistant.components.homekit.const import ATTR_VALUE @@ -14,6 +17,7 @@ from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, EVENT_HOMEASSISTANT_START, + SERVICE_SET_COVER_TILT_POSITION, STATE_CLOSED, STATE_CLOSING, STATE_OPEN, @@ -193,6 +197,72 @@ async def test_window_set_cover_position(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == 75 +async def test_window_cover_set_tilt(hass, hk_driver, cls, events): + """Test if accessory and HA update slat tilt accordingly.""" + entity_id = "cover.window" + + hass.states.async_set( + entity_id, STATE_UNKNOWN, {ATTR_SUPPORTED_FEATURES: SUPPORT_SET_TILT_POSITION} + ) + await hass.async_block_till_done() + acc = cls.window(hass, hk_driver, "Cover", entity_id, 2, None) + await hass.async_add_job(acc.run) + + assert acc.aid == 2 + assert acc.category == 14 # CATEGORY_WINDOW_COVERING + + assert acc.char_current_tilt.value == 0 + assert acc.char_target_tilt.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: None}) + await hass.async_block_till_done() + assert acc.char_current_tilt.value == 0 + assert acc.char_target_tilt.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 100}) + await hass.async_block_till_done() + assert acc.char_current_tilt.value == 90 + assert acc.char_target_tilt.value == 90 + + hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 50}) + await hass.async_block_till_done() + assert acc.char_current_tilt.value == 0 + assert acc.char_target_tilt.value == 0 + + hass.states.async_set(entity_id, STATE_UNKNOWN, {ATTR_CURRENT_TILT_POSITION: 0}) + await hass.async_block_till_done() + assert acc.char_current_tilt.value == -90 + assert acc.char_target_tilt.value == -90 + + # set from HomeKit + call_set_tilt_position = async_mock_service( + hass, DOMAIN, SERVICE_SET_COVER_TILT_POSITION + ) + + # 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_job(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 + assert call_set_tilt_position[0].data[ATTR_TILT_POSITION] == 100 + assert acc.char_current_tilt.value == -90 + assert acc.char_target_tilt.value == 90 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == 100 + + await hass.async_add_job(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 + assert call_set_tilt_position[1].data[ATTR_TILT_POSITION] == 75 + assert acc.char_current_tilt.value == -90 + assert acc.char_target_tilt.value == 45 + assert len(events) == 2 + assert events[-1].data[ATTR_VALUE] == 75 + + async def test_window_open_close(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "cover.window" From 23668f3c5e03a72e32b1c1d2d09c5417eea60e9a Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 30 Mar 2020 21:32:29 -0600 Subject: [PATCH 335/431] Overhaul the Slack integration (async and Block Kit support) (#33287) * Overhaul the Slack integration * Docstring * Empty commit to re-trigger build * Remove remote file option * Remove unused function * Adjust log message * Update homeassistant/components/slack/notify.py Co-Authored-By: Paulus Schoutsen * Code review * Add deprecation warning Co-authored-by: Paulus Schoutsen --- homeassistant/components/slack/manifest.json | 2 +- homeassistant/components/slack/notify.py | 230 +++++++++---------- requirements_all.txt | 2 +- 3 files changed, 108 insertions(+), 126 deletions(-) diff --git a/homeassistant/components/slack/manifest.json b/homeassistant/components/slack/manifest.json index 72e2b0267d2..86785868170 100644 --- a/homeassistant/components/slack/manifest.json +++ b/homeassistant/components/slack/manifest.json @@ -2,7 +2,7 @@ "domain": "slack", "name": "Slack", "documentation": "https://www.home-assistant.io/integrations/slack", - "requirements": ["slacker==0.14.0"], + "requirements": ["slackclient==2.5.0"], "dependencies": [], "codeowners": [] } diff --git a/homeassistant/components/slack/notify.py b/homeassistant/components/slack/notify.py index 20daa261b8f..fe6f7ab0d26 100644 --- a/homeassistant/components/slack/notify.py +++ b/homeassistant/components/slack/notify.py @@ -1,10 +1,11 @@ """Slack platform for notify component.""" +import asyncio import logging +import os +from urllib.parse import urlparse -import requests -from requests.auth import HTTPBasicAuth, HTTPDigestAuth -import slacker -from slacker import Slacker +from slack import WebClient +from slack.errors import SlackApiError import voluptuous as vol from homeassistant.components.notify import ( @@ -15,157 +16,138 @@ from homeassistant.components.notify import ( BaseNotificationService, ) from homeassistant.const import CONF_API_KEY, CONF_ICON, CONF_USERNAME -import homeassistant.helpers.config_validation as cv +from homeassistant.core import callback +from homeassistant.helpers import aiohttp_client, config_validation as cv _LOGGER = logging.getLogger(__name__) -CONF_CHANNEL = "default_channel" -CONF_TIMEOUT = 15 - -# Top level attributes in 'data' ATTR_ATTACHMENTS = "attachments" +ATTR_BLOCKS = "blocks" 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" -# Any other value or absence of 'auth' lead to basic authentication being used -ATTR_FILE_AUTH_DIGEST = "digest" + +CONF_DEFAULT_CHANNEL = "default_channel" + +DEFAULT_TIMEOUT_SECONDS = 15 PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_API_KEY): cv.string, - vol.Required(CONF_CHANNEL): cv.string, + vol.Required(CONF_DEFAULT_CHANNEL): cv.string, vol.Optional(CONF_ICON): cv.string, vol.Optional(CONF_USERNAME): cv.string, } ) -def get_service(hass, config, discovery_info=None): - """Get the Slack notification service.""" - - channel = config.get(CONF_CHANNEL) - api_key = config.get(CONF_API_KEY) - username = config.get(CONF_USERNAME) - icon = config.get(CONF_ICON) +async def async_get_service(hass, config, discovery_info=None): + """Set up the Slack notification service.""" + session = aiohttp_client.async_get_clientsession(hass) + client = WebClient(token=config[CONF_API_KEY], run_async=True, session=session) try: - return SlackNotificationService( - channel, api_key, username, icon, hass.config.is_allowed_path - ) + await client.auth_test() + except SlackApiError as err: + _LOGGER.error("Error while setting up integration: %s", err) + return - except slacker.Error: - _LOGGER.exception("Authentication failed") - return None + return SlackNotificationService( + hass, + client, + config[CONF_DEFAULT_CHANNEL], + username=config.get(CONF_USERNAME), + icon=config.get(CONF_ICON), + ) + + +@callback +def _async_sanitize_channel_names(channel_list): + """Remove any # symbols from a channel list.""" + return [channel.lstrip("#") for channel in channel_list] class SlackNotificationService(BaseNotificationService): - """Implement the notification service for Slack.""" - - def __init__(self, default_channel, api_token, username, icon, is_allowed_path): - """Initialize the service.""" + """Define the Slack notification logic.""" + def __init__(self, hass, client, default_channel, username, icon): + """Initialize.""" + self._client = client self._default_channel = default_channel - self._api_token = api_token - self._username = username + self._hass = hass self._icon = icon - if self._username or self._icon: + + if username or self._icon: self._as_user = False else: self._as_user = True - self.is_allowed_path = is_allowed_path - self.slack = Slacker(self._api_token) - self.slack.auth.test() + async def _async_send_local_file_message(self, path, targets, message, title): + """Upload a local file (with message) to Slack.""" + if not self._hass.config.is_allowed_path(path): + _LOGGER.error("Path does not exist or is not allowed: %s", path) + return - def send_message(self, message="", **kwargs): - """Send a message to a user.""" + parsed_url = urlparse(path) + filename = os.path.basename(parsed_url.path) - if kwargs.get(ATTR_TARGET) is None: - targets = [self._default_channel] - else: - targets = kwargs.get(ATTR_TARGET) - - data = kwargs.get(ATTR_DATA) - attachments = data.get(ATTR_ATTACHMENTS) if data else None - file = data.get(ATTR_FILE) if data else None - title = kwargs.get(ATTR_TITLE) - - for target in targets: - try: - if file is not None: - # Load from file or URL - file_as_bytes = self.load_file( - url=file.get(ATTR_FILE_URL), - local_path=file.get(ATTR_FILE_PATH), - username=file.get(ATTR_FILE_USERNAME), - password=file.get(ATTR_FILE_PASSWORD), - auth=file.get(ATTR_FILE_AUTH), - ) - # Choose filename - if file.get(ATTR_FILE_URL): - filename = file.get(ATTR_FILE_URL) - else: - filename = file.get(ATTR_FILE_PATH) - # Prepare structure for Slack API - data = { - "content": None, - "filetype": None, - "filename": filename, - # If optional title is none use the filename - "title": title if title else filename, - "initial_comment": message, - "channels": target, - } - # Post to slack - self.slack.files.post( - "files.upload", data=data, files={"file": file_as_bytes} - ) - else: - self.slack.chat.post_message( - target, - message, - as_user=self._as_user, - username=self._username, - icon_emoji=self._icon, - attachments=attachments, - link_names=True, - ) - except slacker.Error as err: - _LOGGER.error("Could not send notification. Error: %s", err) - - def load_file( - self, url=None, local_path=None, username=None, password=None, auth=None - ): - """Load image/document/etc from a local path or URL.""" try: - if url: - # Check whether authentication parameters are provided - if username: - # Use digest or basic authentication - if ATTR_FILE_AUTH_DIGEST == auth: - auth_ = HTTPDigestAuth(username, password) - else: - auth_ = HTTPBasicAuth(username, password) - # Load file from URL with authentication - req = requests.get(url, auth=auth_, timeout=CONF_TIMEOUT) - else: - # Load file from URL without authentication - req = requests.get(url, timeout=CONF_TIMEOUT) - return req.content + await self._client.files_upload( + channels=",".join(targets), + file=path, + filename=filename, + initial_comment=message, + title=title or filename, + ) + except SlackApiError as err: + _LOGGER.error("Error while uploading file-based message: %s", err) - if local_path: - # Check whether path is whitelisted in configuration.yaml - if self.is_allowed_path(local_path): - return open(local_path, "rb") - _LOGGER.warning("'%s' is not secure to load data from!", local_path) - else: - _LOGGER.warning("Neither URL nor local path found in parameters!") + async def _async_send_text_only_message( + self, targets, message, title, attachments, blocks + ): + """Send a text-only message.""" + tasks = { + target: self._client.chat_postMessage( + channel=target, + text=message, + as_user=self._as_user, + attachments=attachments, + blocks=blocks, + icon_emoji=self._icon, + link_names=True, + ) + for target in targets + } - except OSError as error: - _LOGGER.error("Can't load from URL or local path: %s", error) + results = await asyncio.gather(*tasks.values(), return_exceptions=True) + for target, result in zip(tasks, results): + if isinstance(result, SlackApiError): + _LOGGER.error( + "There was a Slack API error while sending to %s: %s", + target, + result, + ) - return None + async def async_send_message(self, message, **kwargs): + """Send a message to Slack.""" + data = kwargs[ATTR_DATA] or {} + title = kwargs.get(ATTR_TITLE) + targets = _async_sanitize_channel_names( + kwargs.get(ATTR_TARGET, [self._default_channel]) + ) + + if ATTR_FILE in data: + return await self._async_send_local_file_message( + data[ATTR_FILE], targets, message, title + ) + + attachments = data.get(ATTR_ATTACHMENTS, {}) + if attachments: + _LOGGER.warning( + "Attachments are deprecated and part of Slack's legacy API; support " + "for them will be dropped in 0.114.0. In most cases, Blocks should be " + "used instead: https://www.home-assistant.io/integrations/slack/" + ) + blocks = data.get(ATTR_BLOCKS, {}) + + return await self._async_send_text_only_message( + targets, message, title, attachments, blocks + ) diff --git a/requirements_all.txt b/requirements_all.txt index 3063b5d1030..46fd27fa1fd 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1879,7 +1879,7 @@ sisyphus-control==2.2.1 skybellpy==0.4.0 # homeassistant.components.slack -slacker==0.14.0 +slackclient==2.5.0 # homeassistant.components.sleepiq sleepyq==0.7 From f4eb1f06520d36def43953598558dba77dbefe06 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Tue, 31 Mar 2020 11:31:43 +0200 Subject: [PATCH 336/431] Update issue templates (#33434) * Update issue templates * Process review suggestions --- .github/ISSUE_TEMPLATE.md | 49 ++++++++++++++++++++++++++++ .github/ISSUE_TEMPLATE/BUG_REPORT.md | 14 ++++---- 2 files changed, 56 insertions(+), 7 deletions(-) create mode 100644 .github/ISSUE_TEMPLATE.md diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md new file mode 100644 index 00000000000..713c7dc2872 --- /dev/null +++ b/.github/ISSUE_TEMPLATE.md @@ -0,0 +1,49 @@ + +## The problem + + + +## Environment + + +- Home Assistant Core release with the issue: +- Last working Home Assistant Core release (if known): +- Operating environment (Home Assistant/Supervised/Docker/venv): +- Integration causing this issue: +- Link to integration documentation on our website: + +## Problem-relevant `configuration.yaml` + + +```yaml + +``` + +## Traceback/Error logs + + +```txt + +``` + +## Additional information + diff --git a/.github/ISSUE_TEMPLATE/BUG_REPORT.md b/.github/ISSUE_TEMPLATE/BUG_REPORT.md index 977abc6ef6b..9bfecda724f 100644 --- a/.github/ISSUE_TEMPLATE/BUG_REPORT.md +++ b/.github/ISSUE_TEMPLATE/BUG_REPORT.md @@ -1,10 +1,10 @@ --- -name: Report a bug with Home Assistant -about: Report an issue with Home Assistant +name: Report a bug with Home Assistant Core +about: Report an issue with Home Assistant Core --- @@ -23,9 +23,9 @@ about: Report an issue with Home Assistant Home Assistant frontend: Developer tools -> Info. --> -- Home Assistant release with the issue: -- Last working Home Assistant release (if known): -- Operating environment (Hass.io/Docker/Windows/etc.): +- Home Assistant Core release with the issue: +- Last working Home Assistant Core release (if known): +- Operating environment (Home Assistant/Supervised/Docker/venv): - Integration causing this issue: - Link to integration documentation on our website: From ffefdcfe228d3978b19d5a7f32ca4ba1c97df0db Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 31 Mar 2020 11:43:21 +0200 Subject: [PATCH 337/431] Require admin to manage lovelace (#33435) --- homeassistant/components/lovelace/websocket.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/homeassistant/components/lovelace/websocket.py b/homeassistant/components/lovelace/websocket.py index a4e67fda929..45a042c1f2e 100644 --- a/homeassistant/components/lovelace/websocket.py +++ b/homeassistant/components/lovelace/websocket.py @@ -72,6 +72,7 @@ async def websocket_lovelace_config(hass, connection, msg, config): return await config.async_load(msg["force"]) +@websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( { @@ -86,6 +87,7 @@ async def websocket_lovelace_save_config(hass, connection, msg, config): await config.async_save(msg["config"]) +@websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( { From 977f1a691612e90e96deab1640005c33b32f6cf9 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 31 Mar 2020 17:17:09 +0200 Subject: [PATCH 338/431] Fix hue tests that have uncaught exceptions (#33443) --- tests/components/hue/test_bridge.py | 2 +- tests/components/hue/test_init.py | 10 ++++------ tests/components/hue/test_light.py | 4 ++-- tests/ignore_uncaught_exceptions.py | 3 --- 4 files changed, 7 insertions(+), 12 deletions(-) diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 03966560d8d..6ac68d222eb 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -99,7 +99,7 @@ async def test_reset_unloads_entry_if_setup(hass): async def test_handle_unauthorized(hass): """Test handling an unauthorized error on update.""" - entry = Mock() + entry = Mock(async_setup=Mock(return_value=mock_coro(Mock()))) entry.data = {"host": "1.2.3.4", "username": "mock-username"} hue_bridge = bridge.HueBridge(hass, entry, False, False) diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index d9131dad226..51ea3f2ae71 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -193,17 +193,15 @@ async def test_security_vuln_check(hass): entry = MockConfigEntry(domain=hue.DOMAIN, data={"host": "0.0.0.0"}) entry.add_to_hass(hass) + config = Mock(bridgeid="", mac="", modelid="BSB002", swversion="1935144020") + config.name = "Hue" + with patch.object( hue, "HueBridge", Mock( return_value=Mock( - async_setup=CoroutineMock(return_value=True), - api=Mock( - config=Mock( - bridgeid="", mac="", modelid="BSB002", swversion="1935144020" - ) - ), + async_setup=CoroutineMock(return_value=True), api=Mock(config=config) ) ), ): diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index 72546891a63..a99b947e48e 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -893,7 +893,7 @@ async def test_group_features(hass, mock_bridge): "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", - "uniqueid": "456", + "uniqueid": "4567", } light_3 = { "state": { @@ -945,7 +945,7 @@ async def test_group_features(hass, mock_bridge): "modelid": "LCT001", "swversion": "66009461", "manufacturername": "Philips", - "uniqueid": "123", + "uniqueid": "1234", } light_response = { "1": light_1, diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 58531b251e0..1a37f2d0f54 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -52,9 +52,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"), ("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"), ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), - ("tests.components.hue.test_bridge", "test_handle_unauthorized"), - ("tests.components.hue.test_init", "test_security_vuln_check"), - ("tests.components.hue.test_light", "test_group_features"), ("tests.components.ios.test_init", "test_creating_entry_sets_up_sensor"), ("tests.components.ios.test_init", "test_not_configuring_ios_not_creates_entry"), ("tests.components.local_file.test_camera", "test_file_not_readable"), From f2f03cf55255ea03e3680387ed96f6808e09c64a Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 31 Mar 2020 18:19:34 +0200 Subject: [PATCH 339/431] Fix deconz tests that have uncaught exceptions (#33462) --- homeassistant/components/deconz/deconz_device.py | 7 ++++--- homeassistant/components/deconz/gateway.py | 4 +++- tests/ignore_uncaught_exceptions.py | 5 ----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/deconz/deconz_device.py b/homeassistant/components/deconz/deconz_device.py index b3dedf6cf00..0724f9f9b45 100644 --- a/homeassistant/components/deconz/deconz_device.py +++ b/homeassistant/components/deconz/deconz_device.py @@ -86,9 +86,10 @@ class DeconzDevice(DeconzBase, Entity): async def async_will_remove_from_hass(self) -> None: """Disconnect device object when removed.""" self._device.remove_callback(self.async_update_callback) - del self.gateway.deconz_ids[self.entity_id] - for unsub_dispatcher in self.listeners: - unsub_dispatcher() + if self.entity_id in self.gateway.deconz_ids: + del self.gateway.deconz_ids[self.entity_id] + for unsub_dispatcher in self.listeners: + unsub_dispatcher() async def async_remove_self(self, deconz_ids: list) -> None: """Schedule removal of this entity. diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index b59c80a0dc8..eb83f5c15c5 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -31,7 +31,7 @@ from .errors import AuthenticationRequired, CannotConnect @callback def get_gateway_from_config_entry(hass, config_entry): """Return gateway with a matching bridge id.""" - return hass.data[DOMAIN][config_entry.unique_id] + return hass.data[DOMAIN].get(config_entry.unique_id) class DeconzGateway: @@ -126,6 +126,8 @@ class DeconzGateway: Causes for this is either discovery updating host address or config entry options changing. """ gateway = get_gateway_from_config_entry(hass, entry) + if not gateway: + return if gateway.api.host != entry.data[CONF_HOST]: gateway.api.close() gateway.api.host = entry.data[CONF_HOST] diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 1a37f2d0f54..26170ac2b86 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -6,11 +6,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.cast.test_media_player", "test_entry_setup_platform_not_ready"), ("tests.components.config.test_automation", "test_delete_automation"), ("tests.components.config.test_group", "test_update_device_config"), - ("tests.components.deconz.test_binary_sensor", "test_allow_clip_sensor"), - ("tests.components.deconz.test_climate", "test_clip_climate_device"), - ("tests.components.deconz.test_init", "test_unload_entry_multiple_gateways"), - ("tests.components.deconz.test_light", "test_disable_light_groups"), - ("tests.components.deconz.test_sensor", "test_allow_clip_sensors"), ("tests.components.default_config.test_init", "test_setup"), ("tests.components.demo.test_init", "test_setting_up_demo"), ("tests.components.discovery.test_init", "test_discover_config_flow"), From dd1608db0dfe0d140f9c5e92f57e97df5d87e46d Mon Sep 17 00:00:00 2001 From: Jeff McGehee Date: Tue, 31 Mar 2020 12:41:29 -0400 Subject: [PATCH 340/431] Best effort state initialization of bayesian binary sensor (#30962) * Best effort state initialization of bayesian binary sensor. Why: * https://github.com/home-assistant/home-assistant/issues/30119 This change addresses the need by: * Running the main update logic eagerly for each entity being observed on `async_added_to_hass`. * Test of the new behavior. * Refactor in order to reduce number of methods with side effects that mutate instance attributes. * Improve test coverage Why: * Because for some reason my commits decreased test coverage. This change addresses the need by: * Adding coverage for the case where a device returns `STATE_UNKNOWN` * Adding coverage for configurations with templates * rebase and ensure upstream tests passed * Delete commented code from addressing merge conflict. --- .../components/bayesian/binary_sensor.py | 176 +++++++++++------- .../components/bayesian/test_binary_sensor.py | 152 +++++++++++++-- 2 files changed, 240 insertions(+), 88 deletions(-) diff --git a/homeassistant/components/bayesian/binary_sensor.py b/homeassistant/components/bayesian/binary_sensor.py index c2e9e220a20..b922c966ff5 100644 --- a/homeassistant/components/bayesian/binary_sensor.py +++ b/homeassistant/components/bayesian/binary_sensor.py @@ -1,6 +1,5 @@ """Use Bayesian Inference to trigger a binary sensor.""" from collections import OrderedDict -from itertools import chain import voluptuous as vol @@ -88,10 +87,10 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) -def update_probability(prior, prob_true, prob_false): +def update_probability(prior, prob_given_true, prob_given_false): """Update probability using Bayes' rule.""" - numerator = prob_true * prior - denominator = numerator + prob_false * (1 - prior) + numerator = prob_given_true * prior + denominator = numerator + prob_given_false * (1 - prior) probability = numerator / denominator return probability @@ -127,84 +126,124 @@ class BayesianBinarySensor(BinarySensorDevice): self.prior = prior self.probability = prior - self.current_obs = OrderedDict({}) - self.entity_obs_dict = [] + self.current_observations = OrderedDict({}) - for obs in self._observations: - if "entity_id" in obs: - self.entity_obs_dict.append([obs.get("entity_id")]) - if "value_template" in obs: - self.entity_obs_dict.append( - list(obs.get(CONF_VALUE_TEMPLATE).extract_entities()) - ) + self.observations_by_entity = self._build_observations_by_entity() - to_observe = set() - for obs in self._observations: - if "entity_id" in obs: - to_observe.update(set([obs.get("entity_id")])) - if "value_template" in obs: - to_observe.update(set(obs.get(CONF_VALUE_TEMPLATE).extract_entities())) - self.entity_obs = {key: [] for key in to_observe} - - for ind, obs in enumerate(self._observations): - obs["id"] = ind - if "entity_id" in obs: - self.entity_obs[obs["entity_id"]].append(obs) - if "value_template" in obs: - for ent in obs.get(CONF_VALUE_TEMPLATE).extract_entities(): - self.entity_obs[ent].append(obs) - - self.watchers = { + self.observation_handlers = { "numeric_state": self._process_numeric_state, "state": self._process_state, "template": self._process_template, } async def async_added_to_hass(self): - """Call when entity about to be added.""" + """ + Call when entity about to be added. + + All relevant update logic for instance attributes occurs within this closure. + Other methods in this class are designed to avoid directly modifying instance + attributes, by instead focusing on returning relevant data back to this method. + + The goal of this method is to ensure that `self.current_observations` and `self.probability` + are set on a best-effort basis when this entity is register with hass. + + In addition, this method must register the state listener defined within, which + will be called any time a relevant entity changes its state. + """ @callback - def async_threshold_sensor_state_listener(entity, old_state, new_state): - """Handle sensor state changes.""" + def async_threshold_sensor_state_listener(entity, _old_state, new_state): + """ + Handle sensor state changes. + + When a state changes, we must update our list of current observations, + then calculate the new probability. + """ if new_state.state == STATE_UNKNOWN: return - entity_obs_list = self.entity_obs[entity] - - for entity_obs in entity_obs_list: - platform = entity_obs["platform"] - - self.watchers[platform](entity_obs) - - prior = self.prior - for obs in self.current_obs.values(): - prior = update_probability(prior, obs["prob_true"], obs["prob_false"]) - self.probability = prior + self.current_observations.update(self._record_entity_observations(entity)) + self.probability = self._calculate_new_probability() self.hass.async_add_job(self.async_update_ha_state, True) + self.current_observations.update(self._initialize_current_observations()) + self.probability = self._calculate_new_probability() async_track_state_change( - self.hass, self.entity_obs, async_threshold_sensor_state_listener + self.hass, + self.observations_by_entity, + async_threshold_sensor_state_listener, ) - def _update_current_obs(self, entity_observation, should_trigger): - """Update current observation.""" - obs_id = entity_observation["id"] + def _initialize_current_observations(self): + local_observations = OrderedDict({}) + for entity in self.observations_by_entity: + local_observations.update(self._record_entity_observations(entity)) + return local_observations - if should_trigger: - prob_true = entity_observation["prob_given_true"] - prob_false = entity_observation.get("prob_given_false", 1 - prob_true) + def _record_entity_observations(self, entity): + local_observations = OrderedDict({}) + entity_obs_list = self.observations_by_entity[entity] - self.current_obs[obs_id] = { - "prob_true": prob_true, - "prob_false": prob_false, - } + for entity_obs in entity_obs_list: + platform = entity_obs["platform"] - else: - self.current_obs.pop(obs_id, None) + should_trigger = self.observation_handlers[platform](entity_obs) + + if should_trigger: + obs_entry = {"entity_id": entity, **entity_obs} + else: + obs_entry = None + + local_observations[entity_obs["id"]] = obs_entry + + return local_observations + + def _calculate_new_probability(self): + prior = self.prior + + for obs in self.current_observations.values(): + if obs is not None: + prior = update_probability( + prior, + obs["prob_given_true"], + obs.get("prob_given_false", 1 - obs["prob_given_true"]), + ) + + return prior + + def _build_observations_by_entity(self): + """ + Build and return data structure of the form below. + + { + "sensor.sensor1": [{"id": 0, ...}, {"id": 1, ...}], + "sensor.sensor2": [{"id": 2, ...}], + ... + } + + Each "observation" must be recognized uniquely, and it should be possible + for all relevant observations to be looked up via their `entity_id`. + """ + + observations_by_entity = {} + for ind, obs in enumerate(self._observations): + obs["id"] = ind + + if "entity_id" in obs: + entity_ids = [obs["entity_id"]] + elif "value_template" in obs: + entity_ids = obs.get(CONF_VALUE_TEMPLATE).extract_entities() + + for e_id in entity_ids: + obs_list = observations_by_entity.get(e_id, []) + obs_list.append(obs) + observations_by_entity[e_id] = obs_list + + return observations_by_entity def _process_numeric_state(self, entity_observation): - """Add entity to current_obs if numeric state conditions are met.""" + """Return True if numeric condition is met.""" entity = entity_observation["entity_id"] should_trigger = condition.async_numeric_state( @@ -215,27 +254,26 @@ class BayesianBinarySensor(BinarySensorDevice): None, entity_observation, ) - - self._update_current_obs(entity_observation, should_trigger) + return should_trigger def _process_state(self, entity_observation): - """Add entity to current observations if state conditions are met.""" + """Return True if state conditions are met.""" entity = entity_observation["entity_id"] should_trigger = condition.state( self.hass, entity, entity_observation.get("to_state") ) - self._update_current_obs(entity_observation, should_trigger) + return should_trigger def _process_template(self, entity_observation): - """Add entity to current_obs if template is true.""" + """Return True if template condition is True.""" template = entity_observation.get(CONF_VALUE_TEMPLATE) template.hass = self.hass should_trigger = condition.async_template( self.hass, template, entity_observation ) - self._update_current_obs(entity_observation, should_trigger) + return should_trigger @property def name(self): @@ -260,13 +298,15 @@ class BayesianBinarySensor(BinarySensorDevice): @property def device_state_attributes(self): """Return the state attributes of the sensor.""" + print(self.current_observations) + print(self.observations_by_entity) return { - ATTR_OBSERVATIONS: list(self.current_obs.values()), + ATTR_OBSERVATIONS: list(self.current_observations.values()), ATTR_OCCURRED_OBSERVATION_ENTITIES: list( set( - chain.from_iterable( - self.entity_obs_dict[obs] for obs in self.current_obs.keys() - ) + obs.get("entity_id") + for obs in self.current_observations.values() + if obs is not None ) ), ATTR_PROBABILITY: round(self.probability, 2), diff --git a/tests/components/bayesian/test_binary_sensor.py b/tests/components/bayesian/test_binary_sensor.py index fb9bc7d5e5c..495c8a63a0b 100644 --- a/tests/components/bayesian/test_binary_sensor.py +++ b/tests/components/bayesian/test_binary_sensor.py @@ -2,6 +2,7 @@ import unittest from homeassistant.components.bayesian import binary_sensor as bayesian +from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import setup_component from tests.common import get_test_home_assistant @@ -18,6 +19,65 @@ class TestBayesianBinarySensor(unittest.TestCase): """Stop everything that was started.""" self.hass.stop() + def test_load_values_when_added_to_hass(self): + """Test that sensor initializes with observations of relevant entities.""" + + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "off", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + } + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + self.hass.states.set("sensor.test_monitored", "off") + self.hass.block_till_done() + + assert setup_component(self.hass, "binary_sensor", config) + + state = self.hass.states.get("binary_sensor.test_binary") + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 + assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 + + def test_unknown_state_does_not_influence_probability(self): + """Test that an unknown state does not change the output probability.""" + + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "state", + "entity_id": "sensor.test_monitored", + "to_state": "off", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + } + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + self.hass.states.set("sensor.test_monitored", STATE_UNKNOWN) + self.hass.block_till_done() + + assert setup_component(self.hass, "binary_sensor", config) + + state = self.hass.states.get("binary_sensor.test_binary") + assert state.attributes.get("observations") == [None] + def test_sensor_numeric_state(self): """Test sensor on numeric state platform observations.""" config = { @@ -52,7 +112,7 @@ class TestBayesianBinarySensor(unittest.TestCase): state = self.hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("observations") + assert [None, None] == state.attributes.get("observations") assert 0.2 == state.attributes.get("probability") assert state.state == "off" @@ -66,10 +126,9 @@ class TestBayesianBinarySensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_binary") - assert [ - {"prob_false": 0.4, "prob_true": 0.6}, - {"prob_false": 0.1, "prob_true": 0.9}, - ] == state.attributes.get("observations") + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.6 + assert state.attributes.get("observations")[1]["prob_given_true"] == 0.9 + assert state.attributes.get("observations")[1]["prob_given_false"] == 0.1 assert round(abs(0.77 - state.attributes.get("probability")), 7) == 0 assert state.state == "on" @@ -118,7 +177,7 @@ class TestBayesianBinarySensor(unittest.TestCase): state = self.hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("observations") + assert [None] == state.attributes.get("observations") assert 0.2 == state.attributes.get("probability") assert state.state == "off" @@ -131,9 +190,62 @@ class TestBayesianBinarySensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_binary") - assert [{"prob_true": 0.8, "prob_false": 0.4}] == state.attributes.get( - "observations" - ) + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 + assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 + assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0 + + assert state.state == "on" + + self.hass.states.set("sensor.test_monitored", "off") + self.hass.block_till_done() + self.hass.states.set("sensor.test_monitored", "on") + self.hass.block_till_done() + + state = self.hass.states.get("binary_sensor.test_binary") + assert round(abs(0.2 - state.attributes.get("probability")), 7) == 0 + + assert state.state == "off" + + def test_sensor_value_template(self): + """Test sensor on template platform observations.""" + config = { + "binary_sensor": { + "name": "Test_Binary", + "platform": "bayesian", + "observations": [ + { + "platform": "template", + "value_template": "{{states('sensor.test_monitored') == 'off'}}", + "prob_given_true": 0.8, + "prob_given_false": 0.4, + } + ], + "prior": 0.2, + "probability_threshold": 0.32, + } + } + + assert setup_component(self.hass, "binary_sensor", config) + + self.hass.states.set("sensor.test_monitored", "on") + + state = self.hass.states.get("binary_sensor.test_binary") + + assert [None] == state.attributes.get("observations") + assert 0.2 == state.attributes.get("probability") + + assert state.state == "off" + + self.hass.states.set("sensor.test_monitored", "off") + self.hass.block_till_done() + self.hass.states.set("sensor.test_monitored", "on") + self.hass.block_till_done() + self.hass.states.set("sensor.test_monitored", "off") + self.hass.block_till_done() + + state = self.hass.states.get("binary_sensor.test_binary") + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 + assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0 assert state.state == "on" @@ -210,7 +322,7 @@ class TestBayesianBinarySensor(unittest.TestCase): state = self.hass.states.get("binary_sensor.test_binary") - assert [] == state.attributes.get("observations") + assert [None, None] == state.attributes.get("observations") assert 0.2 == state.attributes.get("probability") assert state.state == "off" @@ -223,9 +335,9 @@ class TestBayesianBinarySensor(unittest.TestCase): self.hass.block_till_done() state = self.hass.states.get("binary_sensor.test_binary") - assert [{"prob_true": 0.8, "prob_false": 0.4}] == state.attributes.get( - "observations" - ) + + assert state.attributes.get("observations")[0]["prob_given_true"] == 0.8 + assert state.attributes.get("observations")[0]["prob_given_false"] == 0.4 assert round(abs(0.33 - state.attributes.get("probability")), 7) == 0 assert state.state == "on" @@ -242,20 +354,20 @@ class TestBayesianBinarySensor(unittest.TestCase): def test_probability_updates(self): """Test probability update function.""" - prob_true = [0.3, 0.6, 0.8] - prob_false = [0.7, 0.4, 0.2] + prob_given_true = [0.3, 0.6, 0.8] + prob_given_false = [0.7, 0.4, 0.2] prior = 0.5 - for pt, pf in zip(prob_true, prob_false): + for pt, pf in zip(prob_given_true, prob_given_false): prior = bayesian.update_probability(prior, pt, pf) assert round(abs(0.720000 - prior), 7) == 0 - prob_true = [0.8, 0.3, 0.9] - prob_false = [0.6, 0.4, 0.2] + prob_given_true = [0.8, 0.3, 0.9] + prob_given_false = [0.6, 0.4, 0.2] prior = 0.7 - for pt, pf in zip(prob_true, prob_false): + for pt, pf in zip(prob_given_true, prob_given_false): prior = bayesian.update_probability(prior, pt, pf) assert round(abs(0.9130434782608695 - prior), 7) == 0 @@ -271,7 +383,7 @@ class TestBayesianBinarySensor(unittest.TestCase): "platform": "state", "entity_id": "sensor.test_monitored", "to_state": "off", - "prob_given_true": 0.8, + "prob_given_true": 0.9, "prob_given_false": 0.4, }, { From f5cbc9d208e93a1b357f466e526c6bc66912b78c Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Tue, 31 Mar 2020 19:27:30 +0200 Subject: [PATCH 341/431] Fire events for hue remote buttons pressed (#33277) * Add remote platform to hue integration supporting ZGPSwitch, ZLLSwitch and ZLLRotary switches. * Ported from custom component Hue-remotes-HASS from @robmarkcole * Add options flow for hue, to toggle handling of sensors and remotes * Sensors are enabled by default, and remotes are disabled, to not generate any breaking change for existent users. Also, when linking a new bridge these defaults are used, so unless going explicitly to the Options menu, the old behavior is preserved. * SensorManager stores the enabled platforms and ignores everything else. * Bridge is created with flags for `add_sensors` and `add_remotes`, and uses them to forward entry setup to only the enabled platforms. * Update listener removes disabled kinds of devices when options are changed, so device list is in sync with options, and disabled kinds disappear from HA, leaving the enable/disable entity option for individual devices. * Fix hue bridge mock with new parameters * Revert changes in hue bridge mock * Remove OptionsFlow and platform flags * Extract `GenericHueDevice` from `GenericHueSensor` to use it as base class for all hue devices, including those without any entity, like remotes without battery. * Add `HueBattery` sensor for battery powered remotes and generate entities for TYPE_ZLL_ROTARY and TYPE_ZLL_SWITCH remotes. * Remove remote platform * Add HueEvent class to fire events for button presses * Use `sensor.lastupdated` string to control state changes * Event data includes: - "id", as pretty name of the remote - "unique_id" of the remote device - "event", with the raw code of the pressed button ('buttonevent' or 'rotaryevent' property) - "last_updated", with the bridge timestamp for the button press * Register ZGP_SWITCH, ZLL_SWITCH, ZLL_ROTARY remotes * fix removal * Exclude W0611 * Extract GenericHueDevice to its own module and solve import tree, also fixing lint in CI * Store registered events to do not repeat device reg * Minor cleaning * Add tests for hue_event and battery entities for hue remotes --- homeassistant/components/hue/hue_event.py | 93 ++++++++++ homeassistant/components/hue/sensor.py | 46 ++++- homeassistant/components/hue/sensor_base.py | 91 ++++----- homeassistant/components/hue/sensor_device.py | 53 ++++++ tests/components/hue/test_sensor_base.py | 175 ++++++++++++++++-- 5 files changed, 385 insertions(+), 73 deletions(-) create mode 100644 homeassistant/components/hue/hue_event.py create mode 100644 homeassistant/components/hue/sensor_device.py diff --git a/homeassistant/components/hue/hue_event.py b/homeassistant/components/hue/hue_event.py new file mode 100644 index 00000000000..838d5ead6da --- /dev/null +++ b/homeassistant/components/hue/hue_event.py @@ -0,0 +1,93 @@ +"""Representation of a Hue remote firing events for button presses.""" +import logging + +from aiohue.sensors import TYPE_ZGP_SWITCH, TYPE_ZLL_ROTARY, TYPE_ZLL_SWITCH + +from homeassistant.const import CONF_EVENT, CONF_ID +from homeassistant.core import callback +from homeassistant.util import slugify + +from .sensor_device import GenericHueDevice + +_LOGGER = logging.getLogger(__name__) + +CONF_HUE_EVENT = "hue_event" +CONF_LAST_UPDATED = "last_updated" +CONF_UNIQUE_ID = "unique_id" + +EVENT_NAME_FORMAT = "{}" + + +class HueEvent(GenericHueDevice): + """When you want signals instead of entities. + + Stateless sensors such as remotes are expected to generate an event + instead of a sensor entity in hass. + """ + + def __init__(self, sensor, name, bridge, primary_sensor=None): + """Register callback that will be used for signals.""" + super().__init__(sensor, name, bridge, primary_sensor) + + self.event_id = slugify(self.sensor.name) + # Use the 'lastupdated' string to detect new remote presses + self._last_updated = self.sensor.lastupdated + + # Register callback in coordinator and add job to remove it on bridge reset. + self.bridge.sensor_manager.coordinator.async_add_listener( + self.async_update_callback + ) + self.bridge.reset_jobs.append(self.async_will_remove_from_hass) + _LOGGER.debug("Hue event created: %s", self.event_id) + + @callback + def async_will_remove_from_hass(self): + """Remove listener on bridge reset.""" + self.bridge.sensor_manager.coordinator.async_remove_listener( + self.async_update_callback + ) + + @callback + def async_update_callback(self): + """Fire the event if reason is that state is updated.""" + if self.sensor.lastupdated == self._last_updated: + return + + # Extract the press code as state + if hasattr(self.sensor, "rotaryevent"): + state = self.sensor.rotaryevent + else: + state = self.sensor.buttonevent + + self._last_updated = self.sensor.lastupdated + + # Fire event + data = { + CONF_ID: self.event_id, + CONF_UNIQUE_ID: self.unique_id, + CONF_EVENT: state, + CONF_LAST_UPDATED: self.sensor.lastupdated, + } + self.bridge.hass.bus.async_fire(CONF_HUE_EVENT, data) + + async def async_update_device_registry(self): + """Update device registry.""" + device_registry = ( + await self.bridge.hass.helpers.device_registry.async_get_registry() + ) + + entry = device_registry.async_get_or_create( + config_entry_id=self.bridge.config_entry.entry_id, **self.device_info + ) + _LOGGER.debug( + "Event registry with entry_id: %s and device_id: %s", + entry.id, + self.device_id, + ) + + +EVENT_CONFIG_MAP = { + TYPE_ZGP_SWITCH: {"name_format": EVENT_NAME_FORMAT, "class": HueEvent}, + TYPE_ZLL_SWITCH: {"name_format": EVENT_NAME_FORMAT, "class": HueEvent}, + TYPE_ZLL_ROTARY: {"name_format": EVENT_NAME_FORMAT, "class": HueEvent}, +} diff --git a/homeassistant/components/hue/sensor.py b/homeassistant/components/hue/sensor.py index 61acd097b01..0da8e77eeee 100644 --- a/homeassistant/components/hue/sensor.py +++ b/homeassistant/components/hue/sensor.py @@ -1,17 +1,25 @@ """Hue sensor entities.""" -from aiohue.sensors import TYPE_ZLL_LIGHTLEVEL, TYPE_ZLL_TEMPERATURE +from aiohue.sensors import ( + TYPE_ZLL_LIGHTLEVEL, + TYPE_ZLL_ROTARY, + TYPE_ZLL_SWITCH, + TYPE_ZLL_TEMPERATURE, +) from homeassistant.const import ( + DEVICE_CLASS_BATTERY, DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_TEMPERATURE, TEMP_CELSIUS, + UNIT_PERCENTAGE, ) from homeassistant.helpers.entity import Entity from .const import DOMAIN as HUE_DOMAIN -from .sensor_base import SENSOR_CONFIG_MAP, GenericZLLSensor +from .sensor_base import SENSOR_CONFIG_MAP, GenericHueSensor, GenericZLLSensor LIGHT_LEVEL_NAME_FORMAT = "{} light level" +REMOTE_NAME_FORMAT = "{} battery level" TEMPERATURE_NAME_FORMAT = "{} temperature" @@ -79,6 +87,30 @@ class HueTemperature(GenericHueGaugeSensorEntity): return self.sensor.temperature / 100 +class HueBattery(GenericHueSensor): + """Battery class for when a batt-powered device is only represented as an event.""" + + @property + def unique_id(self): + """Return a unique identifier for this device.""" + return f"{self.sensor.uniqueid}-battery" + + @property + def state(self): + """Return the state of the battery.""" + return self.sensor.battery + + @property + def device_class(self): + """Return the class of the sensor.""" + return DEVICE_CLASS_BATTERY + + @property + def unit_of_measurement(self): + """Return the unit of measurement of this entity.""" + return UNIT_PERCENTAGE + + SENSOR_CONFIG_MAP.update( { TYPE_ZLL_LIGHTLEVEL: { @@ -91,5 +123,15 @@ SENSOR_CONFIG_MAP.update( "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/sensor_base.py index 9596d7457aa..113957d140e 100644 --- a/homeassistant/components/hue/sensor_base.py +++ b/homeassistant/components/hue/sensor_base.py @@ -10,8 +10,10 @@ from homeassistant.core import callback from homeassistant.helpers import debounce, entity from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed -from .const import DOMAIN as HUE_DOMAIN, 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 = {} _LOGGER = logging.getLogger(__name__) @@ -38,6 +40,9 @@ class SensorManager: self.bridge = bridge self._component_add_entities = {} self.current = {} + self.current_events = {} + + self._enabled_platforms = ("binary_sensor", "sensor") self.coordinator = DataUpdateCoordinator( bridge.hass, _LOGGER, @@ -66,7 +71,8 @@ class SensorManager: """Register async_add_entities methods for components.""" self._component_add_entities[platform] = async_add_entities - if len(self._component_add_entities) < 2: + if len(self._component_add_entities) < len(self._enabled_platforms): + _LOGGER.debug("Aborting start with %s, waiting for the rest", platform) return # We have all components available, start the updating. @@ -81,7 +87,7 @@ class SensorManager: """Update sensors from the bridge.""" api = self.bridge.api.sensors - if len(self._component_add_entities) < 2: + if len(self._component_add_entities) < len(self._enabled_platforms): return to_add = {} @@ -110,12 +116,24 @@ class SensorManager: # Iterate again now we have all the presence sensors, and add the # related sensors with nice names where appropriate. for item_id in api: - existing = current.get(api[item_id].uniqueid) - if existing is not None: + uniqueid = api[item_id].uniqueid + if current.get(uniqueid, self.current_events.get(uniqueid)) is not None: continue - primary_sensor = None - sensor_config = SENSOR_CONFIG_MAP.get(api[item_id].type) + sensor_type = api[item_id].type + + # Check for event generator devices + event_config = EVENT_CONFIG_MAP.get(sensor_type) + if event_config is not None: + base_name = api[item_id].name + name = event_config["name_format"].format(base_name) + new_event = event_config["class"](api[item_id], name, self.bridge) + self.bridge.hass.async_create_task( + new_event.async_update_device_registry() + ) + self.current_events[uniqueid] = new_event + + sensor_config = SENSOR_CONFIG_MAP.get(sensor_type) if sensor_config is None: continue @@ -125,13 +143,11 @@ class SensorManager: base_name = primary_sensor.name name = sensor_config["name_format"].format(base_name) - current[api[item_id].uniqueid] = sensor_config["class"]( + current[uniqueid] = sensor_config["class"]( api[item_id], name, self.bridge, primary_sensor=primary_sensor ) - to_add.setdefault(sensor_config["platform"], []).append( - current[api[item_id].uniqueid] - ) + to_add.setdefault(sensor_config["platform"], []).append(current[uniqueid]) self.bridge.hass.async_create_task( remove_devices( @@ -143,53 +159,23 @@ class SensorManager: self._component_add_entities[platform](to_add[platform]) -class GenericHueSensor(entity.Entity): +class GenericHueSensor(GenericHueDevice, entity.Entity): """Representation of a Hue sensor.""" should_poll = False - def __init__(self, sensor, name, bridge, primary_sensor=None): - """Initialize the sensor.""" - self.sensor = sensor - self._name = name - self._primary_sensor = primary_sensor - self.bridge = bridge - async def _async_update_ha_state(self, *args, **kwargs): raise NotImplementedError - @property - def primary_sensor(self): - """Return the primary sensor entity of the physical device.""" - return self._primary_sensor or self.sensor - - @property - def device_id(self): - """Return the ID of the physical device this sensor is part of.""" - return self.unique_id[:23] - - @property - def unique_id(self): - """Return the ID of this Hue sensor.""" - return self.sensor.uniqueid - - @property - def name(self): - """Return a friendly name for the sensor.""" - return self._name - @property def available(self): """Return if sensor is available.""" return self.bridge.sensor_manager.coordinator.last_update_success and ( - self.bridge.allow_unreachable or self.sensor.config["reachable"] + self.bridge.allow_unreachable + # remotes like Hue Tap (ZGPSwitchSensor) have no _reachability_ + or self.sensor.config.get("reachable", True) ) - @property - def swupdatestate(self): - """Return detail of available software updates for this device.""" - return self.primary_sensor.raw.get("swupdate", {}).get("state") - async def async_added_to_hass(self): """When entity is added to hass.""" self.bridge.sensor_manager.coordinator.async_add_listener( @@ -209,21 +195,6 @@ class GenericHueSensor(entity.Entity): """ await self.bridge.sensor_manager.coordinator.async_request_refresh() - @property - def device_info(self): - """Return the device info. - - Links individual entities together in the hass device registry. - """ - return { - "identifiers": {(HUE_DOMAIN, self.device_id)}, - "name": self.primary_sensor.name, - "manufacturer": self.primary_sensor.manufacturername, - "model": (self.primary_sensor.productname or self.primary_sensor.modelid), - "sw_version": self.primary_sensor.swversion, - "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), - } - class GenericZLLSensor(GenericHueSensor): """Representation of a Hue-brand, physical sensor.""" diff --git a/homeassistant/components/hue/sensor_device.py b/homeassistant/components/hue/sensor_device.py new file mode 100644 index 00000000000..91719debeb5 --- /dev/null +++ b/homeassistant/components/hue/sensor_device.py @@ -0,0 +1,53 @@ +"""Support for the Philips Hue sensor devices.""" +from .const import DOMAIN as HUE_DOMAIN + + +class GenericHueDevice: + """Representation of a Hue device.""" + + def __init__(self, sensor, name, bridge, primary_sensor=None): + """Initialize the sensor.""" + self.sensor = sensor + self._name = name + self._primary_sensor = primary_sensor + self.bridge = bridge + + @property + def primary_sensor(self): + """Return the primary sensor entity of the physical device.""" + return self._primary_sensor or self.sensor + + @property + def device_id(self): + """Return the ID of the physical device this sensor is part of.""" + return self.unique_id[:23] + + @property + def unique_id(self): + """Return the ID of this Hue sensor.""" + return self.sensor.uniqueid + + @property + def name(self): + """Return a friendly name for the sensor.""" + return self._name + + @property + def swupdatestate(self): + """Return detail of available software updates for this device.""" + return self.primary_sensor.raw.get("swupdate", {}).get("state") + + @property + def device_info(self): + """Return the device info. + + Links individual entities together in the hass device registry. + """ + return { + "identifiers": {(HUE_DOMAIN, self.device_id)}, + "name": self.primary_sensor.name, + "manufacturer": self.primary_sensor.manufacturername, + "model": (self.primary_sensor.productname or self.primary_sensor.modelid), + "sw_version": self.primary_sensor.swversion, + "via_device": (HUE_DOMAIN, self.bridge.api.config.bridgeid), + } diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index ca83da725fa..cf1a4ab7983 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -11,6 +11,7 @@ import pytest from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base +from homeassistant.components.hue.hue_event import CONF_HUE_EVENT _LOGGER = logging.getLogger(__name__) @@ -241,6 +242,33 @@ UNSUPPORTED_SENSOR = { "uniqueid": "arbitrary", "recycle": True, } +HUE_TAP_REMOTE_1 = { + "state": {"buttonevent": 17, "lastupdated": "2019-06-22T14:43:50"}, + "swupdate": {"state": "notupdatable", "lastinstall": None}, + "config": {"on": True}, + "name": "Hue Tap", + "type": "ZGPSwitch", + "modelid": "ZGPSWITCH", + "manufacturername": "Philips", + "productname": "Hue tap switch", + "diversityid": "d8cde5d5-0eef-4b95-b0f0-71ddd2952af4", + "uniqueid": "00:00:00:00:00:44:23:08-f2", + "capabilities": {"certified": True, "primary": True, "inputs": []}, +} +HUE_DIMMER_REMOTE_1 = { + "state": {"buttonevent": 4002, "lastupdated": "2019-12-28T21:58:02"}, + "swupdate": {"state": "noupdates", "lastinstall": "2019-10-13T13:16:15"}, + "config": {"on": True, "battery": 100, "reachable": True, "pending": []}, + "name": "Hue dimmer switch 1", + "type": "ZLLSwitch", + "modelid": "RWL021", + "manufacturername": "Philips", + "productname": "Hue dimmer switch", + "diversityid": "73bbabea-3420-499a-9856-46bf437e119b", + "swversion": "6.1.1.28573", + "uniqueid": "00:17:88:01:10:3e:3a:dc-02-fc00", + "capabilities": {"certified": True, "primary": True, "inputs": []}, +} SENSOR_RESPONSE = { "1": PRESENCE_SENSOR_1_PRESENT, "2": LIGHT_LEVEL_SENSOR_1, @@ -248,6 +276,8 @@ SENSOR_RESPONSE = { "4": PRESENCE_SENSOR_2_NOT_PRESENT, "5": LIGHT_LEVEL_SENSOR_2, "6": TEMPERATURE_SENSOR_2, + "7": HUE_TAP_REMOTE_1, + "8": HUE_DIMMER_REMOTE_1, } @@ -341,8 +371,8 @@ async def test_sensors_with_multiple_bridges(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 1 assert len(mock_bridge_2.mock_requests) == 1 - # 3 "physical" sensors with 3 virtual sensors each - assert len(hass.states.async_all()) == 9 + # 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): @@ -351,7 +381,7 @@ async def test_sensors(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 # 2 "physical" sensors with 3 virtual sensors each - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 7 presence_sensor_1 = hass.states.get("binary_sensor.living_room_sensor_motion") light_level_sensor_1 = hass.states.get("sensor.living_room_sensor_light_level") @@ -377,6 +407,11 @@ async def test_sensors(hass, mock_bridge): assert temperature_sensor_2.state == "18.75" assert temperature_sensor_2.name == "Kitchen sensor temperature" + battery_remote_1 = hass.states.get("sensor.hue_dimmer_switch_1_battery_level") + assert battery_remote_1 is not None + assert battery_remote_1.state == "100" + assert battery_remote_1.name == "Hue dimmer switch 1 battery level" + async def test_unsupported_sensors(hass, mock_bridge): """Test that unsupported sensors don't get added and don't fail.""" @@ -385,8 +420,8 @@ async def test_unsupported_sensors(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(response_with_unsupported) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 - # 2 "physical" sensors with 3 virtual sensors each - assert len(hass.states.async_all()) == 6 + # 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): @@ -395,14 +430,14 @@ async def test_new_sensor_discovered(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 7 new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response.update( { - "7": PRESENCE_SENSOR_3_PRESENT, - "8": LIGHT_LEVEL_SENSOR_3, - "9": TEMPERATURE_SENSOR_3, + "9": PRESENCE_SENSOR_3_PRESENT, + "10": LIGHT_LEVEL_SENSOR_3, + "11": TEMPERATURE_SENSOR_3, } ) @@ -413,7 +448,7 @@ async def test_new_sensor_discovered(hass, mock_bridge): await hass.async_block_till_done() assert len(mock_bridge.mock_requests) == 2 - assert len(hass.states.async_all()) == 9 + assert len(hass.states.async_all()) == 10 presence = hass.states.get("binary_sensor.bedroom_sensor_motion") assert presence is not None @@ -429,7 +464,7 @@ async def test_sensor_removed(hass, mock_bridge): await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 - assert len(hass.states.async_all()) == 6 + assert len(hass.states.async_all()) == 7 mock_bridge.mock_sensor_responses.clear() keys = ("1", "2", "3") @@ -466,3 +501,121 @@ async def test_update_unauthorized(hass, mock_bridge): assert len(mock_bridge.mock_requests) == 0 assert len(hass.states.async_all()) == 0 assert len(mock_bridge.handle_unauthorized_error.mock_calls) == 1 + + +async def test_hue_events(hass, mock_bridge): + """Test that hue remotes fire events when pressed.""" + mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) + + mock_listener = Mock() + unsub = hass.bus.async_listen(CONF_HUE_EVENT, mock_listener) + + await setup_bridge(hass, mock_bridge) + assert len(mock_bridge.mock_requests) == 1 + assert len(hass.states.async_all()) == 7 + assert len(mock_listener.mock_calls) == 0 + + new_sensor_response = dict(SENSOR_RESPONSE) + new_sensor_response["7"]["state"] = { + "buttonevent": 18, + "lastupdated": "2019-12-28T22:58:02", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 7 + assert len(mock_listener.mock_calls) == 1 + assert mock_listener.mock_calls[0][1][0].data == { + "id": "hue_tap", + "unique_id": "00:00:00:00:00:44:23:08-f2", + "event": 18, + "last_updated": "2019-12-28T22:58:02", + } + + new_sensor_response = dict(new_sensor_response) + new_sensor_response["8"]["state"] = { + "buttonevent": 3002, + "lastupdated": "2019-12-28T22:58:01", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 7 + assert len(mock_listener.mock_calls) == 2 + assert mock_listener.mock_calls[1][1][0].data == { + "id": "hue_dimmer_switch_1", + "unique_id": "00:17:88:01:10:3e:3a:dc-02-fc00", + "event": 3002, + "last_updated": "2019-12-28T22:58:01", + } + + # Add a new remote. In discovery the new event is registered **but not fired** + new_sensor_response = dict(new_sensor_response) + new_sensor_response["21"] = { + "state": { + "rotaryevent": 2, + "expectedrotation": 208, + "expectedeventduration": 400, + "lastupdated": "2020-01-31T15:56:19", + }, + "swupdate": {"state": "noupdates", "lastinstall": "2019-11-26T03:35:21"}, + "config": {"on": True, "battery": 100, "reachable": True, "pending": []}, + "name": "Lutron Aurora 1", + "type": "ZLLRelativeRotary", + "modelid": "Z3-1BRL", + "manufacturername": "Lutron", + "productname": "Lutron Aurora", + "diversityid": "2c3a75ff-55c4-4e4d-8c44-82d330b8eb9b", + "swversion": "3.4", + "uniqueid": "ff:ff:00:0f:e7:fd:bc:b7-01-fc00-0014", + "capabilities": { + "certified": True, + "primary": True, + "inputs": [ + { + "repeatintervals": [400], + "events": [ + {"rotaryevent": 1, "eventtype": "start"}, + {"rotaryevent": 2, "eventtype": "repeat"}, + ], + } + ], + }, + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 4 + assert len(hass.states.async_all()) == 8 + assert len(mock_listener.mock_calls) == 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) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 5 + assert len(hass.states.async_all()) == 8 + assert len(mock_listener.mock_calls) == 3 + assert mock_listener.mock_calls[2][1][0].data == { + "id": "lutron_aurora_1", + "unique_id": "ff:ff:00:0f:e7:fd:bc:b7-01-fc00-0014", + "event": 2, + "last_updated": "2020-01-31T15:57:19", + } + + unsub() From 6cafc9aaef4836ce27f4fa9bcb9097f6533243a0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 13:45:33 -0500 Subject: [PATCH 342/431] Add humidity support to homekit thermostats (#33367) --- homeassistant/components/homekit/const.py | 1 + .../components/homekit/type_thermostats.py | 54 +++++++++++++++++++ .../homekit/test_type_thermostats.py | 48 +++++++++++++++++ 3 files changed, 103 insertions(+) diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ac421913f6f..c0f0abe8177 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -142,6 +142,7 @@ CHAR_SWING_MODE = "SwingMode" CHAR_TARGET_DOOR_STATE = "TargetDoorState" CHAR_TARGET_HEATING_COOLING = "TargetHeatingCoolingState" CHAR_TARGET_POSITION = "TargetPosition" +CHAR_TARGET_HUMIDITY = "TargetRelativeHumidity" CHAR_TARGET_SECURITY_STATE = "SecuritySystemTargetState" CHAR_TARGET_TEMPERATURE = "TargetTemperature" CHAR_TARGET_TILT_ANGLE = "TargetHorizontalTiltAngle" diff --git a/homeassistant/components/homekit/type_thermostats.py b/homeassistant/components/homekit/type_thermostats.py index 19f7899d79b..b8c3b3f0197 100644 --- a/homeassistant/components/homekit/type_thermostats.py +++ b/homeassistant/components/homekit/type_thermostats.py @@ -4,11 +4,14 @@ import logging from pyhap.const import CATEGORY_THERMOSTAT from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, ATTR_MAX_TEMP, + ATTR_MIN_HUMIDITY, ATTR_MIN_TEMP, ATTR_TARGET_TEMP_HIGH, ATTR_TARGET_TEMP_LOW, @@ -17,6 +20,7 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_IDLE, CURRENT_HVAC_OFF, DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, HVAC_MODE_AUTO, @@ -25,8 +29,10 @@ from homeassistant.components.climate.const import ( HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_OFF, + SERVICE_SET_HUMIDITY, SERVICE_SET_HVAC_MODE as SERVICE_SET_HVAC_MODE_THERMOSTAT, SERVICE_SET_TEMPERATURE as SERVICE_SET_TEMPERATURE_THERMOSTAT, + SUPPORT_TARGET_HUMIDITY, SUPPORT_TARGET_TEMPERATURE_RANGE, ) from homeassistant.components.water_heater import ( @@ -39,6 +45,7 @@ from homeassistant.const import ( ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT, + UNIT_PERCENTAGE, ) from . import TYPES @@ -46,9 +53,11 @@ from .accessories import HomeAccessory, debounce from .const import ( CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_CURRENT_HEATING_COOLING, + CHAR_CURRENT_HUMIDITY, CHAR_CURRENT_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE, CHAR_TARGET_HEATING_COOLING, + CHAR_TARGET_HUMIDITY, CHAR_TARGET_TEMPERATURE, CHAR_TEMP_DISPLAY_UNITS, DEFAULT_MAX_TEMP_WATER_HEATER, @@ -99,6 +108,10 @@ class Thermostat(HomeAccessory): self._flag_heatingthresh = False min_temp, max_temp = self.get_temperature_range() + min_humidity = self.hass.states.get(self.entity_id).attributes.get( + ATTR_MIN_HUMIDITY, DEFAULT_MIN_HUMIDITY + ) + # Add additional characteristics if auto mode is supported self.chars = [] state = self.hass.states.get(self.entity_id) @@ -109,6 +122,9 @@ class Thermostat(HomeAccessory): (CHAR_COOLING_THRESHOLD_TEMPERATURE, CHAR_HEATING_THRESHOLD_TEMPERATURE) ) + if features & SUPPORT_TARGET_HUMIDITY: + self.chars.extend((CHAR_TARGET_HUMIDITY, CHAR_CURRENT_HUMIDITY)) + serv_thermostat = self.add_preload_service(SERV_THERMOSTAT, self.chars) # Current mode characteristics @@ -193,6 +209,23 @@ class Thermostat(HomeAccessory): properties={PROP_MIN_VALUE: min_temp, PROP_MAX_VALUE: max_temp}, setter_callback=self.set_heating_threshold, ) + self.char_target_humidity = None + self.char_current_humidity = None + if CHAR_TARGET_HUMIDITY in self.chars: + self.char_target_humidity = serv_thermostat.configure_char( + CHAR_TARGET_HUMIDITY, + value=50, + # We do not set a max humidity because + # homekit currently has a bug that will show the lower bound + # shifted upwards. For example if you have a max humidity + # of 80% homekit will give you the options 20%-100% instead + # of 0-80% + properties={PROP_MIN_VALUE: min_humidity}, + setter_callback=self.set_target_humidity, + ) + self.char_current_humidity = serv_thermostat.configure_char( + CHAR_CURRENT_HUMIDITY, value=50, + ) def get_temperature_range(self): """Return min and max temperature range.""" @@ -224,6 +257,15 @@ class Thermostat(HomeAccessory): DOMAIN_CLIMATE, SERVICE_SET_HVAC_MODE_THERMOSTAT, params, hass_value ) + @debounce + def set_target_humidity(self, value): + """Set target humidity to value if call came from HomeKit.""" + _LOGGER.debug("%s: Set target humidity to %d", self.entity_id, value) + params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HUMIDITY: value} + self.call_service( + DOMAIN_CLIMATE, SERVICE_SET_HUMIDITY, params, f"{value}{UNIT_PERCENTAGE}", + ) + @debounce def set_cooling_threshold(self, value): """Set cooling threshold temp to value if call came from HomeKit.""" @@ -288,6 +330,12 @@ class Thermostat(HomeAccessory): current_temp = temperature_to_homekit(current_temp, self._unit) self.char_current_temp.set_value(current_temp) + # Update current humidity + if CHAR_CURRENT_HUMIDITY in self.chars: + current_humdity = new_state.attributes.get(ATTR_CURRENT_HUMIDITY) + if isinstance(current_humdity, (int, float)): + self.char_current_humidity.set_value(current_humdity) + # Update target temperature target_temp = new_state.attributes.get(ATTR_TEMPERATURE) if isinstance(target_temp, (int, float)): @@ -296,6 +344,12 @@ class Thermostat(HomeAccessory): self.char_target_temp.set_value(target_temp) self._flag_temperature = False + # Update target humidity + if CHAR_TARGET_HUMIDITY in self.chars: + target_humdity = new_state.attributes.get(ATTR_HUMIDITY) + if isinstance(target_humdity, (int, float)): + self.char_target_humidity.set_value(target_humdity) + # Update cooling threshold temperature if characteristic exists if self.char_cooling_thresh_temp: cooling_thresh = new_state.attributes.get(ATTR_TARGET_TEMP_HIGH) diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index df9a10fc409..756b20456fe 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -5,7 +5,9 @@ from unittest.mock import patch import pytest from homeassistant.components.climate.const import ( + ATTR_CURRENT_HUMIDITY, ATTR_CURRENT_TEMPERATURE, + ATTR_HUMIDITY, ATTR_HVAC_ACTION, ATTR_HVAC_MODE, ATTR_HVAC_MODES, @@ -18,6 +20,7 @@ from homeassistant.components.climate.const import ( CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, DEFAULT_MAX_TEMP, + DEFAULT_MIN_HUMIDITY, DEFAULT_MIN_TEMP, DOMAIN as DOMAIN_CLIMATE, HVAC_MODE_AUTO, @@ -99,6 +102,8 @@ async def test_thermostat(hass, hk_driver, cls, events): assert acc.char_display_units.value == 0 assert acc.char_cooling_thresh_temp is None assert acc.char_heating_thresh_temp is None + assert acc.char_target_humidity is None + assert acc.char_current_humidity is None assert acc.char_target_temp.properties[PROP_MAX_VALUE] == DEFAULT_MAX_TEMP assert acc.char_target_temp.properties[PROP_MIN_VALUE] == DEFAULT_MIN_TEMP @@ -357,6 +362,49 @@ async def test_thermostat_auto(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "cooling threshold 25.0°C" +async def test_thermostat_humidity(hass, hk_driver, cls, events): + """Test if accessory and HA are updated accordingly with humidity.""" + entity_id = "climate.test" + + # support_auto = True + hass.states.async_set(entity_id, HVAC_MODE_OFF, {ATTR_SUPPORTED_FEATURES: 4}) + await hass.async_block_till_done() + acc = cls.thermostat(hass, hk_driver, "Climate", entity_id, 2, None) + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + + assert acc.char_target_humidity.value == 50 + assert acc.char_current_humidity.value == 50 + + assert acc.char_target_humidity.properties[PROP_MIN_VALUE] == DEFAULT_MIN_HUMIDITY + + hass.states.async_set( + entity_id, HVAC_MODE_HEAT_COOL, {ATTR_HUMIDITY: 65, ATTR_CURRENT_HUMIDITY: 40}, + ) + await hass.async_block_till_done() + assert acc.char_current_humidity.value == 40 + assert acc.char_target_humidity.value == 65 + + hass.states.async_set( + entity_id, HVAC_MODE_COOL, {ATTR_HUMIDITY: 35, ATTR_CURRENT_HUMIDITY: 70}, + ) + await hass.async_block_till_done() + assert acc.char_current_humidity.value == 70 + assert acc.char_target_humidity.value == 35 + + # Set from HomeKit + call_set_humidity = async_mock_service(hass, DOMAIN_CLIMATE, "set_humidity") + + await hass.async_add_job(acc.char_target_humidity.client_update_value, 35) + await hass.async_block_till_done() + assert call_set_humidity[0] + assert call_set_humidity[0].data[ATTR_ENTITY_ID] == entity_id + assert call_set_humidity[0].data[ATTR_HUMIDITY] == 35 + assert acc.char_target_humidity.value == 35 + assert len(events) == 1 + assert events[-1].data[ATTR_VALUE] == "35%" + + async def test_thermostat_power_state(hass, hk_driver, cls, events): """Test if accessory and HA are updated accordingly.""" entity_id = "climate.test" From 12b408219e08a14eb802f1b71290b8c4b9fc0013 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 13:55:13 -0500 Subject: [PATCH 343/431] Improve handling of nuheat switching states (#33410) * The api reports success before the state change takes effect * We now set state optimistically and followup with an update 4 seconds in the future after any state change to verify it actually happens. * When hvac_mode is passed to the set_temperature service we now switch to the desired mode. --- homeassistant/components/nuheat/climate.py | 99 ++++++++++++++-------- homeassistant/components/nuheat/const.py | 2 + tests/components/nuheat/mocks.py | 4 + 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index f675b6a90f4..f8d6bf1d8df 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -3,10 +3,16 @@ from datetime import timedelta import logging from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD -from nuheat.util import celsius_to_nuheat, fahrenheit_to_nuheat +from nuheat.util import ( + celsius_to_nuheat, + fahrenheit_to_nuheat, + nuheat_to_celsius, + nuheat_to_fahrenheit, +) from homeassistant.components.climate import ClimateDevice from homeassistant.components.climate.const import ( + ATTR_HVAC_MODE, CURRENT_HVAC_HEAT, CURRENT_HVAC_IDLE, HVAC_MODE_AUTO, @@ -15,9 +21,10 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.helpers import event as event_helper from homeassistant.util import Throttle -from .const import DOMAIN, MANUFACTURER +from .const import DOMAIN, MANUFACTURER, NUHEAT_API_STATE_SHIFT_DELAY _LOGGER = logging.getLogger(__name__) @@ -67,6 +74,8 @@ class NuHeatThermostat(ClimateDevice): """Initialize the thermostat.""" self._thermostat = thermostat self._temperature_unit = temperature_unit + self._schedule_mode = None + self._target_temperature = None self._force_update = False @property @@ -107,19 +116,15 @@ class NuHeatThermostat(ClimateDevice): def set_hvac_mode(self, hvac_mode): """Set the system mode.""" - - # This is the same as what res if hvac_mode == HVAC_MODE_AUTO: - self._thermostat.resume_schedule() + self._set_schedule_mode(SCHEDULE_RUN) elif hvac_mode == HVAC_MODE_HEAT: - self._thermostat.schedule_mode = SCHEDULE_HOLD - - self._schedule_update() + self._set_schedule_mode(SCHEDULE_HOLD) @property def hvac_mode(self): """Return current setting heat or auto.""" - if self._thermostat.schedule_mode in (SCHEDULE_TEMPORARY_HOLD, SCHEDULE_HOLD): + if self._schedule_mode in (SCHEDULE_TEMPORARY_HOLD, SCHEDULE_HOLD): return HVAC_MODE_HEAT return HVAC_MODE_AUTO @@ -148,15 +153,14 @@ class NuHeatThermostat(ClimateDevice): def target_temperature(self): """Return the currently programmed temperature.""" if self._temperature_unit == "C": - return self._thermostat.target_celsius + return nuheat_to_celsius(self._target_temperature) - return self._thermostat.target_fahrenheit + return nuheat_to_fahrenheit(self._target_temperature) @property def preset_mode(self): """Return current preset mode.""" - schedule_mode = self._thermostat.schedule_mode - return SCHEDULE_MODE_TO_PRESET_MODE_MAP.get(schedule_mode, PRESET_RUN) + return SCHEDULE_MODE_TO_PRESET_MODE_MAP.get(self._schedule_mode, PRESET_RUN) @property def preset_modes(self): @@ -168,35 +172,44 @@ class NuHeatThermostat(ClimateDevice): """Return list of possible operation modes.""" return OPERATION_LIST - def resume_program(self): - """Resume the thermostat's programmed schedule.""" - self._thermostat.resume_schedule() - self._schedule_update() - def set_preset_mode(self, preset_mode): """Update the hold mode of the thermostat.""" - - self._thermostat.schedule_mode = PRESET_MODE_TO_SCHEDULE_MODE_MAP.get( - preset_mode, SCHEDULE_RUN + self._set_schedule_mode( + PRESET_MODE_TO_SCHEDULE_MODE_MAP.get(preset_mode, SCHEDULE_RUN) ) + + def _set_schedule_mode(self, schedule_mode): + """Set a schedule mode.""" + self._schedule_mode = schedule_mode + # Changing the property here does the actual set + self._thermostat.schedule_mode = schedule_mode self._schedule_update() def set_temperature(self, **kwargs): """Set a new target temperature.""" - self._set_temperature(kwargs.get(ATTR_TEMPERATURE)) + self._set_temperature_and_mode( + kwargs.get(ATTR_TEMPERATURE), hvac_mode=kwargs.get(ATTR_HVAC_MODE) + ) - def _set_temperature(self, temperature): + def _set_temperature_and_mode(self, temperature, hvac_mode=None, preset_mode=None): + """Set temperature and hvac mode at the same time.""" if self._temperature_unit == "C": - target_temp = celsius_to_nuheat(temperature) + target_temperature = celsius_to_nuheat(temperature) else: - target_temp = fahrenheit_to_nuheat(temperature) + target_temperature = fahrenheit_to_nuheat(temperature) # If they set a temperature without changing the mode # to heat, we behave like the device does locally # and set a temp hold. - target_schedule_mode = SCHEDULE_HOLD - if self._thermostat.schedule_mode in (SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD): - target_schedule_mode = SCHEDULE_TEMPORARY_HOLD + target_schedule_mode = SCHEDULE_TEMPORARY_HOLD + if preset_mode: + target_schedule_mode = PRESET_MODE_TO_SCHEDULE_MODE_MAP.get( + preset_mode, SCHEDULE_RUN + ) + elif self._schedule_mode == SCHEDULE_HOLD or ( + hvac_mode and hvac_mode == HVAC_MODE_HEAT + ): + target_schedule_mode = SCHEDULE_HOLD _LOGGER.debug( "Setting NuHeat thermostat temperature to %s %s and schedule mode: %s", @@ -204,15 +217,32 @@ class NuHeatThermostat(ClimateDevice): self.temperature_unit, target_schedule_mode, ) - # If we do not send schedule_mode we always get - # SCHEDULE_HOLD - self._thermostat.set_target_temperature(target_temp, target_schedule_mode) + + self._thermostat.set_target_temperature( + target_temperature, target_schedule_mode + ) + self._schedule_mode = target_schedule_mode + self._target_temperature = target_temperature self._schedule_update() def _schedule_update(self): + if not self.hass: + return + + # Update the new state + self.schedule_update_ha_state(False) + + # nuheat has a delay switching state + # so we schedule a poll of the api + # in the future to make sure the change actually + # took effect + event_helper.call_later( + self.hass, NUHEAT_API_STATE_SHIFT_DELAY, self._schedule_force_refresh + ) + + def _schedule_force_refresh(self, _): self._force_update = True - if self.hass: - self.schedule_update_ha_state(True) + self.schedule_update_ha_state(True) def update(self): """Get the latest state from the thermostat.""" @@ -226,6 +256,8 @@ class NuHeatThermostat(ClimateDevice): def _throttled_update(self, **kwargs): """Get the latest state from the thermostat with a throttle.""" self._thermostat.get_data() + self._schedule_mode = self._thermostat.schedule_mode + self._target_temperature = self._thermostat.target_temperature @property def device_info(self): @@ -233,5 +265,6 @@ class NuHeatThermostat(ClimateDevice): return { "identifiers": {(DOMAIN, self._thermostat.serial_number)}, "name": self._thermostat.room, + "model": "nVent Signature", "manufacturer": MANUFACTURER, } diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py index e9465d69275..1bb6c3825e7 100644 --- a/homeassistant/components/nuheat/const.py +++ b/homeassistant/components/nuheat/const.py @@ -7,3 +7,5 @@ PLATFORMS = ["climate"] CONF_SERIAL_NUMBER = "serial_number" MANUFACTURER = "NuHeat" + +NUHEAT_API_STATE_SHIFT_DELAY = 4 diff --git a/tests/components/nuheat/mocks.py b/tests/components/nuheat/mocks.py index 7b7c9d1ac06..a9adfd3aa57 100644 --- a/tests/components/nuheat/mocks.py +++ b/tests/components/nuheat/mocks.py @@ -23,6 +23,7 @@ def _get_mock_thermostat_run(): schedule_mode=SCHEDULE_RUN, target_celsius=22, target_fahrenheit=72, + target_temperature=2217, ) thermostat.get_data = Mock() @@ -48,6 +49,7 @@ def _get_mock_thermostat_schedule_hold_unavailable(): schedule_mode=SCHEDULE_HOLD, target_celsius=23, target_fahrenheit=79, + target_temperature=2609, ) thermostat.get_data = Mock() @@ -73,6 +75,7 @@ def _get_mock_thermostat_schedule_hold_available(): schedule_mode=SCHEDULE_HOLD, target_celsius=23, target_fahrenheit=79, + target_temperature=2609, ) thermostat.get_data = Mock() @@ -98,6 +101,7 @@ def _get_mock_thermostat_schedule_temporary_hold(): schedule_mode=SCHEDULE_TEMPORARY_HOLD, target_celsius=43, target_fahrenheit=99, + target_temperature=3729, ) thermostat.get_data = Mock() From b783aab41b9e5fb0eba03a67c4a4d7014d725f9e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 13:58:44 -0500 Subject: [PATCH 344/431] Add binary sensor for myq gateway connectivity (#33423) --- .coveragerc | 1 - homeassistant/components/myq/binary_sensor.py | 108 ++++++++++++++ homeassistant/components/myq/const.py | 39 ++++- homeassistant/components/myq/cover.py | 7 +- tests/components/myq/test_binary_sensor.py | 20 +++ tests/components/myq/test_cover.py | 50 +++++++ tests/components/myq/util.py | 42 ++++++ tests/fixtures/myq/devices.json | 133 ++++++++++++++++++ 8 files changed, 397 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/myq/binary_sensor.py create mode 100644 tests/components/myq/test_binary_sensor.py create mode 100644 tests/components/myq/test_cover.py create mode 100644 tests/components/myq/util.py create mode 100644 tests/fixtures/myq/devices.json diff --git a/.coveragerc b/.coveragerc index 14a731498b9..851922e4f3a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -442,7 +442,6 @@ omit = homeassistant/components/mychevy/* homeassistant/components/mycroft/* homeassistant/components/mycroft/notify.py - homeassistant/components/myq/cover.py homeassistant/components/mysensors/* homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py diff --git a/homeassistant/components/myq/binary_sensor.py b/homeassistant/components/myq/binary_sensor.py new file mode 100644 index 00000000000..7ce303e5d19 --- /dev/null +++ b/homeassistant/components/myq/binary_sensor.py @@ -0,0 +1,108 @@ +"""Support for MyQ gateways.""" +import logging + +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, +) +from homeassistant.core import callback + +from .const import ( + DOMAIN, + KNOWN_MODELS, + MANUFACTURER, + MYQ_COORDINATOR, + MYQ_DEVICE_FAMILY, + MYQ_DEVICE_FAMILY_GATEWAY, + MYQ_DEVICE_STATE, + MYQ_DEVICE_STATE_ONLINE, + MYQ_GATEWAY, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up mysq covers.""" + data = hass.data[DOMAIN][config_entry.entry_id] + myq = data[MYQ_GATEWAY] + coordinator = data[MYQ_COORDINATOR] + + entities = [] + + for device in myq.devices.values(): + if device.device_json[MYQ_DEVICE_FAMILY] == MYQ_DEVICE_FAMILY_GATEWAY: + entities.append(MyQBinarySensorDevice(coordinator, device)) + + async_add_entities(entities, True) + + +class MyQBinarySensorDevice(BinarySensorDevice): + """Representation of a MyQ gateway.""" + + def __init__(self, coordinator, device): + """Initialize with API object, device id.""" + self._coordinator = coordinator + self._device = device + + @property + def device_class(self): + """We track connectivity for gateways.""" + return DEVICE_CLASS_CONNECTIVITY + + @property + def name(self): + """Return the name of the garage door if any.""" + return f"{self._device.name} MyQ Gateway" + + @property + def is_on(self): + """Return if the device is online.""" + if not self._coordinator.last_update_success: + return False + + # Not all devices report online so assume True if its missing + return self._device.device_json[MYQ_DEVICE_STATE].get( + MYQ_DEVICE_STATE_ONLINE, True + ) + + @property + def unique_id(self): + """Return a unique, Home Assistant friendly identifier for this entity.""" + return self._device.device_id + + async def async_update(self): + """Update status of cover.""" + await self._coordinator.async_request_refresh() + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(DOMAIN, self._device.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + "sw_version": self._device.firmware_version, + } + model = KNOWN_MODELS.get(self._device.device_id[2:4]) + if model: + device_info["model"] = model + + return device_info + + @property + def should_poll(self): + """Return False, updates are controlled via coordinator.""" + return False + + @callback + def _async_consume_update(self): + self.async_write_ha_state() + + async def async_added_to_hass(self): + """Subscribe to updates.""" + self._coordinator.async_add_listener(self._async_consume_update) + + async def async_will_remove_from_hass(self): + """Undo subscription.""" + self._coordinator.async_remove_listener(self._async_consume_update) diff --git a/homeassistant/components/myq/const.py b/homeassistant/components/myq/const.py index dcae53bd080..352c19ebd24 100644 --- a/homeassistant/components/myq/const.py +++ b/homeassistant/components/myq/const.py @@ -10,10 +10,14 @@ from homeassistant.const import STATE_CLOSED, STATE_CLOSING, STATE_OPEN, STATE_O DOMAIN = "myq" -PLATFORMS = ["cover"] +PLATFORMS = ["cover", "binary_sensor"] MYQ_DEVICE_TYPE = "device_type" MYQ_DEVICE_TYPE_GATE = "gate" + +MYQ_DEVICE_FAMILY = "device_family" +MYQ_DEVICE_FAMILY_GATEWAY = "gateway" + MYQ_DEVICE_STATE = "state" MYQ_DEVICE_STATE_ONLINE = "online" @@ -39,3 +43,36 @@ TRANSITION_START_DURATION = 7 # Estimated time it takes myq to complete a transition # from one state to another TRANSITION_COMPLETE_DURATION = 37 + +MANUFACTURER = "The Chamberlain Group Inc." + +KNOWN_MODELS = { + "00": "Chamberlain Ethernet Gateway", + "01": "LiftMaster Ethernet Gateway", + "02": "Craftsman Ethernet Gateway", + "03": "Chamberlain Wi-Fi hub", + "04": "LiftMaster Wi-Fi hub", + "05": "Craftsman Wi-Fi hub", + "08": "LiftMaster Wi-Fi GDO DC w/Battery Backup", + "09": "Chamberlain Wi-Fi GDO DC w/Battery Backup", + "10": "Craftsman Wi-Fi GDO DC 3/4HP", + "11": "MyQ Replacement Logic Board Wi-Fi GDO DC 3/4HP", + "12": "Chamberlain Wi-Fi GDO DC 1.25HP", + "13": "LiftMaster Wi-Fi GDO DC 1.25HP", + "14": "Craftsman Wi-Fi GDO DC 1.25HP", + "15": "MyQ Replacement Logic Board Wi-Fi GDO DC 1.25HP", + "0A": "Chamberlain Wi-Fi GDO or Gate Operator AC", + "0B": "LiftMaster Wi-Fi GDO or Gate Operator AC", + "0C": "Craftsman Wi-Fi GDO or Gate Operator AC", + "0D": "MyQ Replacement Logic Board Wi-Fi GDO or Gate Operator AC", + "0E": "Chamberlain Wi-Fi GDO DC 3/4HP", + "0F": "LiftMaster Wi-Fi GDO DC 3/4HP", + "20": "Chamberlain MyQ Home Bridge", + "21": "LiftMaster MyQ Home Bridge", + "23": "Chamberlain Smart Garage Hub", + "24": "LiftMaster Smart Garage Hub", + "27": "LiftMaster Wi-Fi Wall Mount opener", + "28": "LiftMaster Commercial Wi-Fi Wall Mount operator", + "80": "EU LiftMaster Ethernet Gateway", + "81": "EU Chamberlain Ethernet Gateway", +} diff --git a/homeassistant/components/myq/cover.py b/homeassistant/components/myq/cover.py index 21eca6179dd..57308a778a5 100644 --- a/homeassistant/components/myq/cover.py +++ b/homeassistant/components/myq/cover.py @@ -27,6 +27,8 @@ from homeassistant.helpers.event import async_call_later from .const import ( DOMAIN, + KNOWN_MODELS, + MANUFACTURER, MYQ_COORDINATOR, MYQ_DEVICE_STATE, MYQ_DEVICE_STATE_ONLINE, @@ -181,9 +183,12 @@ class MyQDevice(CoverDevice): device_info = { "identifiers": {(DOMAIN, self._device.device_id)}, "name": self._device.name, - "manufacturer": "The Chamberlain Group Inc.", + "manufacturer": MANUFACTURER, "sw_version": self._device.firmware_version, } + model = KNOWN_MODELS.get(self._device.device_id[2:4]) + if model: + device_info["model"] = model if self._device.parent_device_id: device_info["via_device"] = (DOMAIN, self._device.parent_device_id) return device_info diff --git a/tests/components/myq/test_binary_sensor.py b/tests/components/myq/test_binary_sensor.py new file mode 100644 index 00000000000..cef1f2e2409 --- /dev/null +++ b/tests/components/myq/test_binary_sensor.py @@ -0,0 +1,20 @@ +"""The scene tests for the myq platform.""" + +from homeassistant.const import STATE_ON + +from .util import async_init_integration + + +async def test_create_binary_sensors(hass): + """Test creation of binary_sensors.""" + + await async_init_integration(hass) + + state = hass.states.get("binary_sensor.happy_place_myq_gateway") + assert state.state == STATE_ON + expected_attributes = {"device_class": "connectivity"} + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/myq/test_cover.py b/tests/components/myq/test_cover.py new file mode 100644 index 00000000000..5029c4f6b0b --- /dev/null +++ b/tests/components/myq/test_cover.py @@ -0,0 +1,50 @@ +"""The scene tests for the myq platform.""" + +from homeassistant.const import STATE_CLOSED + +from .util import async_init_integration + + +async def test_create_covers(hass): + """Test creation of covers.""" + + await async_init_integration(hass) + + state = hass.states.get("cover.large_garage_door") + assert state.state == STATE_CLOSED + expected_attributes = { + "device_class": "garage", + "friendly_name": "Large Garage Door", + "supported_features": 3, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("cover.small_garage_door") + assert state.state == STATE_CLOSED + expected_attributes = { + "device_class": "garage", + "friendly_name": "Small Garage Door", + "supported_features": 3, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) + + state = hass.states.get("cover.gate") + assert state.state == STATE_CLOSED + expected_attributes = { + "device_class": "gate", + "friendly_name": "Gate", + "supported_features": 3, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all( + state.attributes[key] == expected_attributes[key] for key in expected_attributes + ) diff --git a/tests/components/myq/util.py b/tests/components/myq/util.py new file mode 100644 index 00000000000..48af17188eb --- /dev/null +++ b/tests/components/myq/util.py @@ -0,0 +1,42 @@ +"""Tests for the myq integration.""" + +import json + +from asynctest import patch + +from homeassistant.components.myq.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry, load_fixture + + +async def async_init_integration( + hass: HomeAssistant, skip_setup: bool = False, +) -> MockConfigEntry: + """Set up the myq integration in Home Assistant.""" + + devices_fixture = "myq/devices.json" + devices_json = load_fixture(devices_fixture) + devices_dict = json.loads(devices_json) + + def _handle_mock_api_request(method, endpoint, **kwargs): + if endpoint == "Login": + return {"SecurityToken": 1234} + elif endpoint == "My": + return {"Account": {"Id": 1}} + elif endpoint == "Accounts/1/Devices": + return devices_dict + return {} + + with patch("pymyq.api.API.request", side_effect=_handle_mock_api_request): + entry = MockConfigEntry( + domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} + ) + entry.add_to_hass(hass) + + if not skip_setup: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/fixtures/myq/devices.json b/tests/fixtures/myq/devices.json new file mode 100644 index 00000000000..f7c65c6bb20 --- /dev/null +++ b/tests/fixtures/myq/devices.json @@ -0,0 +1,133 @@ +{ + "count" : 4, + "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices", + "items" : [ + { + "device_type" : "ethernetgateway", + "created_date" : "2020-02-10T22:54:58.423", + "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "device_family" : "gateway", + "name" : "Happy place", + "device_platform" : "myq", + "state" : { + "homekit_enabled" : false, + "pending_bootload_abandoned" : false, + "online" : true, + "last_status" : "2020-03-30T02:49:46.4121303Z", + "physical_devices" : [], + "firmware_version" : "1.6", + "learn_mode" : false, + "learn" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial/learn", + "homekit_capable" : false, + "updated_date" : "2020-03-30T02:49:46.4171299Z" + }, + "serial_number" : "gateway_serial" + }, + { + "serial_number" : "gate_serial", + "state" : { + "report_ajar" : false, + "aux_relay_delay" : "00:00:00", + "is_unattended_close_allowed" : true, + "door_ajar_interval" : "00:00:00", + "aux_relay_behavior" : "None", + "last_status" : "2020-03-30T02:47:40.2794038Z", + "online" : true, + "rex_fires_door" : false, + "close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/close", + "invalid_shutout_period" : "00:00:00", + "invalid_credential_window" : "00:00:00", + "use_aux_relay" : false, + "command_channel_report_status" : false, + "last_update" : "2020-03-28T23:07:39.5611776Z", + "door_state" : "closed", + "max_invalid_attempts" : 0, + "open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial/open", + "passthrough_interval" : "00:00:00", + "control_from_browser" : false, + "report_forced" : false, + "is_unattended_open_allowed" : true + }, + "parent_device_id" : "gateway_serial", + "name" : "Gate", + "device_platform" : "myq", + "device_family" : "garagedoor", + "parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gate_serial", + "device_type" : "gate", + "created_date" : "2020-02-10T22:54:58.423" + }, + { + "parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial", + "device_type" : "wifigaragedooropener", + "created_date" : "2020-02-10T22:55:25.863", + "device_platform" : "myq", + "name" : "Large Garage Door", + "device_family" : "garagedoor", + "serial_number" : "large_garage_serial", + "state" : { + "report_forced" : false, + "is_unattended_open_allowed" : true, + "passthrough_interval" : "00:00:00", + "control_from_browser" : false, + "attached_work_light_error_present" : false, + "max_invalid_attempts" : 0, + "open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/open", + "command_channel_report_status" : false, + "last_update" : "2020-03-28T23:58:55.5906643Z", + "door_state" : "closed", + "invalid_shutout_period" : "00:00:00", + "use_aux_relay" : false, + "invalid_credential_window" : "00:00:00", + "rex_fires_door" : false, + "close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/large_garage_serial/close", + "online" : true, + "last_status" : "2020-03-30T02:49:46.4121303Z", + "aux_relay_behavior" : "None", + "door_ajar_interval" : "00:00:00", + "gdo_lock_connected" : false, + "report_ajar" : false, + "aux_relay_delay" : "00:00:00", + "is_unattended_close_allowed" : true + }, + "parent_device_id" : "gateway_serial" + }, + { + "serial_number" : "small_garage_serial", + "state" : { + "last_status" : "2020-03-30T02:48:45.7501595Z", + "online" : true, + "report_ajar" : false, + "aux_relay_delay" : "00:00:00", + "is_unattended_close_allowed" : true, + "gdo_lock_connected" : false, + "door_ajar_interval" : "00:00:00", + "aux_relay_behavior" : "None", + "attached_work_light_error_present" : false, + "control_from_browser" : false, + "passthrough_interval" : "00:00:00", + "is_unattended_open_allowed" : true, + "report_forced" : false, + "close" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/close", + "rex_fires_door" : false, + "invalid_credential_window" : "00:00:00", + "use_aux_relay" : false, + "invalid_shutout_period" : "00:00:00", + "door_state" : "closed", + "last_update" : "2020-03-26T15:45:31.4713796Z", + "command_channel_report_status" : false, + "open" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial/open", + "max_invalid_attempts" : 0 + }, + "parent_device_id" : "gateway_serial", + "device_platform" : "myq", + "name" : "Small Garage Door", + "device_family" : "garagedoor", + "parent_device" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/gateway_serial", + "href" : "http://api.myqdevice.com/api/v5/accounts/account_id/devices/small_garage_serial", + "device_type" : "wifigaragedooropener", + "created_date" : "2020-02-10T23:11:47.487" + } + ] +} From f085a0c54a6208216190342a5865f1d2addc98a4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 13:59:03 -0500 Subject: [PATCH 345/431] Retry sense setup later if listing devices times out. (#33455) --- homeassistant/components/sense/__init__.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/sense/__init__.py b/homeassistant/components/sense/__init__.py index 13452c97088..80e75bce400 100644 --- a/homeassistant/components/sense/__init__.py +++ b/homeassistant/components/sense/__init__.py @@ -106,7 +106,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): raise ConfigEntryNotReady sense_devices_data = SenseDevicesData() - sense_discovered_devices = await gateway.get_discovered_device_data() + try: + sense_discovered_devices = await gateway.get_discovered_device_data() + except SENSE_TIMEOUT_EXCEPTIONS: + raise ConfigEntryNotReady hass.data[DOMAIN][entry.entry_id] = { SENSE_DATA: gateway, From 06216a8a45e2cae303eaf4303e22b72a4ed8ea24 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 31 Mar 2020 12:01:31 -0700 Subject: [PATCH 346/431] Google Assistant: parallize as many requests as possible (#33472) * Google Assistant: parallize as many requests as possible * Fix double comment --- .../components/google_assistant/smart_home.py | 55 ++++++++++++++----- .../google_assistant/test_smart_home.py | 3 + 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/google_assistant/smart_home.py b/homeassistant/components/google_assistant/smart_home.py index 97c872bdaf8..55e121e2fc7 100644 --- a/homeassistant/components/google_assistant/smart_home.py +++ b/homeassistant/components/google_assistant/smart_home.py @@ -131,6 +131,24 @@ async def async_devices_query(hass, data, payload): return {"devices": devices} +async def _entity_execute(entity, data, executions): + """Execute all commands for an entity. + + Returns a dict if a special result needs to be set. + """ + for execution in executions: + try: + await entity.execute(data, execution) + except SmartHomeError as err: + return { + "ids": [entity.entity_id], + "status": "ERROR", + **err.to_response(), + } + + return None + + @HANDLERS.register("action.devices.EXECUTE") async def handle_devices_execute(hass, data, payload): """Handle action.devices.EXECUTE request. @@ -138,6 +156,7 @@ async def handle_devices_execute(hass, data, payload): https://developers.google.com/assistant/smarthome/develop/process-intents#EXECUTE """ entities = {} + executions = {} results = {} for command in payload["commands"]: @@ -159,27 +178,33 @@ async def handle_devices_execute(hass, data, payload): if entity_id in results: continue - if entity_id not in entities: - state = hass.states.get(entity_id) + if entity_id in entities: + executions[entity_id].append(execution) + continue - if state is None: - results[entity_id] = { - "ids": [entity_id], - "status": "ERROR", - "errorCode": ERR_DEVICE_OFFLINE, - } - continue + state = hass.states.get(entity_id) - entities[entity_id] = GoogleEntity(hass, data.config, state) - - try: - await entities[entity_id].execute(data, execution) - except SmartHomeError as err: + if state is None: results[entity_id] = { "ids": [entity_id], "status": "ERROR", - **err.to_response(), + "errorCode": ERR_DEVICE_OFFLINE, } + continue + + entities[entity_id] = GoogleEntity(hass, data.config, state) + executions[entity_id] = [execution] + + execute_results = await asyncio.gather( + *[ + _entity_execute(entities[entity_id], data, executions[entity_id]) + for entity_id in executions + ] + ) + + for entity_id, result in zip(executions, execute_results): + if result is not None: + results[entity_id] = result final_results = list(results.values()) diff --git a/tests/components/google_assistant/test_smart_home.py b/tests/components/google_assistant/test_smart_home.py index c08c15a02f4..42002d62906 100644 --- a/tests/components/google_assistant/test_smart_home.py +++ b/tests/components/google_assistant/test_smart_home.py @@ -442,6 +442,9 @@ async def test_execute(hass): "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", From e6ed2f0377fdffca0016ecf083f6c142d60b1920 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 14:55:50 -0500 Subject: [PATCH 347/431] Add version and device type to powerwall device_info (#33453) * Add version and device type to powerwall device_info * Upstream powerwall now supports a http_session --- .../components/powerwall/__init__.py | 38 +++++++++++++------ .../components/powerwall/binary_sensor.py | 10 +++-- .../components/powerwall/config_flow.py | 6 ++- homeassistant/components/powerwall/const.py | 10 ++++- homeassistant/components/powerwall/entity.py | 16 ++++++-- .../components/powerwall/manifest.json | 4 +- homeassistant/components/powerwall/sensor.py | 18 ++++++--- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/powerwall/mocks.py | 15 +++++++- tests/components/powerwall/test_sensor.py | 3 +- tests/fixtures/powerwall/device_type.json | 1 + tests/fixtures/powerwall/status.json | 1 + 13 files changed, 95 insertions(+), 31 deletions(-) create mode 100644 tests/fixtures/powerwall/device_type.json create mode 100644 tests/fixtures/powerwall/status.json diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index a5401206379..d5c7a534180 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -3,6 +3,7 @@ import asyncio from datetime import timedelta import logging +import requests from tesla_powerwall import ( ApiError, MetersResponse, @@ -21,12 +22,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( DOMAIN, POWERWALL_API_CHARGE, + POWERWALL_API_DEVICE_TYPE, POWERWALL_API_GRID_STATUS, POWERWALL_API_METERS, + POWERWALL_API_SITE_INFO, POWERWALL_API_SITEMASTER, + POWERWALL_API_STATUS, POWERWALL_COORDINATOR, + POWERWALL_HTTP_SESSION, POWERWALL_OBJECT, - POWERWALL_SITE_INFO, UPDATE_INTERVAL, ) @@ -62,10 +66,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): entry_id = entry.entry_id hass.data[DOMAIN].setdefault(entry_id, {}) - power_wall = PowerWall(entry.data[CONF_IP_ADDRESS]) + http_session = requests.Session() + power_wall = PowerWall(entry.data[CONF_IP_ADDRESS], http_session=http_session) try: - site_info = await hass.async_add_executor_job(call_site_info, power_wall) + powerwall_data = await hass.async_add_executor_job(call_base_info, power_wall) except (PowerWallUnreachableError, ApiError, ConnectionError): + http_session.close() raise ConfigEntryNotReady async def async_update_data(): @@ -80,11 +86,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): update_interval=timedelta(seconds=UPDATE_INTERVAL), ) - hass.data[DOMAIN][entry.entry_id] = { - POWERWALL_OBJECT: power_wall, - POWERWALL_COORDINATOR: coordinator, - POWERWALL_SITE_INFO: site_info, - } + hass.data[DOMAIN][entry.entry_id] = powerwall_data + hass.data[DOMAIN][entry.entry_id].update( + { + POWERWALL_OBJECT: power_wall, + POWERWALL_COORDINATOR: coordinator, + POWERWALL_HTTP_SESSION: http_session, + } + ) await coordinator.async_refresh() @@ -96,9 +105,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): return True -def call_site_info(power_wall): - """Wrap site_info to be a callable.""" - return power_wall.site_info +def call_base_info(power_wall): + """Wrap powerwall properties to be a callable.""" + return { + POWERWALL_API_SITE_INFO: power_wall.site_info, + POWERWALL_API_STATUS: power_wall.status, + POWERWALL_API_DEVICE_TYPE: power_wall.device_type, + } def _fetch_powerwall_data(power_wall): @@ -124,6 +137,9 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ] ) ) + + hass.data[DOMAIN][entry.entry_id][POWERWALL_HTTP_SESSION].close() + if unload_ok: hass.data[DOMAIN].pop(entry.entry_id) diff --git a/homeassistant/components/powerwall/binary_sensor.py b/homeassistant/components/powerwall/binary_sensor.py index 52b82531472..329b26221b8 100644 --- a/homeassistant/components/powerwall/binary_sensor.py +++ b/homeassistant/components/powerwall/binary_sensor.py @@ -12,13 +12,15 @@ from .const import ( ATTR_NOMINAL_SYSTEM_POWER, ATTR_REGION, DOMAIN, + POWERWALL_API_DEVICE_TYPE, POWERWALL_API_GRID_STATUS, + POWERWALL_API_SITE_INFO, POWERWALL_API_SITEMASTER, + POWERWALL_API_STATUS, POWERWALL_CONNECTED_KEY, POWERWALL_COORDINATOR, POWERWALL_GRID_ONLINE, POWERWALL_RUNNING_KEY, - POWERWALL_SITE_INFO, SITE_INFO_GRID_CODE, SITE_INFO_NOMINAL_SYSTEM_POWER_KW, SITE_INFO_REGION, @@ -33,7 +35,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): powerwall_data = hass.data[DOMAIN][config_entry.entry_id] coordinator = powerwall_data[POWERWALL_COORDINATOR] - site_info = powerwall_data[POWERWALL_SITE_INFO] + site_info = powerwall_data[POWERWALL_API_SITE_INFO] + device_type = powerwall_data[POWERWALL_API_DEVICE_TYPE] + status = powerwall_data[POWERWALL_API_STATUS] entities = [] for sensor_class in ( @@ -41,7 +45,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): PowerWallGridStatusSensor, PowerWallConnectedSensor, ): - entities.append(sensor_class(coordinator, site_info)) + entities.append(sensor_class(coordinator, site_info, status, device_type)) async_add_entities(entities, True) diff --git a/homeassistant/components/powerwall/config_flow.py b/homeassistant/components/powerwall/config_flow.py index e94b0cd4056..7e1b3eb3fb1 100644 --- a/homeassistant/components/powerwall/config_flow.py +++ b/homeassistant/components/powerwall/config_flow.py @@ -7,7 +7,6 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_IP_ADDRESS -from . import call_site_info from .const import DOMAIN # pylint:disable=unused-import from .const import POWERWALL_SITE_NAME @@ -33,6 +32,11 @@ async def validate_input(hass: core.HomeAssistant, data): return {"title": site_info[POWERWALL_SITE_NAME]} +def call_site_info(power_wall): + """Wrap site_info to be a callable.""" + return power_wall.site_info + + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Tesla Powerwall.""" diff --git a/homeassistant/components/powerwall/const.py b/homeassistant/components/powerwall/const.py index 59accc9e9a3..2e9c3739c48 100644 --- a/homeassistant/components/powerwall/const.py +++ b/homeassistant/components/powerwall/const.py @@ -6,7 +6,6 @@ POWERWALL_SITE_NAME = "site_name" POWERWALL_OBJECT = "powerwall" POWERWALL_COORDINATOR = "coordinator" -POWERWALL_SITE_INFO = "site_info" UPDATE_INTERVAL = 60 @@ -24,12 +23,21 @@ SITE_INFO_NOMINAL_SYSTEM_POWER_KW = "nominal_system_power_kW" SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH = "nominal_system_energy_kWh" SITE_INFO_REGION = "region" +DEVICE_TYPE_DEVICE_TYPE = "device_type" + +STATUS_VERSION = "version" + POWERWALL_SITE_NAME = "site_name" POWERWALL_API_METERS = "meters" POWERWALL_API_CHARGE = "charge" POWERWALL_API_GRID_STATUS = "grid_status" POWERWALL_API_SITEMASTER = "sitemaster" +POWERWALL_API_STATUS = "status" +POWERWALL_API_DEVICE_TYPE = "device_type" +POWERWALL_API_SITE_INFO = "site_info" + +POWERWALL_HTTP_SESSION = "http_session" POWERWALL_GRID_ONLINE = "SystemGridConnected" POWERWALL_CONNECTED_KEY = "connected_to_tesla" diff --git a/homeassistant/components/powerwall/entity.py b/homeassistant/components/powerwall/entity.py index 04bb75fd47a..c09a1aca612 100644 --- a/homeassistant/components/powerwall/entity.py +++ b/homeassistant/components/powerwall/entity.py @@ -3,6 +3,7 @@ from homeassistant.helpers.entity import Entity from .const import ( + DEVICE_TYPE_DEVICE_TYPE, DOMAIN, MANUFACTURER, MODEL, @@ -10,17 +11,20 @@ from .const import ( SITE_INFO_GRID_CODE, SITE_INFO_NOMINAL_SYSTEM_ENERGY_KWH, SITE_INFO_UTILITY, + STATUS_VERSION, ) class PowerWallEntity(Entity): """Base class for powerwall entities.""" - def __init__(self, coordinator, site_info): + def __init__(self, coordinator, site_info, status, device_type): """Initialize the sensor.""" super().__init__() self._coordinator = coordinator self._site_info = site_info + self._device_type = device_type.get(DEVICE_TYPE_DEVICE_TYPE) + self._version = status.get(STATUS_VERSION) # This group of properties will be unique to to the site unique_group = ( site_info[SITE_INFO_UTILITY], @@ -32,12 +36,18 @@ class PowerWallEntity(Entity): @property def device_info(self): """Powerwall device info.""" - return { + device_info = { "identifiers": {(DOMAIN, self.base_unique_id)}, "name": self._site_info[POWERWALL_SITE_NAME], "manufacturer": MANUFACTURER, - "model": MODEL, } + model = MODEL + if self._device_type: + model += f" ({self._device_type})" + device_info["model"] = model + if self._version: + device_info["sw_version"] = self._version + return device_info @property def available(self): diff --git a/homeassistant/components/powerwall/manifest.json b/homeassistant/components/powerwall/manifest.json index ed90bc339fc..951ad960e14 100644 --- a/homeassistant/components/powerwall/manifest.json +++ b/homeassistant/components/powerwall/manifest.json @@ -4,7 +4,7 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/powerwall", "requirements": [ - "tesla-powerwall==0.1.1" + "tesla-powerwall==0.1.3" ], "ssdp": [], "zeroconf": [], @@ -13,4 +13,4 @@ "codeowners": [ "@bdraco" ] -} \ No newline at end of file +} diff --git a/homeassistant/components/powerwall/sensor.py b/homeassistant/components/powerwall/sensor.py index 3ebb467d4fc..cf49b36a570 100644 --- a/homeassistant/components/powerwall/sensor.py +++ b/homeassistant/components/powerwall/sensor.py @@ -15,9 +15,11 @@ from .const import ( ATTR_INSTANT_AVERAGE_VOLTAGE, DOMAIN, POWERWALL_API_CHARGE, + POWERWALL_API_DEVICE_TYPE, POWERWALL_API_METERS, + POWERWALL_API_SITE_INFO, + POWERWALL_API_STATUS, POWERWALL_COORDINATOR, - POWERWALL_SITE_INFO, ) from .entity import PowerWallEntity @@ -30,13 +32,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): _LOGGER.debug("Powerwall_data: %s", powerwall_data) coordinator = powerwall_data[POWERWALL_COORDINATOR] - site_info = powerwall_data[POWERWALL_SITE_INFO] + site_info = powerwall_data[POWERWALL_API_SITE_INFO] + device_type = powerwall_data[POWERWALL_API_DEVICE_TYPE] + status = powerwall_data[POWERWALL_API_STATUS] entities = [] for meter in coordinator.data[POWERWALL_API_METERS]: - entities.append(PowerWallEnergySensor(meter, coordinator, site_info)) + entities.append( + PowerWallEnergySensor(meter, coordinator, site_info, status, device_type) + ) - entities.append(PowerWallChargeSensor(coordinator, site_info)) + entities.append(PowerWallChargeSensor(coordinator, site_info, status, device_type)) async_add_entities(entities, True) @@ -73,9 +79,9 @@ class PowerWallChargeSensor(PowerWallEntity): class PowerWallEnergySensor(PowerWallEntity): """Representation of an Powerwall Energy sensor.""" - def __init__(self, meter, coordinator, site_info): + def __init__(self, meter, coordinator, site_info, status, device_type): """Initialize the sensor.""" - super().__init__(coordinator, site_info) + super().__init__(coordinator, site_info, status, device_type) self._meter = meter @property diff --git a/requirements_all.txt b/requirements_all.txt index 46fd27fa1fd..eefa3ce8a02 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2011,7 +2011,7 @@ temperusb==1.5.3 # tensorflow==1.13.2 # homeassistant.components.powerwall -tesla-powerwall==0.1.1 +tesla-powerwall==0.1.3 # homeassistant.components.tesla teslajsonpy==0.6.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 09542dddd54..5dc69a651e6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -729,7 +729,7 @@ sunwatcher==0.2.1 tellduslive==0.10.10 # homeassistant.components.powerwall -tesla-powerwall==0.1.1 +tesla-powerwall==0.1.3 # homeassistant.components.tesla teslajsonpy==0.6.0 diff --git a/tests/components/powerwall/mocks.py b/tests/components/powerwall/mocks.py index 330f1123b8f..aba6ecfeb23 100644 --- a/tests/components/powerwall/mocks.py +++ b/tests/components/powerwall/mocks.py @@ -16,17 +16,28 @@ async def _mock_powerwall_with_fixtures(hass): meters = await _async_load_json_fixture(hass, "meters.json") sitemaster = await _async_load_json_fixture(hass, "sitemaster.json") site_info = await _async_load_json_fixture(hass, "site_info.json") + status = await _async_load_json_fixture(hass, "status.json") + device_type = await _async_load_json_fixture(hass, "device_type.json") + return _mock_powerwall_return_value( site_info=site_info, charge=47.31993232, sitemaster=sitemaster, meters=meters, grid_status="SystemGridConnected", + status=status, + device_type=device_type, ) def _mock_powerwall_return_value( - site_info=None, charge=None, sitemaster=None, meters=None, grid_status=None + site_info=None, + charge=None, + sitemaster=None, + meters=None, + grid_status=None, + status=None, + device_type=None, ): powerwall_mock = MagicMock() type(powerwall_mock).site_info = PropertyMock(return_value=site_info) @@ -34,6 +45,8 @@ def _mock_powerwall_return_value( type(powerwall_mock).sitemaster = PropertyMock(return_value=sitemaster) type(powerwall_mock).meters = PropertyMock(return_value=meters) type(powerwall_mock).grid_status = PropertyMock(return_value=grid_status) + type(powerwall_mock).status = PropertyMock(return_value=status) + type(powerwall_mock).device_type = PropertyMock(return_value=device_type) return powerwall_mock diff --git a/tests/components/powerwall/test_sensor.py b/tests/components/powerwall/test_sensor.py index 090e5dac445..7f092683b7c 100644 --- a/tests/components/powerwall/test_sensor.py +++ b/tests/components/powerwall/test_sensor.py @@ -27,7 +27,8 @@ async def test_sensors(hass): identifiers={("powerwall", "Wom Energy_60Hz_240V_s_IEEE1547a_2014_13.5")}, connections=set(), ) - assert reg_device.model == "PowerWall 2" + assert reg_device.model == "PowerWall 2 (hec)" + assert reg_device.sw_version == "1.45.1" assert reg_device.manufacturer == "Tesla" assert reg_device.name == "MySite" diff --git a/tests/fixtures/powerwall/device_type.json b/tests/fixtures/powerwall/device_type.json new file mode 100644 index 00000000000..a94c047219e --- /dev/null +++ b/tests/fixtures/powerwall/device_type.json @@ -0,0 +1 @@ +{"device_type":"hec"} diff --git a/tests/fixtures/powerwall/status.json b/tests/fixtures/powerwall/status.json new file mode 100644 index 00000000000..41e0288b18d --- /dev/null +++ b/tests/fixtures/powerwall/status.json @@ -0,0 +1 @@ +{"start_time":"2020-03-10 11:57:25 +0800","up_time_seconds":"217h40m57.470801079s","is_new":false,"version":"1.45.1","git_hash":"13bf684a633175f884079ec79f42997080d90310"} From 5047635224ddef7a33635c049ea08e63b5595cc2 Mon Sep 17 00:00:00 2001 From: Raman Gupta <7243222+raman325@users.noreply.github.com> Date: Tue, 31 Mar 2020 16:28:08 -0400 Subject: [PATCH 348/431] update VIZIO name to match brand guidelines (#33465) --- homeassistant/components/vizio/manifest.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/vizio/manifest.json b/homeassistant/components/vizio/manifest.json index 7608b6eae53..885cfacca41 100644 --- a/homeassistant/components/vizio/manifest.json +++ b/homeassistant/components/vizio/manifest.json @@ -1,6 +1,6 @@ { "domain": "vizio", - "name": "Vizio SmartCast", + "name": "VIZIO SmartCast", "documentation": "https://www.home-assistant.io/integrations/vizio", "requirements": ["pyvizio==0.1.44"], "dependencies": [], From be99f3bf32bd689dda1b1c88ffa0c50e8d0fcce0 Mon Sep 17 00:00:00 2001 From: Chris Caron Date: Tue, 31 Mar 2020 16:30:27 -0400 Subject: [PATCH 349/431] Bumped Apprise version to v0.8.5 (#33473) --- 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 0895c2af1f9..ba934b804d7 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.8.4"], + "requirements": ["apprise==0.8.5"], "dependencies": [], "codeowners": ["@caronc"] } diff --git a/requirements_all.txt b/requirements_all.txt index eefa3ce8a02..08f54a3924a 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -250,7 +250,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.4 +apprise==0.8.5 # homeassistant.components.aprs aprslib==0.6.46 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5dc69a651e6..0df53b2e0b6 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -112,7 +112,7 @@ androidtv==0.0.39 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.4 +apprise==0.8.5 # homeassistant.components.aprs aprslib==0.6.46 From a473ae6711de6ebbd915167aa349b702e43001fd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 16:20:29 -0500 Subject: [PATCH 350/431] Ignore link local addresses during doorbird ssdp config flow (#33401) --- .../components/doorbird/.translations/en.json | 66 ++++++++++--------- .../components/doorbird/config_flow.py | 2 + .../components/doorbird/strings.json | 4 +- tests/components/doorbird/test_config_flow.py | 23 +++++++ 4 files changed, 62 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index dc7c2fd0cbe..9b2c95dd7c9 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -1,34 +1,36 @@ { - "config": { - "abort": { - "already_configured": "This DoorBird is already configured" - }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error" - }, - "step": { - "user": { - "data": { - "host": "Host (IP Address)", - "name": "Device Name", - "password": "Password", - "username": "Username" - }, - "title": "Connect to the DoorBird" + "options" : { + "step" : { + "init" : { + "data" : { + "events" : "Comma separated list of events." + }, + "description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } + } + }, + "config" : { + "step" : { + "user" : { + "title" : "Connect to the DoorBird", + "data" : { + "password" : "Password", + "host" : "Host (IP Address)", + "name" : "Device Name", + "username" : "Username" } - }, - "title": "DoorBird" - }, - "options": { - "step": { - "init": { - "data": { - "events": "Comma separated list of events." - }, - "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" - } - } - } -} \ No newline at end of file + } + }, + "abort" : { + "already_configured" : "This DoorBird is already configured", + "link_local_address": "Link local addresses are not supported", + "not_doorbird_device": "This device is not a DoorBird" + }, + "title" : "DoorBird", + "error" : { + "invalid_auth" : "Invalid authentication", + "unknown" : "Unexpected error", + "cannot_connect" : "Failed to connect, please try again" + } + } +} diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 37d46c23a9d..410fb13a212 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -90,6 +90,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") + if discovery_info[CONF_HOST].startswith("169.254"): + return self.async_abort(reason="link_local_address") await self.async_set_unique_id(macaddress) diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index caf3177c681..9b2c95dd7c9 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -22,7 +22,9 @@ } }, "abort" : { - "already_configured" : "This DoorBird is already configured" + "already_configured" : "This DoorBird is already configured", + "link_local_address": "Link local addresses are not supported", + "not_doorbird_device": "This device is not a DoorBird" }, "title" : "DoorBird", "error" : { diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 009062d0193..f911787c1c3 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -140,10 +140,33 @@ async def test_form_zeroconf_wrong_oui(hass): context={"source": config_entries.SOURCE_ZEROCONF}, data={ "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" + + +async def test_form_zeroconf_link_local_ignored(hass): + """Test we abort when we get a link local address via zeroconf.""" + await hass.async_add_executor_job( + init_recorder_component, hass + ) # force in memory db + + await setup.async_setup_component(hass, "persistent_notification", {}) + + 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.", + }, + ) + assert result["type"] == "abort" + assert result["reason"] == "link_local_address" async def test_form_zeroconf_correct_oui(hass): From 90dd796644e459c7dd14df630d49c264949e46b9 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 16:46:30 -0500 Subject: [PATCH 351/431] Prepare rachio for cloudhooks conversion (#33422) Reorganize code in order to prepare for webhooks --- homeassistant/components/rachio/__init__.py | 286 +----------------- .../components/rachio/binary_sensor.py | 37 +-- homeassistant/components/rachio/const.py | 13 + homeassistant/components/rachio/device.py | 180 +++++++++++ homeassistant/components/rachio/entity.py | 33 ++ homeassistant/components/rachio/switch.py | 51 ++-- homeassistant/components/rachio/webhooks.py | 96 ++++++ 7 files changed, 377 insertions(+), 319 deletions(-) create mode 100644 homeassistant/components/rachio/device.py create mode 100644 homeassistant/components/rachio/entity.py create mode 100644 homeassistant/components/rachio/webhooks.py diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 7eaa76dedd4..9bd3b16d12c 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -2,41 +2,25 @@ import asyncio import logging import secrets -from typing import Optional -from aiohttp import web from rachiopy import Rachio import voluptuous as vol -from homeassistant.components.http import HomeAssistantView from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_API_KEY, EVENT_HOMEASSISTANT_STOP, URL_API +from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from homeassistant.helpers import config_validation as cv, device_registry -from homeassistant.helpers.dispatcher import async_dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers import config_validation as cv from .const import ( CONF_CUSTOM_URL, CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, - DEFAULT_NAME, DOMAIN, - KEY_DEVICES, - KEY_ENABLED, - KEY_EXTERNAL_ID, - KEY_ID, - KEY_MAC_ADDRESS, - KEY_MODEL, - KEY_NAME, - KEY_SERIAL_NUMBER, - KEY_STATUS, - KEY_TYPE, - KEY_USERNAME, - KEY_ZONES, RACHIO_API_EXCEPTIONS, ) +from .device import RachioPerson +from .webhooks import WEBHOOK_PATH, RachioWebhookView _LOGGER = logging.getLogger(__name__) @@ -58,51 +42,6 @@ CONFIG_SCHEMA = vol.Schema( ) -STATUS_ONLINE = "ONLINE" -STATUS_OFFLINE = "OFFLINE" - -# Device webhook values -TYPE_CONTROLLER_STATUS = "DEVICE_STATUS" -SUBTYPE_OFFLINE = "OFFLINE" -SUBTYPE_ONLINE = "ONLINE" -SUBTYPE_OFFLINE_NOTIFICATION = "OFFLINE_NOTIFICATION" -SUBTYPE_COLD_REBOOT = "COLD_REBOOT" -SUBTYPE_SLEEP_MODE_ON = "SLEEP_MODE_ON" -SUBTYPE_SLEEP_MODE_OFF = "SLEEP_MODE_OFF" -SUBTYPE_BROWNOUT_VALVE = "BROWNOUT_VALVE" -SUBTYPE_RAIN_SENSOR_DETECTION_ON = "RAIN_SENSOR_DETECTION_ON" -SUBTYPE_RAIN_SENSOR_DETECTION_OFF = "RAIN_SENSOR_DETECTION_OFF" -SUBTYPE_RAIN_DELAY_ON = "RAIN_DELAY_ON" -SUBTYPE_RAIN_DELAY_OFF = "RAIN_DELAY_OFF" - -# Schedule webhook values -TYPE_SCHEDULE_STATUS = "SCHEDULE_STATUS" -SUBTYPE_SCHEDULE_STARTED = "SCHEDULE_STARTED" -SUBTYPE_SCHEDULE_STOPPED = "SCHEDULE_STOPPED" -SUBTYPE_SCHEDULE_COMPLETED = "SCHEDULE_COMPLETED" -SUBTYPE_WEATHER_NO_SKIP = "WEATHER_INTELLIGENCE_NO_SKIP" -SUBTYPE_WEATHER_SKIP = "WEATHER_INTELLIGENCE_SKIP" -SUBTYPE_WEATHER_CLIMATE_SKIP = "WEATHER_INTELLIGENCE_CLIMATE_SKIP" -SUBTYPE_WEATHER_FREEZE = "WEATHER_INTELLIGENCE_FREEZE" - -# Zone webhook values -TYPE_ZONE_STATUS = "ZONE_STATUS" -SUBTYPE_ZONE_STARTED = "ZONE_STARTED" -SUBTYPE_ZONE_STOPPED = "ZONE_STOPPED" -SUBTYPE_ZONE_COMPLETED = "ZONE_COMPLETED" -SUBTYPE_ZONE_CYCLING = "ZONE_CYCLING" -SUBTYPE_ZONE_CYCLING_COMPLETED = "ZONE_CYCLING_COMPLETED" - -# Webhook callbacks -LISTEN_EVENT_TYPES = ["DEVICE_STATUS_EVENT", "ZONE_STATUS_EVENT"] -WEBHOOK_CONST_ID = "homeassistant.rachio:" -WEBHOOK_PATH = URL_API + DOMAIN -SIGNAL_RACHIO_UPDATE = DOMAIN + "_update" -SIGNAL_RACHIO_CONTROLLER_UPDATE = SIGNAL_RACHIO_UPDATE + "_controller" -SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + "_zone" -SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule" - - async def async_setup(hass: HomeAssistant, config: dict): """Set up the rachio component from YAML.""" @@ -189,220 +128,3 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) return True - - -class RachioPerson: - """Represent a Rachio user.""" - - def __init__(self, rachio, config_entry): - """Create an object from the provided API instance.""" - # Use API token to get user ID - self.rachio = rachio - self.config_entry = config_entry - self.username = None - self._id = None - self._controllers = [] - - def setup(self, hass): - """Rachio device setup.""" - response = self.rachio.person.getInfo() - assert int(response[0][KEY_STATUS]) == 200, "API key error" - self._id = response[1][KEY_ID] - - # Use user ID to get user data - data = self.rachio.person.get(self._id) - assert int(data[0][KEY_STATUS]) == 200, "User ID error" - self.username = data[1][KEY_USERNAME] - devices = data[1][KEY_DEVICES] - for controller in devices: - webhooks = self.rachio.notification.getDeviceWebhook(controller[KEY_ID])[1] - # The API does not provide a way to tell if a controller is shared - # or if they are the owner. To work around this problem we fetch the webooks - # before we setup the device so we can skip it instead of failing. - # webhooks are normally a list, however if there is an error - # rachio hands us back a dict - if isinstance(webhooks, dict): - _LOGGER.error( - "Failed to add rachio controller '%s' because of an error: %s", - controller[KEY_NAME], - webhooks.get("error", "Unknown Error"), - ) - continue - - rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) - rachio_iro.setup() - self._controllers.append(rachio_iro) - _LOGGER.info('Using Rachio API as user "%s"', self.username) - - @property - def user_id(self) -> str: - """Get the user ID as defined by the Rachio API.""" - return self._id - - @property - def controllers(self) -> list: - """Get a list of controllers managed by this account.""" - return self._controllers - - -class RachioIro: - """Represent a Rachio Iro.""" - - def __init__(self, hass, rachio, data, webhooks): - """Initialize a Rachio device.""" - self.hass = hass - self.rachio = rachio - self._id = data[KEY_ID] - self.name = data[KEY_NAME] - self.serial_number = data[KEY_SERIAL_NUMBER] - self.mac_address = data[KEY_MAC_ADDRESS] - self.model = data[KEY_MODEL] - self._zones = data[KEY_ZONES] - self._init_data = data - self._webhooks = webhooks - _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) - - def setup(self): - """Rachio Iro setup for webhooks.""" - # Listen for all updates - self._init_webhooks() - - def _init_webhooks(self) -> None: - """Start getting updates from the Rachio API.""" - current_webhook_id = None - - # First delete any old webhooks that may have stuck around - def _deinit_webhooks(event) -> None: - """Stop getting updates from the Rachio API.""" - if not self._webhooks: - # We fetched webhooks when we created the device, however if we call _init_webhooks - # again we need to fetch again - self._webhooks = self.rachio.notification.getDeviceWebhook( - self.controller_id - )[1] - for webhook in self._webhooks: - if ( - webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) - or webhook[KEY_ID] == current_webhook_id - ): - self.rachio.notification.deleteWebhook(webhook[KEY_ID]) - self._webhooks = None - - _deinit_webhooks(None) - - # Choose which events to listen for and get their IDs - event_types = [] - for event_type in self.rachio.notification.getWebhookEventType()[1]: - if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: - event_types.append({"id": event_type[KEY_ID]}) - - # Register to listen to these events from the device - url = self.rachio.webhook_url - auth = WEBHOOK_CONST_ID + self.rachio.webhook_auth - new_webhook = self.rachio.notification.postWebhook( - self.controller_id, auth, url, event_types - ) - # Save ID for deletion at shutdown - current_webhook_id = new_webhook[1][KEY_ID] - self.hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _deinit_webhooks) - - def __str__(self) -> str: - """Display the controller as a string.""" - return f'Rachio controller "{self.name}"' - - @property - def controller_id(self) -> str: - """Return the Rachio API controller ID.""" - return self._id - - @property - def current_schedule(self) -> str: - """Return the schedule that the device is running right now.""" - return self.rachio.device.getCurrentSchedule(self.controller_id)[1] - - @property - def init_data(self) -> dict: - """Return the information used to set up the controller.""" - return self._init_data - - def list_zones(self, include_disabled=False) -> list: - """Return a list of the zone dicts connected to the device.""" - # All zones - if include_disabled: - return self._zones - - # Only enabled zones - return [z for z in self._zones if z[KEY_ENABLED]] - - def get_zone(self, zone_id) -> Optional[dict]: - """Return the zone with the given ID.""" - for zone in self.list_zones(include_disabled=True): - if zone[KEY_ID] == zone_id: - return zone - - return None - - def stop_watering(self) -> None: - """Stop watering all zones connected to this controller.""" - self.rachio.device.stopWater(self.controller_id) - _LOGGER.info("Stopped watering of all zones on %s", str(self)) - - -class RachioDeviceInfoProvider(Entity): - """Mixin to provide device_info.""" - - def __init__(self, controller): - """Initialize a Rachio device.""" - super().__init__() - self._controller = controller - - @property - def device_info(self): - """Return the device_info of the device.""" - return { - "identifiers": {(DOMAIN, self._controller.serial_number,)}, - "connections": { - (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) - }, - "name": self._controller.name, - "model": self._controller.model, - "manufacturer": DEFAULT_NAME, - } - - -class RachioWebhookView(HomeAssistantView): - """Provide a page for the server to call.""" - - SIGNALS = { - TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, - TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, - TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, - } - - requires_auth = False # Handled separately - - def __init__(self, entry_id, webhook_url): - """Initialize the instance of the view.""" - self._entry_id = entry_id - self.url = webhook_url - self.name = webhook_url[1:].replace("/", ":") - _LOGGER.debug( - "Initialize webhook at url: %s, with name %s", self.url, self.name - ) - - async def post(self, request) -> web.Response: - """Handle webhook calls from the server.""" - hass = request.app["hass"] - data = await request.json() - - try: - auth = data.get(KEY_EXTERNAL_ID, str()).split(":")[1] - assert auth == hass.data[DOMAIN][self._entry_id].rachio.webhook_auth - except (AssertionError, IndexError): - return web.Response(status=web.HTTPForbidden.status_code) - - update_type = data[KEY_TYPE] - if update_type in self.SIGNALS: - async_dispatcher_send(hass, self.SIGNALS[update_type], data) - - return web.Response(status=web.HTTPNoContent.status_code) diff --git a/homeassistant/components/rachio/binary_sensor.py b/homeassistant/components/rachio/binary_sensor.py index 43ee9650163..ab3a0b91276 100644 --- a/homeassistant/components/rachio/binary_sensor.py +++ b/homeassistant/components/rachio/binary_sensor.py @@ -2,18 +2,23 @@ from abc import abstractmethod import logging -from homeassistant.components.binary_sensor import BinarySensorDevice +from homeassistant.components.binary_sensor import ( + DEVICE_CLASS_CONNECTIVITY, + BinarySensorDevice, +) from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ( +from .const import ( + DOMAIN as DOMAIN_RACHIO, + KEY_DEVICE_ID, + KEY_STATUS, + KEY_SUBTYPE, SIGNAL_RACHIO_CONTROLLER_UPDATE, STATUS_OFFLINE, STATUS_ONLINE, - SUBTYPE_OFFLINE, - SUBTYPE_ONLINE, - RachioDeviceInfoProvider, ) -from .const import DOMAIN as DOMAIN_RACHIO, KEY_DEVICE_ID, KEY_STATUS, KEY_SUBTYPE +from .entity import RachioDevice +from .webhooks import SUBTYPE_OFFLINE, SUBTYPE_ONLINE _LOGGER = logging.getLogger(__name__) @@ -32,23 +37,18 @@ def _create_entities(hass, config_entry): return entities -class RachioControllerBinarySensor(RachioDeviceInfoProvider, BinarySensorDevice): +class RachioControllerBinarySensor(RachioDevice, BinarySensorDevice): """Represent a binary sensor that reflects a Rachio state.""" def __init__(self, controller, poll=True): """Set up a new Rachio controller binary sensor.""" super().__init__(controller) - + self._undo_dispatcher = None if poll: self._state = self._poll_update() else: self._state = None - @property - def should_poll(self) -> bool: - """Declare that this entity pushes its state to HA.""" - return False - @property def is_on(self) -> bool: """Return whether the sensor has a 'true' value.""" @@ -66,19 +66,22 @@ class RachioControllerBinarySensor(RachioDeviceInfoProvider, BinarySensorDevice) @abstractmethod def _poll_update(self, data=None) -> bool: """Request the state from the API.""" - pass @abstractmethod def _handle_update(self, *args, **kwargs) -> None: """Handle an update to the state of this sensor.""" - pass async def async_added_to_hass(self): """Subscribe to updates.""" - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_RACHIO_CONTROLLER_UPDATE, self._handle_any_update ) + async def async_will_remove_from_hass(self): + """Unsubscribe from updates.""" + if self._undo_dispatcher: + self._undo_dispatcher() + class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): """Represent a binary sensor that reflects if the controller is online.""" @@ -101,7 +104,7 @@ class RachioControllerOnlineBinarySensor(RachioControllerBinarySensor): @property def device_class(self) -> str: """Return the class of this device, from component DEVICE_CLASSES.""" - return "connectivity" + return DEVICE_CLASS_CONNECTIVITY @property def icon(self) -> str: diff --git a/homeassistant/components/rachio/const.py b/homeassistant/components/rachio/const.py index fb66d4378f1..13e8029b512 100644 --- a/homeassistant/components/rachio/const.py +++ b/homeassistant/components/rachio/const.py @@ -33,6 +33,11 @@ KEY_USERNAME = "username" KEY_ZONE_ID = "zoneId" KEY_ZONE_NUMBER = "zoneNumber" KEY_ZONES = "zones" +KEY_CUSTOM_SHADE = "customShade" +KEY_CUSTOM_CROP = "customCrop" + +ATTR_ZONE_TYPE = "type" +ATTR_ZONE_SHADE = "shade" # Yes we really do get all these exceptions (hopefully rachiopy switches to requests) RACHIO_API_EXCEPTIONS = ( @@ -41,3 +46,11 @@ RACHIO_API_EXCEPTIONS = ( OSError, AssertionError, ) + +STATUS_ONLINE = "ONLINE" +STATUS_OFFLINE = "OFFLINE" + +SIGNAL_RACHIO_UPDATE = DOMAIN + "_update" +SIGNAL_RACHIO_CONTROLLER_UPDATE = SIGNAL_RACHIO_UPDATE + "_controller" +SIGNAL_RACHIO_ZONE_UPDATE = SIGNAL_RACHIO_UPDATE + "_zone" +SIGNAL_RACHIO_SCHEDULE_UPDATE = SIGNAL_RACHIO_UPDATE + "_schedule" diff --git a/homeassistant/components/rachio/device.py b/homeassistant/components/rachio/device.py new file mode 100644 index 00000000000..949957ae8ec --- /dev/null +++ b/homeassistant/components/rachio/device.py @@ -0,0 +1,180 @@ +"""Adapter to wrap the rachiopy api for home assistant.""" + +import logging +from typing import Optional + +from homeassistant.const import EVENT_HOMEASSISTANT_STOP + +from .const import ( + KEY_DEVICES, + KEY_ENABLED, + KEY_EXTERNAL_ID, + KEY_ID, + KEY_MAC_ADDRESS, + KEY_MODEL, + KEY_NAME, + KEY_SERIAL_NUMBER, + KEY_STATUS, + KEY_USERNAME, + KEY_ZONES, +) +from .webhooks import LISTEN_EVENT_TYPES, WEBHOOK_CONST_ID + +_LOGGER = logging.getLogger(__name__) + + +class RachioPerson: + """Represent a Rachio user.""" + + def __init__(self, rachio, config_entry): + """Create an object from the provided API instance.""" + # Use API token to get user ID + self.rachio = rachio + self.config_entry = config_entry + self.username = None + self._id = None + self._controllers = [] + + def setup(self, hass): + """Rachio device setup.""" + response = self.rachio.person.getInfo() + assert int(response[0][KEY_STATUS]) == 200, "API key error" + self._id = response[1][KEY_ID] + + # Use user ID to get user data + data = self.rachio.person.get(self._id) + assert int(data[0][KEY_STATUS]) == 200, "User ID error" + self.username = data[1][KEY_USERNAME] + devices = data[1][KEY_DEVICES] + for controller in devices: + webhooks = self.rachio.notification.getDeviceWebhook(controller[KEY_ID])[1] + # The API does not provide a way to tell if a controller is shared + # or if they are the owner. To work around this problem we fetch the webooks + # before we setup the device so we can skip it instead of failing. + # webhooks are normally a list, however if there is an error + # rachio hands us back a dict + if isinstance(webhooks, dict): + _LOGGER.error( + "Failed to add rachio controller '%s' because of an error: %s", + controller[KEY_NAME], + webhooks.get("error", "Unknown Error"), + ) + continue + + rachio_iro = RachioIro(hass, self.rachio, controller, webhooks) + rachio_iro.setup() + self._controllers.append(rachio_iro) + _LOGGER.info('Using Rachio API as user "%s"', self.username) + + @property + def user_id(self) -> str: + """Get the user ID as defined by the Rachio API.""" + return self._id + + @property + def controllers(self) -> list: + """Get a list of controllers managed by this account.""" + return self._controllers + + +class RachioIro: + """Represent a Rachio Iro.""" + + def __init__(self, hass, rachio, data, webhooks): + """Initialize a Rachio device.""" + self.hass = hass + self.rachio = rachio + self._id = data[KEY_ID] + self.name = data[KEY_NAME] + self.serial_number = data[KEY_SERIAL_NUMBER] + self.mac_address = data[KEY_MAC_ADDRESS] + self.model = data[KEY_MODEL] + self._zones = data[KEY_ZONES] + self._init_data = data + self._webhooks = webhooks + _LOGGER.debug('%s has ID "%s"', str(self), self.controller_id) + + def setup(self): + """Rachio Iro setup for webhooks.""" + # Listen for all updates + self._init_webhooks() + + def _init_webhooks(self) -> None: + """Start getting updates from the Rachio API.""" + current_webhook_id = None + + # First delete any old webhooks that may have stuck around + def _deinit_webhooks(_) -> None: + """Stop getting updates from the Rachio API.""" + if not self._webhooks: + # We fetched webhooks when we created the device, however if we call _init_webhooks + # again we need to fetch again + self._webhooks = self.rachio.notification.getDeviceWebhook( + self.controller_id + )[1] + for webhook in self._webhooks: + if ( + webhook[KEY_EXTERNAL_ID].startswith(WEBHOOK_CONST_ID) + or webhook[KEY_ID] == current_webhook_id + ): + self.rachio.notification.deleteWebhook(webhook[KEY_ID]) + self._webhooks = None + + _deinit_webhooks(None) + + # Choose which events to listen for and get their IDs + event_types = [] + for event_type in self.rachio.notification.getWebhookEventType()[1]: + if event_type[KEY_NAME] in LISTEN_EVENT_TYPES: + event_types.append({"id": event_type[KEY_ID]}) + + # Register to listen to these events from the device + url = self.rachio.webhook_url + auth = WEBHOOK_CONST_ID + self.rachio.webhook_auth + new_webhook = self.rachio.notification.postWebhook( + self.controller_id, auth, url, event_types + ) + # Save ID for deletion at shutdown + current_webhook_id = new_webhook[1][KEY_ID] + self.hass.bus.listen(EVENT_HOMEASSISTANT_STOP, _deinit_webhooks) + + def __str__(self) -> str: + """Display the controller as a string.""" + return f'Rachio controller "{self.name}"' + + @property + def controller_id(self) -> str: + """Return the Rachio API controller ID.""" + return self._id + + @property + def current_schedule(self) -> str: + """Return the schedule that the device is running right now.""" + return self.rachio.device.getCurrentSchedule(self.controller_id)[1] + + @property + def init_data(self) -> dict: + """Return the information used to set up the controller.""" + return self._init_data + + def list_zones(self, include_disabled=False) -> list: + """Return a list of the zone dicts connected to the device.""" + # All zones + if include_disabled: + return self._zones + + # Only enabled zones + return [z for z in self._zones if z[KEY_ENABLED]] + + def get_zone(self, zone_id) -> Optional[dict]: + """Return the zone with the given ID.""" + for zone in self.list_zones(include_disabled=True): + if zone[KEY_ID] == zone_id: + return zone + + return None + + def stop_watering(self) -> None: + """Stop watering all zones connected to this controller.""" + self.rachio.device.stopWater(self.controller_id) + _LOGGER.info("Stopped watering of all zones on %s", str(self)) diff --git a/homeassistant/components/rachio/entity.py b/homeassistant/components/rachio/entity.py new file mode 100644 index 00000000000..379c4e785e5 --- /dev/null +++ b/homeassistant/components/rachio/entity.py @@ -0,0 +1,33 @@ +"""Adapter to wrap the rachiopy api for home assistant.""" + +from homeassistant.helpers import device_registry +from homeassistant.helpers.entity import Entity + +from .const import DEFAULT_NAME, DOMAIN + + +class RachioDevice(Entity): + """Base class for rachio devices.""" + + def __init__(self, controller): + """Initialize a Rachio device.""" + super().__init__() + self._controller = controller + + @property + def should_poll(self) -> bool: + """Declare that this entity pushes its state to HA.""" + return False + + @property + def device_info(self): + """Return the device_info of the device.""" + return { + "identifiers": {(DOMAIN, self._controller.serial_number,)}, + "connections": { + (device_registry.CONNECTION_NETWORK_MAC, self._controller.mac_address,) + }, + "name": self._controller.name, + "model": self._controller.model, + "manufacturer": DEFAULT_NAME, + } diff --git a/homeassistant/components/rachio/switch.py b/homeassistant/components/rachio/switch.py index 5320d434d00..5df084a11a4 100644 --- a/homeassistant/components/rachio/switch.py +++ b/homeassistant/components/rachio/switch.py @@ -6,20 +6,14 @@ import logging from homeassistant.components.switch import SwitchDevice from homeassistant.helpers.dispatcher import async_dispatcher_connect -from . import ( - SIGNAL_RACHIO_CONTROLLER_UPDATE, - SIGNAL_RACHIO_ZONE_UPDATE, - SUBTYPE_SLEEP_MODE_OFF, - SUBTYPE_SLEEP_MODE_ON, - SUBTYPE_ZONE_COMPLETED, - SUBTYPE_ZONE_STARTED, - SUBTYPE_ZONE_STOPPED, - RachioDeviceInfoProvider, -) from .const import ( + ATTR_ZONE_SHADE, + ATTR_ZONE_TYPE, CONF_MANUAL_RUN_MINS, DEFAULT_MANUAL_RUN_MINS, DOMAIN as DOMAIN_RACHIO, + KEY_CUSTOM_CROP, + KEY_CUSTOM_SHADE, KEY_DEVICE_ID, KEY_ENABLED, KEY_ID, @@ -30,6 +24,16 @@ from .const import ( KEY_SUMMARY, KEY_ZONE_ID, KEY_ZONE_NUMBER, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_ZONE_UPDATE, +) +from .entity import RachioDevice +from .webhooks import ( + SUBTYPE_SLEEP_MODE_OFF, + SUBTYPE_SLEEP_MODE_ON, + SUBTYPE_ZONE_COMPLETED, + SUBTYPE_ZONE_STARTED, + SUBTYPE_ZONE_STOPPED, ) _LOGGER = logging.getLogger(__name__) @@ -62,7 +66,7 @@ def _create_entities(hass, config_entry): return entities -class RachioSwitch(RachioDeviceInfoProvider, SwitchDevice): +class RachioSwitch(RachioDevice, SwitchDevice): """Represent a Rachio state that can be toggled.""" def __init__(self, controller, poll=True): @@ -74,11 +78,6 @@ class RachioSwitch(RachioDeviceInfoProvider, SwitchDevice): else: self._state = None - @property - def should_poll(self) -> bool: - """Declare that this entity pushes its state to HA.""" - return False - @property def name(self) -> str: """Get a name for this switch.""" @@ -92,7 +91,6 @@ class RachioSwitch(RachioDeviceInfoProvider, SwitchDevice): @abstractmethod def _poll_update(self, data=None) -> bool: """Poll the API.""" - pass def _handle_any_update(self, *args, **kwargs) -> None: """Determine whether an update event applies to this device.""" @@ -106,7 +104,6 @@ class RachioSwitch(RachioDeviceInfoProvider, SwitchDevice): @abstractmethod def _handle_update(self, *args, **kwargs) -> None: """Handle incoming webhook data.""" - pass class RachioStandbySwitch(RachioSwitch): @@ -169,15 +166,19 @@ class RachioZone(RachioSwitch): def __init__(self, person, controller, data, current_schedule): """Initialize a new Rachio Zone.""" self._id = data[KEY_ID] + _LOGGER.debug("zone_data: %s", data) self._zone_name = data[KEY_NAME] self._zone_number = data[KEY_ZONE_NUMBER] self._zone_enabled = data[KEY_ENABLED] self._entity_picture = data.get(KEY_IMAGE_URL) self._person = person + self._shade_type = data.get(KEY_CUSTOM_SHADE, {}).get(KEY_NAME) + self._zone_type = data.get(KEY_CUSTOM_CROP, {}).get(KEY_NAME) self._summary = str() self._current_schedule = current_schedule super().__init__(controller, poll=False) self._state = self.zone_id == self._current_schedule.get(KEY_ZONE_ID) + self._undo_dispatcher = None def __str__(self): """Display the zone as a string.""" @@ -216,7 +217,12 @@ class RachioZone(RachioSwitch): @property def state_attributes(self) -> dict: """Return the optional state attributes.""" - return {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary} + props = {ATTR_ZONE_NUMBER: self._zone_number, ATTR_ZONE_SUMMARY: self._summary} + if self._shade_type: + props[ATTR_ZONE_SHADE] = self._shade_type + if self._zone_type: + props[ATTR_ZONE_TYPE] = self._zone_type + return props def turn_on(self, **kwargs) -> None: """Start watering this zone.""" @@ -262,6 +268,11 @@ class RachioZone(RachioSwitch): async def async_added_to_hass(self): """Subscribe to updates.""" - async_dispatcher_connect( + self._undo_dispatcher = async_dispatcher_connect( self.hass, SIGNAL_RACHIO_ZONE_UPDATE, self._handle_update ) + + async def async_will_remove_from_hass(self): + """Unsubscribe from updates.""" + if self._undo_dispatcher: + self._undo_dispatcher() diff --git a/homeassistant/components/rachio/webhooks.py b/homeassistant/components/rachio/webhooks.py new file mode 100644 index 00000000000..c12f2ccfd3e --- /dev/null +++ b/homeassistant/components/rachio/webhooks.py @@ -0,0 +1,96 @@ +"""Webhooks used by rachio.""" + +import logging + +from aiohttp import web + +from homeassistant.components.http import HomeAssistantView +from homeassistant.const import URL_API +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from .const import ( + DOMAIN, + KEY_EXTERNAL_ID, + KEY_TYPE, + SIGNAL_RACHIO_CONTROLLER_UPDATE, + SIGNAL_RACHIO_SCHEDULE_UPDATE, + SIGNAL_RACHIO_ZONE_UPDATE, +) + +# Device webhook values +TYPE_CONTROLLER_STATUS = "DEVICE_STATUS" +SUBTYPE_OFFLINE = "OFFLINE" +SUBTYPE_ONLINE = "ONLINE" +SUBTYPE_OFFLINE_NOTIFICATION = "OFFLINE_NOTIFICATION" +SUBTYPE_COLD_REBOOT = "COLD_REBOOT" +SUBTYPE_SLEEP_MODE_ON = "SLEEP_MODE_ON" +SUBTYPE_SLEEP_MODE_OFF = "SLEEP_MODE_OFF" +SUBTYPE_BROWNOUT_VALVE = "BROWNOUT_VALVE" +SUBTYPE_RAIN_SENSOR_DETECTION_ON = "RAIN_SENSOR_DETECTION_ON" +SUBTYPE_RAIN_SENSOR_DETECTION_OFF = "RAIN_SENSOR_DETECTION_OFF" +SUBTYPE_RAIN_DELAY_ON = "RAIN_DELAY_ON" +SUBTYPE_RAIN_DELAY_OFF = "RAIN_DELAY_OFF" + +# Schedule webhook values +TYPE_SCHEDULE_STATUS = "SCHEDULE_STATUS" +SUBTYPE_SCHEDULE_STARTED = "SCHEDULE_STARTED" +SUBTYPE_SCHEDULE_STOPPED = "SCHEDULE_STOPPED" +SUBTYPE_SCHEDULE_COMPLETED = "SCHEDULE_COMPLETED" +SUBTYPE_WEATHER_NO_SKIP = "WEATHER_INTELLIGENCE_NO_SKIP" +SUBTYPE_WEATHER_SKIP = "WEATHER_INTELLIGENCE_SKIP" +SUBTYPE_WEATHER_CLIMATE_SKIP = "WEATHER_INTELLIGENCE_CLIMATE_SKIP" +SUBTYPE_WEATHER_FREEZE = "WEATHER_INTELLIGENCE_FREEZE" + +# Zone webhook values +TYPE_ZONE_STATUS = "ZONE_STATUS" +SUBTYPE_ZONE_STARTED = "ZONE_STARTED" +SUBTYPE_ZONE_STOPPED = "ZONE_STOPPED" +SUBTYPE_ZONE_COMPLETED = "ZONE_COMPLETED" +SUBTYPE_ZONE_CYCLING = "ZONE_CYCLING" +SUBTYPE_ZONE_CYCLING_COMPLETED = "ZONE_CYCLING_COMPLETED" + +# Webhook callbacks +LISTEN_EVENT_TYPES = ["DEVICE_STATUS_EVENT", "ZONE_STATUS_EVENT"] +WEBHOOK_CONST_ID = "homeassistant.rachio:" +WEBHOOK_PATH = URL_API + DOMAIN + +SIGNAL_MAP = { + TYPE_CONTROLLER_STATUS: SIGNAL_RACHIO_CONTROLLER_UPDATE, + TYPE_SCHEDULE_STATUS: SIGNAL_RACHIO_SCHEDULE_UPDATE, + TYPE_ZONE_STATUS: SIGNAL_RACHIO_ZONE_UPDATE, +} + + +_LOGGER = logging.getLogger(__name__) + + +class RachioWebhookView(HomeAssistantView): + """Provide a page for the server to call.""" + + requires_auth = False # Handled separately + + def __init__(self, entry_id, webhook_url): + """Initialize the instance of the view.""" + self._entry_id = entry_id + self.url = webhook_url + self.name = webhook_url[1:].replace("/", ":") + _LOGGER.debug( + "Initialize webhook at url: %s, with name %s", self.url, self.name + ) + + async def post(self, request) -> web.Response: + """Handle webhook calls from the server.""" + hass = request.app["hass"] + data = await request.json() + + try: + auth = data.get(KEY_EXTERNAL_ID, str()).split(":")[1] + assert auth == hass.data[DOMAIN][self._entry_id].rachio.webhook_auth + except (AssertionError, IndexError): + return web.Response(status=web.HTTPForbidden.status_code) + + update_type = data[KEY_TYPE] + if update_type in SIGNAL_MAP: + async_dispatcher_send(hass, SIGNAL_MAP[update_type], data) + + return web.Response(status=web.HTTPNoContent.status_code) From 774b1d1663ce1ce59a729d3f49f6cc35405cbc70 Mon Sep 17 00:00:00 2001 From: Ron Klinkien Date: Wed, 1 Apr 2020 00:22:20 +0200 Subject: [PATCH 352/431] Enable KNX tunnel auto_reconnect by default (#33387) * Added tunnel reconnect functionality * Code improvements * Update homeassistant/components/knx/__init__.py Co-Authored-By: springstan <46536646+springstan@users.noreply.github.com> * Enable auto_reconnect for tunnels by default Co-authored-by: springstan <46536646+springstan@users.noreply.github.com> --- homeassistant/components/knx/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/knx/__init__.py b/homeassistant/components/knx/__init__.py index edd42678a1f..c302188ff20 100644 --- a/homeassistant/components/knx/__init__.py +++ b/homeassistant/components/knx/__init__.py @@ -205,7 +205,7 @@ class KNXModule: def connection_config_tunneling(self): """Return the connection_config if tunneling is configured.""" - gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_HOST) + gateway_ip = self.config[DOMAIN][CONF_KNX_TUNNELING][CONF_HOST] gateway_port = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_PORT) local_ip = self.config[DOMAIN][CONF_KNX_TUNNELING].get(CONF_KNX_LOCAL_IP) if gateway_port is None: @@ -215,6 +215,7 @@ class KNXModule: gateway_ip=gateway_ip, gateway_port=gateway_port, local_ip=local_ip, + auto_reconnect=True, ) def connection_config_auto(self): From 3566803d2ef17cd5883971ba1ab70c1885833f43 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 17:29:45 -0500 Subject: [PATCH 353/431] Fix setting zone overlays for tados that support swing (#33439) * Fix setting zone overlays for tados that support swing * Support for changing swing mode will come at a later time as another upstream update is required. * remove debug * style --- homeassistant/components/tado/__init__.py | 7 +- homeassistant/components/tado/climate.py | 46 +++++++++++-- homeassistant/components/tado/const.py | 4 ++ homeassistant/components/tado/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/tado/test_climate.py | 29 +++++++++ tests/components/tado/util.py | 14 ++++ tests/fixtures/tado/smartac3.with_swing.json | 64 +++++++++++++++++++ .../tado/zone_with_swing_capabilities.json | 46 +++++++++++++ tests/fixtures/tado/zones.json | 48 ++++++++++++++ 11 files changed, 253 insertions(+), 11 deletions(-) create mode 100644 tests/fixtures/tado/smartac3.with_swing.json create mode 100644 tests/fixtures/tado/zone_with_swing_capabilities.json diff --git a/homeassistant/components/tado/__init__.py b/homeassistant/components/tado/__init__.py index 46dba04a77e..1dba5f5f29e 100644 --- a/homeassistant/components/tado/__init__.py +++ b/homeassistant/components/tado/__init__.py @@ -186,10 +186,11 @@ class TadoConnector: device_type="HEATING", mode=None, fan_speed=None, + swing=None, ): """Set a zone overlay.""" _LOGGER.debug( - "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s, type=%s, mode=%s fan_speed=%s", + "Set overlay for zone %s: overlay_mode=%s, temp=%s, duration=%s, type=%s, mode=%s fan_speed=%s swing=%s", zone_id, overlay_mode, temperature, @@ -197,6 +198,7 @@ class TadoConnector: device_type, mode, fan_speed, + swing, ) try: @@ -208,7 +210,8 @@ class TadoConnector: device_type, "ON", mode, - fan_speed, + fanSpeed=fan_speed, + swing=swing, ) except RequestException as exc: diff --git a/homeassistant/components/tado/climate.py b/homeassistant/components/tado/climate.py index 224960ea3eb..2c6e49f3273 100644 --- a/homeassistant/components/tado/climate.py +++ b/homeassistant/components/tado/climate.py @@ -11,6 +11,7 @@ from homeassistant.components.climate.const import ( PRESET_HOME, SUPPORT_FAN_MODE, SUPPORT_PRESET_MODE, + SUPPORT_SWING_MODE, SUPPORT_TARGET_TEMPERATURE, ) from homeassistant.const import ATTR_TEMPERATURE, PRECISION_TENTHS, TEMP_CELSIUS @@ -35,6 +36,7 @@ from .const import ( SUPPORT_PRESET, TADO_HVAC_ACTION_TO_HA_HVAC_ACTION, TADO_MODES_WITH_NO_TEMP_SETTING, + TADO_SWING_OFF, TADO_TO_HA_FAN_MODE_MAP, TADO_TO_HA_HVAC_MODE_MAP, TYPE_AIR_CONDITIONING, @@ -85,6 +87,9 @@ def create_climate_entity(tado, name: str, zone_id: int): continue supported_hvac_modes.append(TADO_TO_HA_HVAC_MODE_MAP[mode]) + if capabilities[mode].get("swings"): + support_flags |= SUPPORT_SWING_MODE + if not capabilities[mode].get("fanSpeeds"): continue @@ -197,6 +202,7 @@ class TadoClimate(ClimateDevice): self._current_tado_fan_speed = CONST_FAN_OFF self._current_tado_hvac_mode = CONST_MODE_OFF self._current_tado_hvac_action = CURRENT_HVAC_OFF + self._current_tado_swing_mode = TADO_SWING_OFF self._undo_dispatcher = None self._tado_zone_data = None @@ -378,6 +384,25 @@ class TadoClimate(ClimateDevice): return self._heat_max_temp + @property + def swing_mode(self): + """Active swing mode for the device.""" + return self._current_tado_swing_mode + + @property + def swing_modes(self): + """Swing modes for the device.""" + if self._support_flags & SUPPORT_SWING_MODE: + # Currently we only support off. + # On will be added in the future in an update + # to PyTado + return [TADO_SWING_OFF] + return None + + def set_swing_mode(self, swing_mode): + """Set swing modes for the device.""" + self._control_hvac(swing_mode=swing_mode) + @callback def _async_update_zone_data(self): """Load tado data into zone.""" @@ -408,7 +433,9 @@ class TadoClimate(ClimateDevice): elif self._target_temp < self._heat_min_temp: self._target_temp = self._heat_min_temp - def _control_hvac(self, hvac_mode=None, target_temp=None, fan_mode=None): + def _control_hvac( + self, hvac_mode=None, target_temp=None, fan_mode=None, swing_mode=None + ): """Send new target temperature to Tado.""" if hvac_mode: @@ -420,6 +447,9 @@ class TadoClimate(ClimateDevice): if fan_mode: self._current_tado_fan_speed = fan_mode + if swing_mode: + self._current_tado_swing_mode = swing_mode + self._normalize_target_temp_for_hvac_mode() # tado does not permit setting the fan speed to @@ -464,6 +494,13 @@ class TadoClimate(ClimateDevice): # A temperature cannot be passed with these modes temperature_to_send = None + fan_speed = None + if self._support_flags & SUPPORT_FAN_MODE: + fan_speed = self._current_tado_fan_speed + swing = None + if self._support_flags & SUPPORT_SWING_MODE: + swing = self._current_tado_swing_mode + self._tado.set_zone_overlay( zone_id=self.zone_id, overlay_mode=overlay_mode, # What to do when the period ends @@ -471,9 +508,6 @@ class TadoClimate(ClimateDevice): duration=None, device_type=self.zone_type, mode=self._current_tado_hvac_mode, - fan_speed=( - self._current_tado_fan_speed - if (self._support_flags & SUPPORT_FAN_MODE) - else None - ), # api defaults to not sending fanSpeed if not specified + fan_speed=fan_speed, # api defaults to not sending fanSpeed if None specified + swing=swing, # api defaults to not sending swing if None specified ) diff --git a/homeassistant/components/tado/const.py b/homeassistant/components/tado/const.py index 542437d0af0..ab965de035a 100644 --- a/homeassistant/components/tado/const.py +++ b/homeassistant/components/tado/const.py @@ -131,3 +131,7 @@ TADO_TO_HA_FAN_MODE_MAP = {value: key for key, value in HA_TO_TADO_FAN_MODE_MAP. DEFAULT_TADO_PRECISION = 0.1 SUPPORT_PRESET = [PRESET_AWAY, PRESET_HOME] + + +TADO_SWING_OFF = "OFF" +TADO_SWING_ON = "ON" diff --git a/homeassistant/components/tado/manifest.json b/homeassistant/components/tado/manifest.json index e84072b5985..ce4679a23e2 100644 --- a/homeassistant/components/tado/manifest.json +++ b/homeassistant/components/tado/manifest.json @@ -3,7 +3,7 @@ "name": "Tado", "documentation": "https://www.home-assistant.io/integrations/tado", "requirements": [ - "python-tado==0.5.0" + "python-tado==0.6.0" ], "dependencies": [], "codeowners": [ diff --git a/requirements_all.txt b/requirements_all.txt index 08f54a3924a..04e3b1c99a9 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1668,7 +1668,7 @@ python-songpal==0.11.2 python-synology==0.4.0 # homeassistant.components.tado -python-tado==0.5.0 +python-tado==0.6.0 # homeassistant.components.telegram_bot python-telegram-bot==11.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0df53b2e0b6..2ea01f58880 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -620,7 +620,7 @@ python-miio==0.4.8 python-nest==4.1.0 # homeassistant.components.tado -python-tado==0.5.0 +python-tado==0.6.0 # homeassistant.components.twitch python-twitch-client==0.6.0 diff --git a/tests/components/tado/test_climate.py b/tests/components/tado/test_climate.py index 602f4d8424f..dfb2973f4cb 100644 --- a/tests/components/tado/test_climate.py +++ b/tests/components/tado/test_climate.py @@ -57,3 +57,32 @@ async def test_heater(hass): # Only test for a subset of attributes in case # HA changes the implementation and a new one appears assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +async def test_smartac_with_swing(hass): + """Test creation of smart ac with swing climate.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.air_conditioning_with_swing") + assert state.state == "auto" + + expected_attributes = { + "current_humidity": 42.3, + "current_temperature": 20.9, + "fan_mode": "auto", + "fan_modes": ["auto", "high", "medium", "low"], + "friendly_name": "Air Conditioning with swing", + "hvac_action": "heating", + "hvac_modes": ["off", "auto", "heat", "cool", "heat_cool", "dry", "fan_only"], + "max_temp": 30.0, + "min_temp": 16.0, + "preset_mode": "home", + "preset_modes": ["away", "home"], + "supported_features": 57, + "target_temp_step": 1.0, + "temperature": 20.0, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/tado/util.py b/tests/components/tado/util.py index 7ee4c17058d..1b7e1ad888e 100644 --- a/tests/components/tado/util.py +++ b/tests/components/tado/util.py @@ -19,6 +19,11 @@ async def async_init_integration( devices_fixture = "tado/devices.json" me_fixture = "tado/me.json" zones_fixture = "tado/zones.json" + + # Smart AC with Swing + zone_5_state_fixture = "tado/smartac3.with_swing.json" + zone_5_capabilities_fixture = "tado/zone_with_swing_capabilities.json" + # Water Heater 2 zone_4_state_fixture = "tado/tadov2.water_heater.heating.json" zone_4_capabilities_fixture = "tado/water_heater_zone_capabilities.json" @@ -31,6 +36,7 @@ async def async_init_integration( zone_2_state_fixture = "tado/tadov2.water_heater.auto_mode.json" zone_2_capabilities_fixture = "tado/water_heater_zone_capabilities.json" + # Tado V2 with manual heating zone_1_state_fixture = "tado/tadov2.heating.manual_mode.json" zone_1_capabilities_fixture = "tado/tadov2.zone_capabilities.json" @@ -47,6 +53,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones", text=load_fixture(zones_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/5/capabilities", + text=load_fixture(zone_5_capabilities_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/4/capabilities", text=load_fixture(zone_4_capabilities_fixture), @@ -63,6 +73,10 @@ async def async_init_integration( "https://my.tado.com/api/v2/homes/1/zones/1/capabilities", text=load_fixture(zone_1_capabilities_fixture), ) + m.get( + "https://my.tado.com/api/v2/homes/1/zones/5/state", + text=load_fixture(zone_5_state_fixture), + ) m.get( "https://my.tado.com/api/v2/homes/1/zones/4/state", text=load_fixture(zone_4_state_fixture), diff --git a/tests/fixtures/tado/smartac3.with_swing.json b/tests/fixtures/tado/smartac3.with_swing.json new file mode 100644 index 00000000000..c72cc2ad50b --- /dev/null +++ b/tests/fixtures/tado/smartac3.with_swing.json @@ -0,0 +1,64 @@ +{ + "tadoMode": "HOME", + "geolocationOverride": false, + "geolocationOverrideDisableTime": null, + "preparation": null, + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 20.00, + "fahrenheit": 68.00 + }, + "fanSpeed": "AUTO", + "swing": "ON" + }, + "overlayType": null, + "overlay": null, + "openWindow": null, + "nextScheduleChange": { + "start": "2020-03-28T04:30:00Z", + "setting": { + "type": "AIR_CONDITIONING", + "power": "ON", + "mode": "HEAT", + "temperature": { + "celsius": 23.00, + "fahrenheit": 73.40 + }, + "fanSpeed": "AUTO", + "swing": "ON" + } + }, + "nextTimeBlock": { + "start": "2020-03-28T04:30:00.000Z" + }, + "link": { + "state": "ONLINE" + }, + "activityDataPoints": { + "acPower": { + "timestamp": "2020-03-27T23:02:22.260Z", + "type": "POWER", + "value": "ON" + } + }, + "sensorDataPoints": { + "insideTemperature": { + "celsius": 20.88, + "fahrenheit": 69.58, + "timestamp": "2020-03-28T02:09:27.830Z", + "type": "TEMPERATURE", + "precision": { + "celsius": 0.1, + "fahrenheit": 0.1 + } + }, + "humidity": { + "type": "PERCENTAGE", + "percentage": 42.30, + "timestamp": "2020-03-28T02:09:27.830Z" + } + } +} diff --git a/tests/fixtures/tado/zone_with_swing_capabilities.json b/tests/fixtures/tado/zone_with_swing_capabilities.json new file mode 100644 index 00000000000..fc954890e2a --- /dev/null +++ b/tests/fixtures/tado/zone_with_swing_capabilities.json @@ -0,0 +1,46 @@ +{ + "type": "AIR_CONDITIONING", + "AUTO": { + "fanSpeeds": ["AUTO", "HIGH", "MIDDLE", "LOW"], + "swings": ["OFF", "ON"] + }, + "COOL": { + "temperatures": { + "celsius": { + "min": 18, + "max": 30, + "step": 1.0 + }, + "fahrenheit": { + "min": 64, + "max": 86, + "step": 1.0 + } + }, + "fanSpeeds": ["AUTO", "HIGH", "MIDDLE", "LOW"], + "swings": ["OFF", "ON"] + }, + "DRY": { + "swings": ["OFF", "ON"] + }, + "FAN": { + "fanSpeeds": ["AUTO", "HIGH", "MIDDLE", "LOW"], + "swings": ["OFF", "ON"] + }, + "HEAT": { + "temperatures": { + "celsius": { + "min": 16, + "max": 30, + "step": 1.0 + }, + "fahrenheit": { + "min": 61, + "max": 86, + "step": 1.0 + } + }, + "fanSpeeds": ["AUTO", "HIGH", "MIDDLE", "LOW"], + "swings": ["OFF", "ON"] + } +} diff --git a/tests/fixtures/tado/zones.json b/tests/fixtures/tado/zones.json index 8d7265ade50..d85bc9be3ae 100644 --- a/tests/fixtures/tado/zones.json +++ b/tests/fixtures/tado/zones.json @@ -175,5 +175,53 @@ }, "id" : 4, "supportsDazzle" : true + }, + { + "dazzleMode" : { + "supported" : true, + "enabled" : true + }, + "name" : "Air Conditioning with swing", + "id" : 5, + "supportsDazzle" : true, + "devices" : [ + { + "deviceType" : "WR02", + "shortSerialNo" : "WR4", + "serialNo" : "WR4", + "commandTableUploadState" : "FINISHED", + "duties" : [ + "ZONE_UI", + "ZONE_DRIVER", + "ZONE_LEADER" + ], + "currentFwVersion" : "59.4", + "characteristics" : { + "capabilities" : [ + "INSIDE_TEMPERATURE_MEASUREMENT", + "IDENTIFY" + ] + }, + "accessPointWiFi" : { + "ssid" : "tado8480" + }, + "connectionState" : { + "timestamp" : "2020-03-23T18:30:07.377Z", + "value" : true + } + } + ], + "dazzleEnabled" : true, + "dateCreated" : "2019-11-28T15:58:48.968Z", + "openWindowDetection" : { + "timeoutInSeconds" : 900, + "enabled" : true, + "supported" : true + }, + "deviceTypes" : [ + "WR02" + ], + "reportAvailable" : false, + "type" : "AIR_CONDITIONING" } ] From b892dbc6ea0d76774d36b8b3e313ff54d620acdf Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 31 Mar 2020 17:35:32 -0500 Subject: [PATCH 354/431] Refactor DirecTV Integration to Async (#33114) * switch to directv==0.1.1 * work on directv async. * Update const.py * Update __init__.py * Update media_player.py * Update __init__.py * Update __init__.py * Update __init__.py * Update media_player.py * Update test_config_flow.py * Update media_player.py * Update media_player.py * work on tests and coverage. * Update __init__.py * Update __init__.py * squash. --- homeassistant/components/directv/__init__.py | 72 +-- .../components/directv/config_flow.py | 138 +++--- homeassistant/components/directv/const.py | 14 +- .../components/directv/manifest.json | 3 +- .../components/directv/media_player.py | 290 ++++-------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/directv/__init__.py | 219 +++------- tests/components/directv/test_config_flow.py | 412 ++++++++++-------- tests/components/directv/test_init.py | 41 +- tests/components/directv/test_media_player.py | 275 +++++------- .../fixtures/directv/info-get-locations.json | 22 + tests/fixtures/directv/info-get-version.json | 13 + tests/fixtures/directv/info-mode-error.json | 8 + tests/fixtures/directv/info-mode.json | 9 + .../fixtures/directv/remote-process-key.json | 10 + .../fixtures/directv/tv-get-tuned-movie.json | 24 + tests/fixtures/directv/tv-get-tuned.json | 32 ++ tests/fixtures/directv/tv-tune.json | 8 + 19 files changed, 749 insertions(+), 845 deletions(-) create mode 100644 tests/fixtures/directv/info-get-locations.json create mode 100644 tests/fixtures/directv/info-get-version.json create mode 100644 tests/fixtures/directv/info-mode-error.json create mode 100644 tests/fixtures/directv/info-mode.json create mode 100644 tests/fixtures/directv/remote-process-key.json create mode 100644 tests/fixtures/directv/tv-get-tuned-movie.json create mode 100644 tests/fixtures/directv/tv-get-tuned.json create mode 100644 tests/fixtures/directv/tv-tune.json diff --git a/homeassistant/components/directv/__init__.py b/homeassistant/components/directv/__init__.py index fc7bb78989a..0be5957a29a 100644 --- a/homeassistant/components/directv/__init__.py +++ b/homeassistant/components/directv/__init__.py @@ -1,19 +1,27 @@ """The DirecTV integration.""" import asyncio from datetime import timedelta -from typing import Dict +from typing import Any, Dict -from DirectPy import DIRECTV -from requests.exceptions import RequestException +from directv import DIRECTV, DIRECTVError import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import CONF_HOST +from homeassistant.const import ATTR_NAME, CONF_HOST 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.entity import Entity -from .const import DATA_CLIENT, DATA_LOCATIONS, DATA_VERSION_INFO, DEFAULT_PORT, DOMAIN +from .const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_SOFTWARE_VERSION, + ATTR_VIA_DEVICE, + DOMAIN, +) CONFIG_SCHEMA = vol.Schema( { @@ -28,21 +36,6 @@ PLATFORMS = ["media_player"] SCAN_INTERVAL = timedelta(seconds=30) -def get_dtv_data( - hass: HomeAssistant, host: str, port: int = DEFAULT_PORT, client_addr: str = "0" -) -> dict: - """Retrieve a DIRECTV instance, locations list, and version info for the receiver device.""" - dtv = DIRECTV(host, port, client_addr, determine_state=False) - locations = dtv.get_locations() - version_info = dtv.get_version() - - return { - DATA_CLIENT: dtv, - DATA_LOCATIONS: locations, - DATA_VERSION_INFO: version_info, - } - - async def async_setup(hass: HomeAssistant, config: Dict) -> bool: """Set up the DirecTV component.""" hass.data.setdefault(DOMAIN, {}) @@ -60,14 +53,14 @@ async def async_setup(hass: HomeAssistant, config: Dict) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up DirecTV from a config entry.""" + dtv = DIRECTV(entry.data[CONF_HOST], session=async_get_clientsession(hass)) + try: - dtv_data = await hass.async_add_executor_job( - get_dtv_data, hass, entry.data[CONF_HOST] - ) - except RequestException: + await dtv.update() + except DIRECTVError: raise ConfigEntryNotReady - hass.data[DOMAIN][entry.entry_id] = dtv_data + hass.data[DOMAIN][entry.entry_id] = dtv for component in PLATFORMS: hass.async_create_task( @@ -92,3 +85,32 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN].pop(entry.entry_id) return unload_ok + + +class DIRECTVEntity(Entity): + """Defines a base DirecTV entity.""" + + def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: + """Initialize the DirecTV entity.""" + self._address = address + self._device_id = address if address != "0" else dtv.device.info.receiver_id + self._is_client = address != "0" + self._name = name + self.dtv = dtv + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def device_info(self) -> Dict[str, Any]: + """Return device information about this DirecTV receiver.""" + return { + ATTR_IDENTIFIERS: {(DOMAIN, self._device_id)}, + ATTR_NAME: self.name, + ATTR_MANUFACTURER: self.dtv.device.info.brand, + ATTR_MODEL: None, + ATTR_SOFTWARE_VERSION: self.dtv.device.info.version, + ATTR_VIA_DEVICE: (DOMAIN, self.dtv.device.info.receiver_id), + } diff --git a/homeassistant/components/directv/config_flow.py b/homeassistant/components/directv/config_flow.py index b7d1604622e..406f2628ee4 100644 --- a/homeassistant/components/directv/config_flow.py +++ b/homeassistant/components/directv/config_flow.py @@ -3,18 +3,20 @@ import logging from typing import Any, Dict, Optional from urllib.parse import urlparse -from DirectPy import DIRECTV -from requests.exceptions import RequestException +from directv import DIRECTV, DIRECTVError import voluptuous as vol from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow from homeassistant.const import CONF_HOST, CONF_NAME -from homeassistant.core import callback -from homeassistant.exceptions import HomeAssistantError -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.typing import ( + ConfigType, + DiscoveryInfoType, + HomeAssistantType, +) -from .const import DEFAULT_PORT +from .const import CONF_RECEIVER_ID from .const import DOMAIN # pylint: disable=unused-import _LOGGER = logging.getLogger(__name__) @@ -22,22 +24,17 @@ _LOGGER = logging.getLogger(__name__) ERROR_CANNOT_CONNECT = "cannot_connect" ERROR_UNKNOWN = "unknown" -DATA_SCHEMA = vol.Schema({vol.Required(CONF_HOST): str}) - -def validate_input(data: Dict) -> Dict: +async def validate_input(hass: HomeAssistantType, data: dict) -> Dict[str, Any]: """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - dtv = DIRECTV(data["host"], DEFAULT_PORT, determine_state=False) - version_info = dtv.get_version() + session = async_get_clientsession(hass) + directv = DIRECTV(data[CONF_HOST], session=session) + device = await directv.update() - return { - "title": data["host"], - "host": data["host"], - "receiver_id": "".join(version_info["receiverId"].split()), - } + return {CONF_RECEIVER_ID: device.info.receiver_id} class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): @@ -46,84 +43,91 @@ class DirecTVConfigFlow(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - @callback - def _show_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: - """Show the form to the user.""" - return self.async_show_form( - step_id="user", data_schema=DATA_SCHEMA, errors=errors or {}, - ) + def __init__(self): + """Set up the instance.""" + self.discovery_info = {} async def async_step_import( - self, user_input: Optional[Dict] = None + self, user_input: Optional[ConfigType] = None ) -> Dict[str, Any]: - """Handle a flow initialized by yaml file.""" + """Handle a flow initiated by configuration file.""" return await self.async_step_user(user_input) async def async_step_user( - self, user_input: Optional[Dict] = None + self, user_input: Optional[ConfigType] = None ) -> Dict[str, Any]: - """Handle a flow initialized by user.""" - if not user_input: - return self._show_form() - - errors = {} + """Handle a flow initiated by the user.""" + if user_input is None: + return self._show_setup_form() try: - info = await self.hass.async_add_executor_job(validate_input, user_input) - user_input[CONF_HOST] = info[CONF_HOST] - except RequestException: - errors["base"] = ERROR_CANNOT_CONNECT - return self._show_form(errors) + info = await validate_input(self.hass, user_input) + except DIRECTVError: + return self._show_setup_form({"base": ERROR_CANNOT_CONNECT}) except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") return self.async_abort(reason=ERROR_UNKNOWN) - await self.async_set_unique_id(info["receiver_id"]) - self._abort_if_unique_id_configured() + user_input[CONF_RECEIVER_ID] = info[CONF_RECEIVER_ID] - return self.async_create_entry(title=info["title"], data=user_input) + await self.async_set_unique_id(user_input[CONF_RECEIVER_ID]) + self._abort_if_unique_id_configured(updates={CONF_HOST: user_input[CONF_HOST]}) + + return self.async_create_entry(title=user_input[CONF_HOST], data=user_input) async def async_step_ssdp( - self, discovery_info: Optional[DiscoveryInfoType] = None + self, discovery_info: DiscoveryInfoType ) -> Dict[str, Any]: - """Handle a flow initialized by discovery.""" + """Handle SSDP discovery.""" host = urlparse(discovery_info[ATTR_SSDP_LOCATION]).hostname - receiver_id = discovery_info[ATTR_UPNP_SERIAL][4:] # strips off RID- + receiver_id = None - await self.async_set_unique_id(receiver_id) - self._abort_if_unique_id_configured(updates={CONF_HOST: host}) + if discovery_info.get(ATTR_UPNP_SERIAL): + receiver_id = discovery_info[ATTR_UPNP_SERIAL][4:] # strips off RID- # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - self.context.update( - {CONF_HOST: host, CONF_NAME: host, "title_placeholders": {"name": host}} + self.context.update({"title_placeholders": {"name": host}}) + + self.discovery_info.update( + {CONF_HOST: host, CONF_NAME: host, CONF_RECEIVER_ID: receiver_id} + ) + + try: + info = await validate_input(self.hass, self.discovery_info) + except DIRECTVError: + return self.async_abort(reason=ERROR_CANNOT_CONNECT) + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + return self.async_abort(reason=ERROR_UNKNOWN) + + self.discovery_info[CONF_RECEIVER_ID] = info[CONF_RECEIVER_ID] + + await self.async_set_unique_id(self.discovery_info[CONF_RECEIVER_ID]) + self._abort_if_unique_id_configured( + updates={CONF_HOST: self.discovery_info[CONF_HOST]} ) return await self.async_step_ssdp_confirm() async def async_step_ssdp_confirm( - self, user_input: Optional[Dict] = None + self, user_input: ConfigType = None ) -> Dict[str, Any]: - """Handle user-confirmation of discovered device.""" - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - name = self.context.get(CONF_NAME) + """Handle a confirmation flow initiated by SSDP.""" + if user_input is None: + return self.async_show_form( + step_id="ssdp_confirm", + description_placeholders={"name": self.discovery_info[CONF_NAME]}, + errors={}, + ) - if user_input is not None: - # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 - user_input[CONF_HOST] = self.context.get(CONF_HOST) - - try: - await self.hass.async_add_executor_job(validate_input, user_input) - return self.async_create_entry(title=name, data=user_input) - except (OSError, RequestException): - return self.async_abort(reason=ERROR_CANNOT_CONNECT) - except Exception: # pylint: disable=broad-except - _LOGGER.exception("Unexpected exception") - return self.async_abort(reason=ERROR_UNKNOWN) - - return self.async_show_form( - step_id="ssdp_confirm", description_placeholders={"name": name}, + return self.async_create_entry( + title=self.discovery_info[CONF_NAME], data=self.discovery_info, ) - -class CannotConnect(HomeAssistantError): - """Error to indicate we cannot connect.""" + def _show_setup_form(self, errors: Optional[Dict] = None) -> Dict[str, Any]: + """Show the setup form to the user.""" + return self.async_show_form( + step_id="user", + data_schema=vol.Schema({vol.Required(CONF_HOST): str}), + errors=errors or {}, + ) diff --git a/homeassistant/components/directv/const.py b/homeassistant/components/directv/const.py index e5b04ce34f6..9ad01a0179b 100644 --- a/homeassistant/components/directv/const.py +++ b/homeassistant/components/directv/const.py @@ -2,19 +2,19 @@ DOMAIN = "directv" +# Attributes +ATTR_IDENTIFIERS = "identifiers" +ATTR_MANUFACTURER = "manufacturer" ATTR_MEDIA_CURRENTLY_RECORDING = "media_currently_recording" ATTR_MEDIA_RATING = "media_rating" ATTR_MEDIA_RECORDED = "media_recorded" ATTR_MEDIA_START_TIME = "media_start_time" +ATTR_MODEL = "model" +ATTR_SOFTWARE_VERSION = "sw_version" +ATTR_VIA_DEVICE = "via_device" -DATA_CLIENT = "client" -DATA_LOCATIONS = "locations" -DATA_VERSION_INFO = "version_info" +CONF_RECEIVER_ID = "receiver_id" DEFAULT_DEVICE = "0" -DEFAULT_MANUFACTURER = "DirecTV" DEFAULT_NAME = "DirecTV Receiver" DEFAULT_PORT = 8080 - -MODEL_HOST = "DirecTV Host" -MODEL_CLIENT = "DirecTV Client" diff --git a/homeassistant/components/directv/manifest.json b/homeassistant/components/directv/manifest.json index cb8ed68b304..4a712ba053e 100644 --- a/homeassistant/components/directv/manifest.json +++ b/homeassistant/components/directv/manifest.json @@ -2,9 +2,10 @@ "domain": "directv", "name": "DirecTV", "documentation": "https://www.home-assistant.io/integrations/directv", - "requirements": ["directpy==0.7"], + "requirements": ["directv==0.2.0"], "dependencies": [], "codeowners": ["@ctalkington"], + "quality_scale": "gold", "config_flow": true, "ssdp": [ { diff --git a/homeassistant/components/directv/media_player.py b/homeassistant/components/directv/media_player.py index f487e72f694..b93577a03d6 100644 --- a/homeassistant/components/directv/media_player.py +++ b/homeassistant/components/directv/media_player.py @@ -1,12 +1,10 @@ """Support for the DirecTV receivers.""" import logging -from typing import Callable, Dict, List, Optional +from typing import Callable, List -from DirectPy import DIRECTV -from requests.exceptions import RequestException -import voluptuous as vol +from directv import DIRECTV -from homeassistant.components.media_player import PLATFORM_SCHEMA, MediaPlayerDevice +from homeassistant.components.media_player import MediaPlayerDevice from homeassistant.components.media_player.const import ( MEDIA_TYPE_CHANNEL, MEDIA_TYPE_MOVIE, @@ -21,34 +19,17 @@ from homeassistant.components.media_player.const import ( SUPPORT_TURN_ON, ) from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ( - CONF_DEVICE, - CONF_HOST, - CONF_NAME, - CONF_PORT, - STATE_OFF, - STATE_PAUSED, - STATE_PLAYING, -) -from homeassistant.helpers import config_validation as cv +from homeassistant.const import STATE_OFF, STATE_PAUSED, STATE_PLAYING from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util +from . import DIRECTVEntity from .const import ( ATTR_MEDIA_CURRENTLY_RECORDING, ATTR_MEDIA_RATING, ATTR_MEDIA_RECORDED, ATTR_MEDIA_START_TIME, - DATA_CLIENT, - DATA_LOCATIONS, - DATA_VERSION_INFO, - DEFAULT_DEVICE, - DEFAULT_MANUFACTURER, - DEFAULT_NAME, - DEFAULT_PORT, DOMAIN, - MODEL_CLIENT, - MODEL_HOST, ) _LOGGER = logging.getLogger(__name__) @@ -73,15 +54,6 @@ SUPPORT_DTV_CLIENT = ( | SUPPORT_PLAY ) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( - { - vol.Required(CONF_HOST): cv.string, - vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, - vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, - vol.Optional(CONF_DEVICE, default=DEFAULT_DEVICE): cv.string, - } -) - async def async_setup_entry( hass: HomeAssistantType, @@ -89,139 +61,57 @@ async def async_setup_entry( async_add_entities: Callable[[List, bool], None], ) -> bool: """Set up the DirecTV config entry.""" - locations = hass.data[DOMAIN][entry.entry_id][DATA_LOCATIONS] - version_info = hass.data[DOMAIN][entry.entry_id][DATA_VERSION_INFO] + dtv = hass.data[DOMAIN][entry.entry_id] entities = [] - for loc in locations["locations"]: - if "locationName" not in loc or "clientAddr" not in loc: - continue - - if loc["clientAddr"] != "0": - dtv = DIRECTV( - entry.data[CONF_HOST], - DEFAULT_PORT, - loc["clientAddr"], - determine_state=False, - ) - else: - dtv = hass.data[DOMAIN][entry.entry_id][DATA_CLIENT] - + for location in dtv.device.locations: entities.append( - DirecTvDevice( - str.title(loc["locationName"]), loc["clientAddr"], dtv, version_info, + DIRECTVMediaPlayer( + dtv=dtv, name=str.title(location.name), address=location.address, ) ) async_add_entities(entities, True) -class DirecTvDevice(MediaPlayerDevice): +class DIRECTVMediaPlayer(DIRECTVEntity, MediaPlayerDevice): """Representation of a DirecTV receiver on the network.""" - def __init__( - self, - name: str, - device: str, - dtv: DIRECTV, - version_info: Optional[Dict] = None, - enabled_default: bool = True, - ): - """Initialize the device.""" - self.dtv = dtv - self._name = name - self._unique_id = None - self._is_standby = True - self._current = None - self._last_update = None - self._paused = None - self._last_position = None - self._is_recorded = None - self._is_client = device != "0" + def __init__(self, *, dtv: DIRECTV, name: str, address: str = "0") -> None: + """Initialize DirecTV media player.""" + super().__init__( + dtv=dtv, name=name, address=address, + ) + self._assumed_state = None self._available = False - self._enabled_default = enabled_default - self._first_error_timestamp = None - self._model = None - self._receiver_id = None - self._software_version = None + self._is_recorded = None + self._is_standby = True + self._last_position = None + self._last_update = None + self._paused = None + self._program = None + self._state = None - if self._is_client: - self._model = MODEL_CLIENT - self._unique_id = device - - if version_info: - self._receiver_id = "".join(version_info["receiverId"].split()) - - if not self._is_client: - self._unique_id = self._receiver_id - self._model = MODEL_HOST - self._software_version = version_info["stbSoftwareVersion"] - - def update(self): + async def async_update(self): """Retrieve latest state.""" - _LOGGER.debug("%s: Updating status", self.entity_id) - try: - self._available = True - self._is_standby = self.dtv.get_standby() - if self._is_standby: - self._current = None - self._is_recorded = None - self._paused = None - self._assumed_state = False - self._last_position = None - self._last_update = None - else: - self._current = self.dtv.get_tuned() - if self._current["status"]["code"] == 200: - self._first_error_timestamp = None - self._is_recorded = self._current.get("uniqueId") is not None - self._paused = self._last_position == self._current["offset"] - self._assumed_state = self._is_recorded - self._last_position = self._current["offset"] - self._last_update = ( - dt_util.utcnow() - if not self._paused or self._last_update is None - else self._last_update - ) - else: - # If an error is received then only set to unavailable if - # this started at least 1 minute ago. - log_message = f"{self.entity_id}: Invalid status {self._current['status']['code']} received" - if self._check_state_available(): - _LOGGER.debug(log_message) - else: - _LOGGER.error(log_message) + self._state = await self.dtv.state(self._address) + self._available = self._state.available + self._is_standby = self._state.standby + self._program = self._state.program - except RequestException as exception: - _LOGGER.error( - "%s: Request error trying to update current status: %s", - self.entity_id, - exception, - ) - self._check_state_available() - - except Exception as exception: - _LOGGER.error( - "%s: Exception trying to update current status: %s", - self.entity_id, - exception, - ) - self._available = False - if not self._first_error_timestamp: - self._first_error_timestamp = dt_util.utcnow() - raise - - def _check_state_available(self): - """Set to unavailable if issue been occurring over 1 minute.""" - if not self._first_error_timestamp: - self._first_error_timestamp = dt_util.utcnow() - else: - tdelta = dt_util.utcnow() - self._first_error_timestamp - if tdelta.total_seconds() >= 60: - self._available = False - - return self._available + if self._is_standby: + self._assumed_state = False + self._is_recorded = None + self._last_position = None + self._last_update = None + self._paused = None + elif self._program is not None: + self._paused = self._last_position == self._program.position + self._is_recorded = self._program.recorded + self._last_position = self._program.position + self._last_update = self._state.at + self._assumed_state = self._is_recorded @property def device_state_attributes(self): @@ -243,24 +133,10 @@ class DirecTvDevice(MediaPlayerDevice): @property def unique_id(self): """Return a unique ID to use for this media player.""" - return self._unique_id + if self._address == "0": + return self.dtv.device.info.receiver_id - @property - def device_info(self): - """Return device specific attributes.""" - return { - "name": self.name, - "identifiers": {(DOMAIN, self.unique_id)}, - "manufacturer": DEFAULT_MANUFACTURER, - "model": self._model, - "sw_version": self._software_version, - "via_device": (DOMAIN, self._receiver_id), - } - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default + return self._address # MediaPlayerDevice properties and methods @property @@ -290,29 +166,30 @@ class DirecTvDevice(MediaPlayerDevice): @property def media_content_id(self): """Return the content ID of current playing media.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current["programId"] + return self._program.program_id @property def media_content_type(self): """Return the content type of current playing media.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - if "episodeTitle" in self._current: - return MEDIA_TYPE_TVSHOW + known_types = [MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW] + if self._program.program_type in known_types: + return self._program.program_type return MEDIA_TYPE_MOVIE @property def media_duration(self): """Return the duration of current playing media in seconds.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current["duration"] + return self._program.duration @property def media_position(self): @@ -324,10 +201,7 @@ class DirecTvDevice(MediaPlayerDevice): @property def media_position_updated_at(self): - """When was the position of the current playing media valid. - - Returns value from homeassistant.util.dt.utcnow(). - """ + """When was the position of the current playing media valid.""" if self._is_standby: return None @@ -336,34 +210,34 @@ class DirecTvDevice(MediaPlayerDevice): @property def media_title(self): """Return the title of current playing media.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current["title"] + return self._program.title @property def media_series_title(self): """Return the title of current episode of TV show.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current.get("episodeTitle") + return self._program.episode_title @property def media_channel(self): """Return the channel current playing media.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return f"{self._current['callsign']} ({self._current['major']})" + return f"{self._program.channel_name} ({self._program.channel})" @property def source(self): """Name of the current input source.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current["major"] + return self._program.channel @property def supported_features(self): @@ -373,18 +247,18 @@ class DirecTvDevice(MediaPlayerDevice): @property def media_currently_recording(self): """If the media is currently being recorded or not.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current["isRecording"] + return self._program.recording @property def media_rating(self): """TV Rating of the current playing media.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return self._current["rating"] + return self._program.rating @property def media_recorded(self): @@ -397,53 +271,53 @@ class DirecTvDevice(MediaPlayerDevice): @property def media_start_time(self): """Start time the program aired.""" - if self._is_standby: + if self._is_standby or self._program is None: return None - return dt_util.as_local(dt_util.utc_from_timestamp(self._current["startTime"])) + return dt_util.as_local(self._program.start_time) - def turn_on(self): + async def async_turn_on(self): """Turn on the receiver.""" if self._is_client: raise NotImplementedError() _LOGGER.debug("Turn on %s", self._name) - self.dtv.key_press("poweron") + await self.dtv.remote("poweron", self._address) - def turn_off(self): + async def async_turn_off(self): """Turn off the receiver.""" if self._is_client: raise NotImplementedError() _LOGGER.debug("Turn off %s", self._name) - self.dtv.key_press("poweroff") + await self.dtv.remote("poweroff", self._address) - def media_play(self): + async def async_media_play(self): """Send play command.""" _LOGGER.debug("Play on %s", self._name) - self.dtv.key_press("play") + await self.dtv.remote("play", self._address) - def media_pause(self): + async def async_media_pause(self): """Send pause command.""" _LOGGER.debug("Pause on %s", self._name) - self.dtv.key_press("pause") + await self.dtv.remote("pause", self._address) - def media_stop(self): + async def async_media_stop(self): """Send stop command.""" _LOGGER.debug("Stop on %s", self._name) - self.dtv.key_press("stop") + await self.dtv.remote("stop", self._address) - def media_previous_track(self): + async def async_media_previous_track(self): """Send rewind command.""" _LOGGER.debug("Rewind on %s", self._name) - self.dtv.key_press("rew") + await self.dtv.remote("rew", self._address) - def media_next_track(self): + async def async_media_next_track(self): """Send fast forward command.""" _LOGGER.debug("Fast forward on %s", self._name) - self.dtv.key_press("ffwd") + await self.dtv.remote("ffwd", self._address) - def play_media(self, media_type, media_id, **kwargs): + async def async_play_media(self, media_type, media_id, **kwargs): """Select input source.""" if media_type != MEDIA_TYPE_CHANNEL: _LOGGER.error( @@ -454,4 +328,4 @@ class DirecTvDevice(MediaPlayerDevice): return _LOGGER.debug("Changing channel on %s to %s", self._name, media_id) - self.dtv.tune_channel(media_id) + await self.dtv.tune(media_id, self._address) diff --git a/requirements_all.txt b/requirements_all.txt index 04e3b1c99a9..9a420d98d2d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -447,7 +447,7 @@ deluge-client==1.7.1 denonavr==0.8.1 # homeassistant.components.directv -directpy==0.7 +directv==0.2.0 # homeassistant.components.discogs discogs_client==2.2.2 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 2ea01f58880..84b177eb809 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -178,7 +178,7 @@ defusedxml==0.6.0 denonavr==0.8.1 # homeassistant.components.directv -directpy==0.7 +directv==0.2.0 # homeassistant.components.updater distro==1.4.0 diff --git a/tests/components/directv/__init__.py b/tests/components/directv/__init__.py index 876b1e311ab..cd0f72307d8 100644 --- a/tests/components/directv/__init__.py +++ b/tests/components/directv/__init__.py @@ -1,183 +1,94 @@ """Tests for the DirecTV component.""" -from DirectPy import DIRECTV - -from homeassistant.components.directv.const import DOMAIN +from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN +from homeassistant.components.ssdp import ATTR_SSDP_LOCATION from homeassistant.const import CONF_HOST from homeassistant.helpers.typing import HomeAssistantType -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, load_fixture +from tests.test_util.aiohttp import AiohttpClientMocker -CLIENT_NAME = "Bedroom Client" -CLIENT_ADDRESS = "2CA17D1CD30X" -DEFAULT_DEVICE = "0" HOST = "127.0.0.1" -MAIN_NAME = "Main DVR" RECEIVER_ID = "028877455858" SSDP_LOCATION = "http://127.0.0.1/" UPNP_SERIAL = "RID-028877455858" -LIVE = { - "callsign": "HASSTV", - "date": "20181110", - "duration": 3600, - "isOffAir": False, - "isPclocked": 1, - "isPpv": False, - "isRecording": False, - "isVod": False, - "major": 202, - "minor": 65535, - "offset": 1, - "programId": "102454523", - "rating": "No Rating", - "startTime": 1541876400, - "stationId": 3900947, - "title": "Using Home Assistant to automate your home", -} - -RECORDING = { - "callsign": "HASSTV", - "date": "20181110", - "duration": 3600, - "isOffAir": False, - "isPclocked": 1, - "isPpv": False, - "isRecording": True, - "isVod": False, - "major": 202, - "minor": 65535, - "offset": 1, - "programId": "102454523", - "rating": "No Rating", - "startTime": 1541876400, - "stationId": 3900947, - "title": "Using Home Assistant to automate your home", - "uniqueId": "12345", - "episodeTitle": "Configure DirecTV platform.", -} - MOCK_CONFIG = {DOMAIN: [{CONF_HOST: HOST}]} - -MOCK_GET_LOCATIONS = { - "locations": [{"locationName": MAIN_NAME, "clientAddr": DEFAULT_DEVICE}], - "status": { - "code": 200, - "commandResult": 0, - "msg": "OK.", - "query": "/info/getLocations", - }, -} - -MOCK_GET_LOCATIONS_MULTIPLE = { - "locations": [ - {"locationName": MAIN_NAME, "clientAddr": DEFAULT_DEVICE}, - {"locationName": CLIENT_NAME, "clientAddr": CLIENT_ADDRESS}, - ], - "status": { - "code": 200, - "commandResult": 0, - "msg": "OK.", - "query": "/info/getLocations", - }, -} - -MOCK_GET_VERSION = { - "accessCardId": "0021-1495-6572", - "receiverId": "0288 7745 5858", - "status": { - "code": 200, - "commandResult": 0, - "msg": "OK.", - "query": "/info/getVersion", - }, - "stbSoftwareVersion": "0x4ed7", - "systemTime": 1281625203, - "version": "1.2", -} +MOCK_SSDP_DISCOVERY_INFO = {ATTR_SSDP_LOCATION: SSDP_LOCATION} +MOCK_USER_INPUT = {CONF_HOST: HOST} -class MockDirectvClass(DIRECTV): - """A fake DirecTV DVR device.""" +def mock_connection(aioclient_mock: AiohttpClientMocker) -> None: + """Mock the DirecTV connection for Home Assistant.""" + aioclient_mock.get( + f"http://{HOST}:8080/info/getVersion", + text=load_fixture("directv/info-get-version.json"), + headers={"Content-Type": "application/json"}, + ) - def __init__(self, ip, port=8080, clientAddr="0", determine_state=False): - """Initialize the fake DirecTV device.""" - super().__init__( - ip=ip, port=port, clientAddr=clientAddr, determine_state=determine_state, - ) + aioclient_mock.get( + f"http://{HOST}:8080/info/getLocations", + text=load_fixture("directv/info-get-locations.json"), + headers={"Content-Type": "application/json"}, + ) - self._play = False - self._standby = True + aioclient_mock.get( + f"http://{HOST}:8080/info/mode", + params={"clientAddr": "9XXXXXXXXXX9"}, + status=500, + text=load_fixture("directv/info-mode-error.json"), + headers={"Content-Type": "application/json"}, + ) - if self.clientAddr == CLIENT_ADDRESS: - self.attributes = RECORDING - self._standby = False - else: - self.attributes = LIVE + aioclient_mock.get( + f"http://{HOST}:8080/info/mode", + text=load_fixture("directv/info-mode.json"), + headers={"Content-Type": "application/json"}, + ) - def get_locations(self): - """Mock for get_locations method.""" - return MOCK_GET_LOCATIONS + aioclient_mock.get( + f"http://{HOST}:8080/remote/processKey", + text=load_fixture("directv/remote-process-key.json"), + headers={"Content-Type": "application/json"}, + ) - def get_serial_num(self): - """Mock for get_serial_num method.""" - test_serial_num = { - "serialNum": "9999999999", - "status": { - "code": 200, - "commandResult": 0, - "msg": "OK.", - "query": "/info/getSerialNum", - }, - } + aioclient_mock.get( + f"http://{HOST}:8080/tv/tune", + text=load_fixture("directv/tv-tune.json"), + headers={"Content-Type": "application/json"}, + ) - return test_serial_num + aioclient_mock.get( + f"http://{HOST}:8080/tv/getTuned", + params={"clientAddr": "2CA17D1CD30X"}, + text=load_fixture("directv/tv-get-tuned.json"), + headers={"Content-Type": "application/json"}, + ) - def get_standby(self): - """Mock for get_standby method.""" - return self._standby - - def get_tuned(self): - """Mock for get_tuned method.""" - if self._play: - self.attributes["offset"] = self.attributes["offset"] + 1 - - test_attributes = self.attributes - test_attributes["status"] = { - "code": 200, - "commandResult": 0, - "msg": "OK.", - "query": "/tv/getTuned", - } - return test_attributes - - def get_version(self): - """Mock for get_version method.""" - return MOCK_GET_VERSION - - def key_press(self, keypress): - """Mock for key_press method.""" - if keypress == "poweron": - self._standby = False - self._play = True - elif keypress == "poweroff": - self._standby = True - self._play = False - elif keypress == "play": - self._play = True - elif keypress == "pause" or keypress == "stop": - self._play = False - - def tune_channel(self, source): - """Mock for tune_channel method.""" - self.attributes["major"] = int(source) + aioclient_mock.get( + f"http://{HOST}:8080/tv/getTuned", + text=load_fixture("directv/tv-get-tuned-movie.json"), + headers={"Content-Type": "application/json"}, + ) async def setup_integration( - hass: HomeAssistantType, skip_entry_setup: bool = False + hass: HomeAssistantType, + aioclient_mock: AiohttpClientMocker, + skip_entry_setup: bool = False, + setup_error: bool = False, ) -> MockConfigEntry: """Set up the DirecTV integration in Home Assistant.""" + if setup_error: + aioclient_mock.get( + f"http://{HOST}:8080/info/getVersion", status=500, + ) + else: + mock_connection(aioclient_mock) + entry = MockConfigEntry( - domain=DOMAIN, unique_id=RECEIVER_ID, data={CONF_HOST: HOST} + domain=DOMAIN, + unique_id=RECEIVER_ID, + data={CONF_HOST: HOST, CONF_RECEIVER_ID: RECEIVER_ID}, ) entry.add_to_hass(hass) diff --git a/tests/components/directv/test_config_flow.py b/tests/components/directv/test_config_flow.py index bd5d8b83419..c5cfec50637 100644 --- a/tests/components/directv/test_config_flow.py +++ b/tests/components/directv/test_config_flow.py @@ -1,11 +1,9 @@ """Test the DirecTV config flow.""" -from typing import Any, Dict, Optional - +from aiohttp import ClientError as HTTPClientError from asynctest import patch -from requests.exceptions import RequestException -from homeassistant.components.directv.const import DOMAIN -from homeassistant.components.ssdp import ATTR_SSDP_LOCATION, ATTR_UPNP_SERIAL +from homeassistant.components.directv.const import CONF_RECEIVER_ID, DOMAIN +from homeassistant.components.ssdp import ATTR_UPNP_SERIAL from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP, SOURCE_USER from homeassistant.const import CONF_HOST, CONF_NAME, CONF_SOURCE from homeassistant.data_entry_flow import ( @@ -14,219 +12,259 @@ from homeassistant.data_entry_flow import ( RESULT_TYPE_FORM, ) from homeassistant.helpers.typing import HomeAssistantType -from homeassistant.setup import async_setup_component -from tests.common import MockConfigEntry from tests.components.directv import ( HOST, + MOCK_SSDP_DISCOVERY_INFO, + MOCK_USER_INPUT, RECEIVER_ID, - SSDP_LOCATION, UPNP_SERIAL, - MockDirectvClass, + mock_connection, + setup_integration, ) +from tests.test_util.aiohttp import AiohttpClientMocker -async def async_configure_flow( - hass: HomeAssistantType, flow_id: str, user_input: Optional[Dict] = None -) -> Any: - """Set up mock DirecTV integration flow.""" - with patch( - "homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, - ): - return await hass.config_entries.flow.async_configure( - flow_id=flow_id, user_input=user_input - ) - - -async def async_init_flow( - hass: HomeAssistantType, - handler: str = DOMAIN, - context: Optional[Dict] = None, - data: Any = None, -) -> Any: - """Set up mock DirecTV integration flow.""" - with patch( - "homeassistant.components.directv.config_flow.DIRECTV", new=MockDirectvClass, - ): - return await hass.config_entries.flow.async_init( - handler=handler, context=context, data=data - ) - - -async def test_duplicate_error(hass: HomeAssistantType) -> None: - """Test that errors are shown when duplicates are added.""" - MockConfigEntry( - domain=DOMAIN, unique_id=RECEIVER_ID, data={CONF_HOST: HOST} - ).add_to_hass(hass) - - result = await async_init_flow( - hass, context={CONF_SOURCE: SOURCE_IMPORT}, data={CONF_HOST: HOST} - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - result = await async_init_flow( - hass, context={CONF_SOURCE: SOURCE_USER}, data={CONF_HOST: HOST} - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - result = await async_init_flow( - hass, - context={CONF_SOURCE: SOURCE_SSDP}, - data={ATTR_SSDP_LOCATION: SSDP_LOCATION, ATTR_UPNP_SERIAL: UPNP_SERIAL}, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_form(hass: HomeAssistantType) -> None: - """Test we get the form.""" - await async_setup_component(hass, "persistent_notification", {}) +async def test_show_user_form(hass: HomeAssistantType) -> None: + """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER} + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) + + assert result["step_id"] == "user" assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.directv.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.directv.async_setup_entry", return_value=True, - ) as mock_setup_entry: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST}) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"] == {CONF_HOST: HOST} - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_cannot_connect(hass: HomeAssistantType) -> None: - """Test we handle cannot connect error.""" +async def test_show_ssdp_form( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test that the ssdp confirmation form is served.""" + mock_connection(aioclient_mock) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER} - ) - - with patch( - "tests.components.directv.test_config_flow.MockDirectvClass.get_version", - side_effect=RequestException, - ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) - - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "cannot_connect"} - - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 - - -async def test_form_unknown_error(hass: HomeAssistantType) -> None: - """Test we handle unknown error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER} - ) - - with patch( - "tests.components.directv.test_config_flow.MockDirectvClass.get_version", - side_effect=Exception, - ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {CONF_HOST: HOST},) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "unknown" - - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 - - -async def test_import(hass: HomeAssistantType) -> None: - """Test the import step.""" - with patch( - "homeassistant.components.directv.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.directv.async_setup_entry", return_value=True, - ) as mock_setup_entry: - result = await async_init_flow( - hass, context={CONF_SOURCE: SOURCE_IMPORT}, data={CONF_HOST: HOST}, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"] == {CONF_HOST: HOST} - - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_ssdp_discovery(hass: HomeAssistantType) -> None: - """Test the ssdp discovery step.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_SSDP}, - data={ATTR_SSDP_LOCATION: SSDP_LOCATION, ATTR_UPNP_SERIAL: UPNP_SERIAL}, + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "ssdp_confirm" assert result["description_placeholders"] == {CONF_NAME: HOST} + +async def test_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on connection error.""" + aioclient_mock.get("http://127.0.0.1:8080/info/getVersion", exc=HTTPClientError) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_ssdp_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """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() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_ssdp_confirm_cannot_connect( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """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() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP, CONF_HOST: HOST, CONF_NAME: HOST}, + data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + +async def test_user_device_exists_abort( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort user flow if DirecTV receiver already configured.""" + await setup_integration(hass, aioclient_mock) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_device_exists_abort( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort SSDP flow if DirecTV receiver already configured.""" + await setup_integration(hass, aioclient_mock) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_with_receiver_id_device_exists_abort( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort SSDP flow if DirecTV receiver already configured.""" + await setup_integration(hass, aioclient_mock) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + discovery_info[ATTR_UPNP_SERIAL] = UPNP_SERIAL + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on unknown error.""" + user_input = MOCK_USER_INPUT.copy() with patch( - "homeassistant.components.directv.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.directv.async_setup_entry", return_value=True, - ) as mock_setup_entry: - result = await async_configure_flow(hass, result["flow_id"], {}) + "homeassistant.components.directv.config_flow.DIRECTV.update", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_ssdp_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort SSDP flow on unknown error.""" + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + with patch( + "homeassistant.components.directv.config_flow.DIRECTV.update", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_ssdp_confirm_unknown_error( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort SSDP flow on unknown error.""" + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + with patch( + "homeassistant.components.directv.config_flow.DIRECTV.update", + side_effect=Exception, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={CONF_SOURCE: SOURCE_SSDP, CONF_HOST: HOST, CONF_NAME: HOST}, + data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" + + +async def test_full_import_flow_implementation( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + mock_connection(aioclient_mock) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_IMPORT}, data=user_input, + ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == HOST - assert result["data"] == {CONF_HOST: HOST} - await hass.async_block_till_done() - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_RECEIVER_ID] == RECEIVER_ID -async def test_ssdp_discovery_confirm_abort(hass: HomeAssistantType) -> None: - """Test we handle SSDP confirm cannot connect error.""" +async def test_full_user_flow_implementation( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full manual user flow from start to finish.""" + mock_connection(aioclient_mock) + result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_SSDP}, - data={ATTR_SSDP_LOCATION: SSDP_LOCATION, ATTR_UPNP_SERIAL: UPNP_SERIAL}, + DOMAIN, context={CONF_SOURCE: SOURCE_USER}, ) - with patch( - "tests.components.directv.test_config_flow.MockDirectvClass.get_version", - side_effect=RequestException, - ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {}) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_ABORT - - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 - - -async def test_ssdp_discovery_confirm_unknown_error(hass: HomeAssistantType) -> None: - """Test we handle SSDP confirm unknown error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_SSDP}, - data={ATTR_SSDP_LOCATION: SSDP_LOCATION, ATTR_UPNP_SERIAL: UPNP_SERIAL}, + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=user_input, ) - with patch( - "tests.components.directv.test_config_flow.MockDirectvClass.get_version", - side_effect=Exception, - ) as mock_validate_input: - result = await async_configure_flow(hass, result["flow_id"], {}) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST - assert result["type"] == RESULT_TYPE_ABORT + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_RECEIVER_ID] == RECEIVER_ID - await hass.async_block_till_done() - assert len(mock_validate_input.mock_calls) == 1 + +async def test_full_ssdp_flow_implementation( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the full SSDP flow from start to finish.""" + mock_connection(aioclient_mock) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={CONF_SOURCE: SOURCE_SSDP}, data=discovery_info + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "ssdp_confirm" + assert result["description_placeholders"] == {CONF_NAME: HOST} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["title"] == HOST + + assert result["data"] + assert result["data"][CONF_HOST] == HOST + assert result["data"][CONF_RECEIVER_ID] == RECEIVER_ID diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index 02e97b9b015..0d806d668a0 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -1,7 +1,4 @@ -"""Tests for the Roku integration.""" -from asynctest import patch -from requests.exceptions import RequestException - +"""Tests for the DirecTV integration.""" from homeassistant.components.directv.const import DOMAIN from homeassistant.config_entries import ( ENTRY_STATE_LOADED, @@ -9,34 +6,36 @@ from homeassistant.config_entries import ( ENTRY_STATE_SETUP_RETRY, ) from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.setup import async_setup_component -from tests.components.directv import MockDirectvClass, setup_integration +from tests.components.directv import MOCK_CONFIG, mock_connection, setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker # pylint: disable=redefined-outer-name -async def test_config_entry_not_ready(hass: HomeAssistantType) -> None: +async def test_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the DirecTV setup from configuration.""" + mock_connection(aioclient_mock) + assert await async_setup_component(hass, DOMAIN, MOCK_CONFIG) + + +async def test_config_entry_not_ready( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test the DirecTV configuration entry not ready.""" - with patch( - "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.DIRECTV.get_locations", - side_effect=RequestException, - ): - entry = await setup_integration(hass) + entry = await setup_integration(hass, aioclient_mock, setup_error=True) assert entry.state == ENTRY_STATE_SETUP_RETRY -async def test_unload_config_entry(hass: HomeAssistantType) -> None: +async def test_unload_config_entry( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test the DirecTV configuration entry unloading.""" - with patch( - "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.media_player.async_setup_entry", - return_value=True, - ): - entry = await setup_integration(hass) + entry = await setup_integration(hass, aioclient_mock) assert entry.entry_id in hass.data[DOMAIN] assert entry.state == ENTRY_STATE_LOADED diff --git a/tests/components/directv/test_media_player.py b/tests/components/directv/test_media_player.py index f7cf63355a8..698e6ddac31 100644 --- a/tests/components/directv/test_media_player.py +++ b/tests/components/directv/test_media_player.py @@ -4,7 +4,6 @@ from typing import Optional from asynctest import patch from pytest import fixture -from requests import RequestException from homeassistant.components.directv.media_player import ( ATTR_MEDIA_CURRENTLY_RECORDING, @@ -24,6 +23,7 @@ from homeassistant.components.media_player.const import ( ATTR_MEDIA_SERIES_TITLE, ATTR_MEDIA_TITLE, DOMAIN as MP_DOMAIN, + MEDIA_TYPE_MOVIE, MEDIA_TYPE_TVSHOW, SERVICE_PLAY_MEDIA, SUPPORT_NEXT_TRACK, @@ -44,7 +44,6 @@ from homeassistant.const import ( SERVICE_MEDIA_STOP, SERVICE_TURN_OFF, SERVICE_TURN_ON, - STATE_OFF, STATE_PAUSED, STATE_PLAYING, STATE_UNAVAILABLE, @@ -52,18 +51,13 @@ from homeassistant.const import ( from homeassistant.helpers.typing import HomeAssistantType from homeassistant.util import dt as dt_util -from tests.common import MockConfigEntry, async_fire_time_changed -from tests.components.directv import ( - DOMAIN, - MOCK_GET_LOCATIONS_MULTIPLE, - RECORDING, - MockDirectvClass, - setup_integration, -) +from tests.components.directv import setup_integration +from tests.test_util.aiohttp import AiohttpClientMocker ATTR_UNIQUE_ID = "unique_id" -CLIENT_ENTITY_ID = f"{MP_DOMAIN}.bedroom_client" -MAIN_ENTITY_ID = f"{MP_DOMAIN}.main_dvr" +CLIENT_ENTITY_ID = f"{MP_DOMAIN}.client" +MAIN_ENTITY_ID = f"{MP_DOMAIN}.host" +UNAVAILABLE_ENTITY_ID = f"{MP_DOMAIN}.unavailable_client" # pylint: disable=redefined-outer-name @@ -74,29 +68,6 @@ def mock_now() -> datetime: return dt_util.utcnow() -async def setup_directv(hass: HomeAssistantType) -> MockConfigEntry: - """Set up mock DirecTV integration.""" - with patch( - "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, - ): - return await setup_integration(hass) - - -async def setup_directv_with_locations(hass: HomeAssistantType) -> MockConfigEntry: - """Set up mock DirecTV integration.""" - with patch( - "tests.components.directv.test_media_player.MockDirectvClass.get_locations", - return_value=MOCK_GET_LOCATIONS_MULTIPLE, - ): - with patch( - "homeassistant.components.directv.DIRECTV", new=MockDirectvClass, - ), patch( - "homeassistant.components.directv.media_player.DIRECTV", - new=MockDirectvClass, - ): - return await setup_integration(hass) - - async def async_turn_on( hass: HomeAssistantType, entity_id: Optional[str] = None ) -> None: @@ -172,23 +143,21 @@ async def async_play_media( await hass.services.async_call(MP_DOMAIN, SERVICE_PLAY_MEDIA, data) -async def test_setup(hass: HomeAssistantType) -> None: +async def test_setup( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test setup with basic config.""" - await setup_directv(hass) - assert hass.states.get(MAIN_ENTITY_ID) - - -async def test_setup_with_multiple_locations(hass: HomeAssistantType) -> None: - """Test setup with basic config with client location.""" - await setup_directv_with_locations(hass) - + await setup_integration(hass, aioclient_mock) assert hass.states.get(MAIN_ENTITY_ID) assert hass.states.get(CLIENT_ENTITY_ID) + assert hass.states.get(UNAVAILABLE_ENTITY_ID) -async def test_unique_id(hass: HomeAssistantType) -> None: +async def test_unique_id( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test unique id.""" - await setup_directv_with_locations(hass) + await setup_integration(hass, aioclient_mock) entity_registry = await hass.helpers.entity_registry.async_get_registry() @@ -198,10 +167,15 @@ async def test_unique_id(hass: HomeAssistantType) -> None: client = entity_registry.async_get(CLIENT_ENTITY_ID) assert client.unique_id == "2CA17D1CD30X" + unavailable_client = entity_registry.async_get(UNAVAILABLE_ENTITY_ID) + assert unavailable_client.unique_id == "9XXXXXXXXXX9" -async def test_supported_features(hass: HomeAssistantType) -> None: + +async def test_supported_features( + hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker +) -> None: """Test supported features.""" - await setup_directv_with_locations(hass) + await setup_integration(hass, aioclient_mock) # Features supported for main DVR state = hass.states.get(MAIN_ENTITY_ID) @@ -231,168 +205,123 @@ async def test_supported_features(hass: HomeAssistantType) -> None: async def test_check_attributes( - hass: HomeAssistantType, mock_now: dt_util.dt.datetime + hass: HomeAssistantType, + mock_now: dt_util.dt.datetime, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test attributes.""" - await setup_directv_with_locations(hass) + await setup_integration(hass, aioclient_mock) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) - await hass.async_block_till_done() + state = hass.states.get(MAIN_ENTITY_ID) + assert state.state == STATE_PLAYING - # Start playing TV - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - await async_media_play(hass, CLIENT_ENTITY_ID) - await hass.async_block_till_done() + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "17016356" + assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_MOVIE + assert state.attributes.get(ATTR_MEDIA_DURATION) == 7200 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 4437 + assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) + assert state.attributes.get(ATTR_MEDIA_TITLE) == "Snow Bride" + assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) is None + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format("HALLHD", "312") + assert state.attributes.get(ATTR_INPUT_SOURCE) == "312" + assert not state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) + assert state.attributes.get(ATTR_MEDIA_RATING) == "TV-G" + assert not state.attributes.get(ATTR_MEDIA_RECORDED) + assert state.attributes.get(ATTR_MEDIA_START_TIME) == datetime( + 2020, 3, 21, 13, 0, tzinfo=dt_util.UTC + ) state = hass.states.get(CLIENT_ENTITY_ID) assert state.state == STATE_PLAYING - assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == RECORDING["programId"] + assert state.attributes.get(ATTR_MEDIA_CONTENT_ID) == "4405732" assert state.attributes.get(ATTR_MEDIA_CONTENT_TYPE) == MEDIA_TYPE_TVSHOW - assert state.attributes.get(ATTR_MEDIA_DURATION) == RECORDING["duration"] - assert state.attributes.get(ATTR_MEDIA_POSITION) == 2 - assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) == next_update - assert state.attributes.get(ATTR_MEDIA_TITLE) == RECORDING["title"] - assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == RECORDING["episodeTitle"] - assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format( - RECORDING["callsign"], RECORDING["major"] - ) - assert state.attributes.get(ATTR_INPUT_SOURCE) == RECORDING["major"] - assert ( - state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) == RECORDING["isRecording"] - ) - assert state.attributes.get(ATTR_MEDIA_RATING) == RECORDING["rating"] + assert state.attributes.get(ATTR_MEDIA_DURATION) == 1791 + assert state.attributes.get(ATTR_MEDIA_POSITION) == 263 + assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) + assert state.attributes.get(ATTR_MEDIA_TITLE) == "Tyler's Ultimate" + assert state.attributes.get(ATTR_MEDIA_SERIES_TITLE) == "Spaghetti and Clam Sauce" + assert state.attributes.get(ATTR_MEDIA_CHANNEL) == "{} ({})".format("FOODHD", "231") + assert state.attributes.get(ATTR_INPUT_SOURCE) == "231" + assert not state.attributes.get(ATTR_MEDIA_CURRENTLY_RECORDING) + assert state.attributes.get(ATTR_MEDIA_RATING) == "No Rating" assert state.attributes.get(ATTR_MEDIA_RECORDED) assert state.attributes.get(ATTR_MEDIA_START_TIME) == datetime( - 2018, 11, 10, 19, 0, tzinfo=dt_util.UTC + 2010, 7, 5, 15, 0, 8, tzinfo=dt_util.UTC ) + state = hass.states.get(UNAVAILABLE_ENTITY_ID) + assert state.state == STATE_UNAVAILABLE + + +async def test_attributes_paused( + hass: HomeAssistantType, + mock_now: dt_util.dt.datetime, + aioclient_mock: AiohttpClientMocker, +): + """Test attributes while paused.""" + await setup_integration(hass, aioclient_mock) + + state = hass.states.get(CLIENT_ENTITY_ID) + last_updated = state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) + # Test to make sure that ATTR_MEDIA_POSITION_UPDATED_AT is not # updated if TV is paused. with patch( - "homeassistant.util.dt.utcnow", return_value=next_update + timedelta(minutes=5) + "homeassistant.util.dt.utcnow", return_value=mock_now + timedelta(minutes=5) ): await async_media_pause(hass, CLIENT_ENTITY_ID) await hass.async_block_till_done() state = hass.states.get(CLIENT_ENTITY_ID) assert state.state == STATE_PAUSED - assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) == next_update + assert state.attributes.get(ATTR_MEDIA_POSITION_UPDATED_AT) == last_updated async def test_main_services( - hass: HomeAssistantType, mock_now: dt_util.dt.datetime + hass: HomeAssistantType, + mock_now: dt_util.dt.datetime, + aioclient_mock: AiohttpClientMocker, ) -> None: """Test the different services.""" - await setup_directv(hass) + await setup_integration(hass, aioclient_mock) - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) + with patch("directv.DIRECTV.remote") as remote_mock: + await async_turn_off(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() - # DVR starts in off state. - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_OFF + remote_mock.assert_called_once_with("poweroff", "0") - # Turn main DVR on. When turning on DVR is playing. - await async_turn_on(hass, MAIN_ENTITY_ID) - await hass.async_block_till_done() - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_PLAYING - - # Pause live TV. - await async_media_pause(hass, MAIN_ENTITY_ID) - await hass.async_block_till_done() - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_PAUSED - - # Start play again for live TV. - await async_media_play(hass, MAIN_ENTITY_ID) - await hass.async_block_till_done() - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_PLAYING - - # Change channel, currently it should be 202 - assert state.attributes.get("source") == 202 - await async_play_media(hass, "channel", 7, MAIN_ENTITY_ID) - await hass.async_block_till_done() - state = hass.states.get(MAIN_ENTITY_ID) - assert state.attributes.get("source") == 7 - - # Stop live TV. - await async_media_stop(hass, MAIN_ENTITY_ID) - await hass.async_block_till_done() - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_PAUSED - - # Turn main DVR off. - await async_turn_off(hass, MAIN_ENTITY_ID) - await hass.async_block_till_done() - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_OFF - - -async def test_available( - hass: HomeAssistantType, mock_now: dt_util.dt.datetime -) -> None: - """Test available status.""" - entry = await setup_directv(hass) - - next_update = mock_now + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) + with patch("directv.DIRECTV.remote") as remote_mock: + await async_turn_on(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() + remote_mock.assert_called_once_with("poweron", "0") - # Confirm service is currently set to available. - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state != STATE_UNAVAILABLE - - assert hass.data[DOMAIN] - assert hass.data[DOMAIN][entry.entry_id] - assert hass.data[DOMAIN][entry.entry_id]["client"] - - main_dtv = hass.data[DOMAIN][entry.entry_id]["client"] - - # Make update fail 1st time - next_update = next_update + timedelta(minutes=5) - with patch.object(main_dtv, "get_standby", side_effect=RequestException), patch( - "homeassistant.util.dt.utcnow", return_value=next_update - ): - async_fire_time_changed(hass, next_update) + with patch("directv.DIRECTV.remote") as remote_mock: + await async_media_pause(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() + remote_mock.assert_called_once_with("pause", "0") - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state != STATE_UNAVAILABLE - - # Make update fail 2nd time within 1 minute - next_update = next_update + timedelta(seconds=30) - with patch.object(main_dtv, "get_standby", side_effect=RequestException), patch( - "homeassistant.util.dt.utcnow", return_value=next_update - ): - async_fire_time_changed(hass, next_update) + with patch("directv.DIRECTV.remote") as remote_mock: + await async_media_play(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() + remote_mock.assert_called_once_with("play", "0") - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state != STATE_UNAVAILABLE - - # Make update fail 3rd time more then a minute after 1st failure - next_update = next_update + timedelta(minutes=1) - with patch.object(main_dtv, "get_standby", side_effect=RequestException), patch( - "homeassistant.util.dt.utcnow", return_value=next_update - ): - async_fire_time_changed(hass, next_update) + with patch("directv.DIRECTV.remote") as remote_mock: + await async_media_next_track(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() + remote_mock.assert_called_once_with("ffwd", "0") - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state == STATE_UNAVAILABLE - - # Recheck state, update should work again. - next_update = next_update + timedelta(minutes=5) - with patch("homeassistant.util.dt.utcnow", return_value=next_update): - async_fire_time_changed(hass, next_update) + with patch("directv.DIRECTV.remote") as remote_mock: + await async_media_previous_track(hass, MAIN_ENTITY_ID) await hass.async_block_till_done() + remote_mock.assert_called_once_with("rew", "0") - state = hass.states.get(MAIN_ENTITY_ID) - assert state.state != STATE_UNAVAILABLE + with patch("directv.DIRECTV.remote") as remote_mock: + await async_media_stop(hass, MAIN_ENTITY_ID) + await hass.async_block_till_done() + remote_mock.assert_called_once_with("stop", "0") + + with patch("directv.DIRECTV.tune") as tune_mock: + await async_play_media(hass, "channel", 312, MAIN_ENTITY_ID) + await hass.async_block_till_done() + tune_mock.assert_called_once_with("312", "0") diff --git a/tests/fixtures/directv/info-get-locations.json b/tests/fixtures/directv/info-get-locations.json new file mode 100644 index 00000000000..5279bcebefc --- /dev/null +++ b/tests/fixtures/directv/info-get-locations.json @@ -0,0 +1,22 @@ +{ + "locations": [ + { + "clientAddr": "0", + "locationName": "Host" + }, + { + "clientAddr": "2CA17D1CD30X", + "locationName": "Client" + }, + { + "clientAddr": "9XXXXXXXXXX9", + "locationName": "Unavailable Client" + } + ], + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK.", + "query": "/info/getLocations?callback=jsonp" + } +} diff --git a/tests/fixtures/directv/info-get-version.json b/tests/fixtures/directv/info-get-version.json new file mode 100644 index 00000000000..074e1b89dd8 --- /dev/null +++ b/tests/fixtures/directv/info-get-version.json @@ -0,0 +1,13 @@ +{ + "accessCardId": "0021-1495-6572", + "receiverId": "0288 7745 5858", + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK", + "query": "/info/getVersion" + }, + "stbSoftwareVersion": "0x4ed7", + "systemTime": 1281625203, + "version": "1.2" +} diff --git a/tests/fixtures/directv/info-mode-error.json b/tests/fixtures/directv/info-mode-error.json new file mode 100644 index 00000000000..72bc39b1f5a --- /dev/null +++ b/tests/fixtures/directv/info-mode-error.json @@ -0,0 +1,8 @@ +{ + "status": { + "code": 500, + "commandResult": 1, + "msg": "Internal Server Error.", + "query": "/info/mode" + } +} diff --git a/tests/fixtures/directv/info-mode.json b/tests/fixtures/directv/info-mode.json new file mode 100644 index 00000000000..f1c731a07aa --- /dev/null +++ b/tests/fixtures/directv/info-mode.json @@ -0,0 +1,9 @@ +{ + "mode": 0, + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK", + "query": "/info/mode" + } +} diff --git a/tests/fixtures/directv/remote-process-key.json b/tests/fixtures/directv/remote-process-key.json new file mode 100644 index 00000000000..7f73e02acc7 --- /dev/null +++ b/tests/fixtures/directv/remote-process-key.json @@ -0,0 +1,10 @@ +{ + "hold": "keyPress", + "key": "ANY", + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK", + "query": "/remote/processKey?key=ANY&hold=keyPress" + } +} diff --git a/tests/fixtures/directv/tv-get-tuned-movie.json b/tests/fixtures/directv/tv-get-tuned-movie.json new file mode 100644 index 00000000000..5411e7c7951 --- /dev/null +++ b/tests/fixtures/directv/tv-get-tuned-movie.json @@ -0,0 +1,24 @@ +{ + "callsign": "HALLHD", + "date": "2013", + "duration": 7200, + "isOffAir": false, + "isPclocked": 3, + "isPpv": false, + "isRecording": false, + "isVod": false, + "major": 312, + "minor": 65535, + "offset": 4437, + "programId": "17016356", + "rating": "TV-G", + "startTime": 1584795600, + "stationId": 6580971, + "title": "Snow Bride", + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK.", + "query": "/tv/getTuned" + } +} diff --git a/tests/fixtures/directv/tv-get-tuned.json b/tests/fixtures/directv/tv-get-tuned.json new file mode 100644 index 00000000000..dc4e4092003 --- /dev/null +++ b/tests/fixtures/directv/tv-get-tuned.json @@ -0,0 +1,32 @@ +{ + "callsign": "FOODHD", + "date": "20070324", + "duration": 1791, + "episodeTitle": "Spaghetti and Clam Sauce", + "expiration": "0", + "expiryTime": 0, + "isOffAir": false, + "isPartial": false, + "isPclocked": 1, + "isPpv": false, + "isRecording": false, + "isViewed": true, + "isVod": false, + "keepUntilFull": true, + "major": 231, + "minor": 65535, + "offset": 263, + "programId": "4405732", + "rating": "No Rating", + "recType": 3, + "startTime": 1278342008, + "stationId": 3900976, + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK.", + "query": "/tv/getTuned" + }, + "title": "Tyler's Ultimate", + "uniqueId": "6728716739474078694" +} diff --git a/tests/fixtures/directv/tv-tune.json b/tests/fixtures/directv/tv-tune.json new file mode 100644 index 00000000000..39af4fe7a4e --- /dev/null +++ b/tests/fixtures/directv/tv-tune.json @@ -0,0 +1,8 @@ +{ + "status": { + "code": 200, + "commandResult": 0, + "msg": "OK", + "query": "/tv/tune?major=508" + } +} From 955c94e313a476d408967a2a09ae53b4ba60f353 Mon Sep 17 00:00:00 2001 From: Kit Klein <33464407+kit-klein@users.noreply.github.com> Date: Tue, 31 Mar 2020 18:50:37 -0400 Subject: [PATCH 355/431] allow overriding host api url in config flow (#33481) * allow overriding host api url in config flow * fix typo * capitalize URL --- .../konnected/.translations/en.json | 7 +++- .../components/konnected/__init__.py | 36 +++++++++++++------ .../components/konnected/config_flow.py | 26 ++++++++++++-- homeassistant/components/konnected/panel.py | 4 ++- .../components/konnected/strings.json | 8 +++-- .../components/konnected/test_config_flow.py | 25 ++++++++++--- tests/components/konnected/test_init.py | 5 +++ 7 files changed, 91 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/konnected/.translations/en.json b/homeassistant/components/konnected/.translations/en.json index ae41b64ad98..3ace7783f8b 100644 --- a/homeassistant/components/konnected/.translations/en.json +++ b/homeassistant/components/konnected/.translations/en.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "Not a recognized Konnected.io device" }, + "error": { + "bad_host": "Invalid Override API host url" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "Blink panel LED on when sending state change" + "api_host": "Override API host URL (optional)", + "blink": "Blink panel LED on when sending state change", + "override_api_host": "Override default Home Assistant API host panel URL" }, "description": "Please select the desired behavior for your panel", "title": "Configure Misc" diff --git a/homeassistant/components/konnected/__init__.py b/homeassistant/components/konnected/__init__.py index 72d82fd31be..e5185ff03bc 100644 --- a/homeassistant/components/konnected/__init__.py +++ b/homeassistant/components/konnected/__init__.py @@ -91,7 +91,7 @@ def ensure_zone(value): return str(value) -def import_validator(config): +def import_device_validator(config): """Validate zones and reformat for import.""" config = copy.deepcopy(config) io_cfgs = {} @@ -117,10 +117,22 @@ def import_validator(config): config.pop(CONF_SWITCHES, None) config.pop(CONF_BLINK, None) config.pop(CONF_DISCOVERY, None) + config.pop(CONF_API_HOST, None) config.pop(CONF_IO, None) return config +def import_validator(config): + """Reformat for import.""" + config = copy.deepcopy(config) + + # push api_host into device configs + for device in config.get(CONF_DEVICES, []): + device[CONF_API_HOST] = config.get(CONF_API_HOST, "") + + return config + + # configuration.yaml schemas (legacy) BINARY_SENSOR_SCHEMA_YAML = vol.All( vol.Schema( @@ -179,23 +191,27 @@ DEVICE_SCHEMA_YAML = vol.All( vol.Inclusive(CONF_HOST, "host_info"): cv.string, vol.Inclusive(CONF_PORT, "host_info"): cv.port, vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_API_HOST, default=""): vol.Any("", cv.url), vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, } ), - import_validator, + import_device_validator, ) # pylint: disable=no-value-for-parameter CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Required(CONF_ACCESS_TOKEN): cv.string, - vol.Optional(CONF_API_HOST): vol.Url(), - vol.Optional(CONF_DEVICES): vol.All( - cv.ensure_list, [DEVICE_SCHEMA_YAML] - ), - } + DOMAIN: vol.All( + import_validator, + vol.Schema( + { + vol.Required(CONF_ACCESS_TOKEN): cv.string, + vol.Optional(CONF_API_HOST): vol.Url(), + vol.Optional(CONF_DEVICES): vol.All( + cv.ensure_list, [DEVICE_SCHEMA_YAML] + ), + } + ), ) }, extra=vol.ALLOW_EXTRA, diff --git a/homeassistant/components/konnected/config_flow.py b/homeassistant/components/konnected/config_flow.py index 172f60cd42d..6a3631a8c0d 100644 --- a/homeassistant/components/konnected/config_flow.py +++ b/homeassistant/components/konnected/config_flow.py @@ -31,6 +31,7 @@ from homeassistant.helpers import config_validation as cv from .const import ( CONF_ACTIVATION, + CONF_API_HOST, CONF_BLINK, CONF_DEFAULT_OPTIONS, CONF_DISCOVERY, @@ -61,6 +62,8 @@ CONF_MORE_STATES = "more_states" CONF_YES = "Yes" CONF_NO = "No" +CONF_OVERRIDE_API_HOST = "override_api_host" + KONN_MANUFACTURER = "konnected.io" KONN_PANEL_MODEL_NAMES = { KONN_MODEL: "Konnected Alarm Panel", @@ -138,6 +141,7 @@ OPTIONS_SCHEMA = vol.Schema( vol.Optional(CONF_SENSORS): vol.All(cv.ensure_list, [SENSOR_SCHEMA]), vol.Optional(CONF_SWITCHES): vol.All(cv.ensure_list, [SWITCH_SCHEMA]), vol.Optional(CONF_BLINK, default=True): cv.boolean, + vol.Optional(CONF_API_HOST, default=""): vol.Any("", cv.url), vol.Optional(CONF_DISCOVERY, default=True): cv.boolean, }, extra=vol.REMOVE_EXTRA, @@ -785,8 +789,19 @@ class OptionsFlowHandler(config_entries.OptionsFlow): """Allow the user to configure the LED behavior.""" errors = {} if user_input is not None: - self.new_opt[CONF_BLINK] = user_input[CONF_BLINK] - return self.async_create_entry(title="", data=self.new_opt) + # config schema only does basic schema val so check url here + try: + if user_input[CONF_OVERRIDE_API_HOST]: + cv.url(user_input.get(CONF_API_HOST, "")) + else: + user_input[CONF_API_HOST] = "" + except vol.Invalid: + errors["base"] = "bad_host" + else: + # no need to store the override - can infer + del user_input[CONF_OVERRIDE_API_HOST] + self.new_opt.update(user_input) + return self.async_create_entry(title="", data=self.new_opt) return self.async_show_form( step_id="options_misc", @@ -795,6 +810,13 @@ class OptionsFlowHandler(config_entries.OptionsFlow): vol.Required( CONF_BLINK, default=self.current_opt.get(CONF_BLINK, True) ): bool, + vol.Required( + CONF_OVERRIDE_API_HOST, + default=bool(self.current_opt.get(CONF_API_HOST)), + ): bool, + vol.Optional( + CONF_API_HOST, default=self.current_opt.get(CONF_API_HOST, "") + ): str, } ), errors=errors, diff --git a/homeassistant/components/konnected/panel.py b/homeassistant/components/konnected/panel.py index 783aa78b8b1..efb1e83a728 100644 --- a/homeassistant/components/konnected/panel.py +++ b/homeassistant/components/konnected/panel.py @@ -294,7 +294,9 @@ class AlarmPanel: @callback def async_desired_settings_payload(self): """Return a dict representing the desired device configuration.""" - desired_api_host = ( + # keeping self.hass.data check for backwards compatibility + # newly configured integrations store this in the config entry + desired_api_host = self.options.get(CONF_API_HOST) or ( self.hass.data[DOMAIN].get(CONF_API_HOST) or self.hass.config.api.base_url ) desired_api_endpoint = desired_api_host + ENDPOINT_ROOT diff --git a/homeassistant/components/konnected/strings.json b/homeassistant/components/konnected/strings.json index f1d7ef43ddc..0ea8a40bc0a 100644 --- a/homeassistant/components/konnected/strings.json +++ b/homeassistant/components/konnected/strings.json @@ -94,11 +94,15 @@ "title": "Configure Misc", "description": "Please select the desired behavior for your panel", "data": { - "blink": "Blink panel LED on when sending state change" + "blink": "Blink panel LED on when sending state change", + "override_api_host": "Override default Home Assistant API host panel URL", + "api_host": "Override API host URL (optional)" } } }, - "error": {}, + "error": { + "bad_host": "Invalid Override API host url" + }, "abort": { "not_konn_panel": "Not a recognized Konnected.io device" } diff --git a/tests/components/konnected/test_config_flow.py b/tests/components/konnected/test_config_flow.py index 35814154f47..917afc5357a 100644 --- a/tests/components/konnected/test_config_flow.py +++ b/tests/components/konnected/test_config_flow.py @@ -450,6 +450,7 @@ async def test_import_existing_config(hass, mock_panel): "alarm1": "Switchable Output", }, "blink": True, + "api_host": "", "discovery": True, "binary_sensors": [ {"zone": "2", "type": "door", "inverse": False}, @@ -628,6 +629,7 @@ async def test_import_pin_config(hass, mock_panel): "out": "Switchable Output", }, "blink": True, + "api_host": "", "discovery": True, "binary_sensors": [ {"zone": "1", "type": "door", "inverse": False}, @@ -778,9 +780,21 @@ async def test_option_flow(hass, mock_panel): assert result["type"] == "form" assert result["step_id"] == "options_misc" - + # make sure we enforce url format result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"blink": True}, + result["flow_id"], + user_input={"blink": True, "override_api_host": True, "api_host": "badhosturl"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "options_misc" + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + "blink": True, + "override_api_host": True, + "api_host": "http://overridehost:1111", + }, ) assert result["type"] == "create_entry" assert result["data"] == { @@ -792,6 +806,7 @@ async def test_option_flow(hass, mock_panel): "out": "Switchable Output", }, "blink": True, + "api_host": "http://overridehost:1111", "binary_sensors": [ {"zone": "2", "type": "door", "inverse": False}, {"zone": "6", "type": "window", "name": "winder", "inverse": True}, @@ -958,7 +973,7 @@ async def test_option_flow_pro(hass, mock_panel): assert result["step_id"] == "options_misc" result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"blink": True}, + result["flow_id"], user_input={"blink": True, "override_api_host": False}, ) assert result["type"] == "create_entry" @@ -976,6 +991,7 @@ async def test_option_flow_pro(hass, mock_panel): "out1": "Switchable Output", }, "blink": True, + "api_host": "", "binary_sensors": [ {"zone": "2", "type": "door", "inverse": False}, {"zone": "6", "type": "window", "name": "winder", "inverse": True}, @@ -1121,7 +1137,7 @@ async def test_option_flow_import(hass, mock_panel): schema = result["data_schema"]({}) assert schema["blink"] is True result = await hass.config_entries.options.async_configure( - result["flow_id"], user_input={"blink": False}, + result["flow_id"], user_input={"blink": False, "override_api_host": False}, ) # verify the updated fields @@ -1129,6 +1145,7 @@ async def test_option_flow_import(hass, mock_panel): assert result["data"] == { "io": {"1": "Binary Sensor", "2": "Digital Sensor", "3": "Switchable Output"}, "blink": False, + "api_host": "", "binary_sensors": [ {"zone": "1", "type": "door", "inverse": True, "name": "winder"}, ], diff --git a/tests/components/konnected/test_init.py b/tests/components/konnected/test_init.py index a678716bc03..2a9c3f8cd4f 100644 --- a/tests/components/konnected/test_init.py +++ b/tests/components/konnected/test_init.py @@ -43,6 +43,7 @@ async def test_config_schema(hass): """Test that config schema is imported properly.""" config = { konnected.DOMAIN: { + konnected.CONF_API_HOST: "http://1.1.1.1:8888", konnected.CONF_ACCESS_TOKEN: "abcdefgh", konnected.CONF_DEVICES: [{konnected.CONF_ID: "aabbccddeeff"}], } @@ -50,10 +51,12 @@ async def test_config_schema(hass): assert konnected.CONFIG_SCHEMA(config) == { "konnected": { "access_token": "abcdefgh", + "api_host": "http://1.1.1.1:8888", "devices": [ { "default_options": { "blink": True, + "api_host": "http://1.1.1.1:8888", "discovery": True, "io": { "1": "Disabled", @@ -96,6 +99,7 @@ async def test_config_schema(hass): { "default_options": { "blink": True, + "api_host": "", "discovery": True, "io": { "1": "Disabled", @@ -162,6 +166,7 @@ async def test_config_schema(hass): { "default_options": { "blink": True, + "api_host": "", "discovery": True, "io": { "1": "Binary Sensor", From 83fb5e50719626dfcd37a5f7a7233738c192964b Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 31 Mar 2020 18:40:07 -0500 Subject: [PATCH 356/431] Apply recommendations from IPP review (#33477) * Update test_config_flow.py * Update test_config_flow.py * lint. --- tests/components/ipp/test_config_flow.py | 128 +++++++++-------------- 1 file changed, 52 insertions(+), 76 deletions(-) diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 505ba618505..5a2744eac51 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -2,12 +2,15 @@ import aiohttp from pyipp import IPPConnectionUpgradeRequired -from homeassistant import data_entry_flow -from homeassistant.components.ipp import config_flow -from homeassistant.components.ipp.const import CONF_BASE_PATH, CONF_UUID +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 from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from . import ( MOCK_USER_INPUT, @@ -23,25 +26,11 @@ from tests.test_util.aiohttp import AiohttpClientMocker async def test_show_user_form(hass: HomeAssistant) -> None: """Test that the user set up form is served.""" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: - """Test that the zeroconf confirmation form is served.""" - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - flow.discovery_info = {CONF_NAME: "EPSON123456"} - - result = await flow.async_step_zeroconf_confirm() - - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + assert result["type"] == RESULT_TYPE_FORM async def test_show_zeroconf_form( @@ -54,18 +43,13 @@ async def test_show_zeroconf_form( headers={"Content-Type": "application/ipp"}, ) - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() - result = await flow.async_step_zeroconf(discovery_info) - - assert flow.discovery_info[CONF_HOST] == "EPSON123456.local" - assert flow.discovery_info[CONF_NAME] == "EPSON123456" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} @@ -79,11 +63,11 @@ async def test_connection_error( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "connection_error"} @@ -95,10 +79,10 @@ async def test_zeroconf_connection_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "connection_error" @@ -110,7 +94,7 @@ async def test_zeroconf_confirm_connection_error( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={ "source": SOURCE_ZEROCONF, CONF_HOST: "EPSON123456.local", @@ -119,7 +103,7 @@ async def test_zeroconf_confirm_connection_error( data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "connection_error" @@ -133,11 +117,11 @@ async def test_user_connection_upgrade_required( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "connection_upgrade"} @@ -151,10 +135,10 @@ async def test_zeroconf_connection_upgrade_required( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "connection_upgrade" @@ -166,10 +150,10 @@ async def test_user_device_exists_abort( user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, data=user_input, + DOMAIN, context={"source": SOURCE_USER}, data=user_input, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -181,10 +165,10 @@ async def test_zeroconf_device_exists_abort( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -197,10 +181,10 @@ async def test_zeroconf_with_uuid_device_exists_abort( discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() discovery_info["properties"]["UUID"] = "cfe92100-67c4-11d4-a45f-f8d027761251" result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -215,18 +199,18 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, context={"source": SOURCE_USER}, + DOMAIN, context={"source": SOURCE_USER}, ) assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["type"] == RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "EPSON123456.local" assert result["data"] @@ -244,25 +228,19 @@ async def test_full_zeroconf_flow_implementation( headers={"Content-Type": "application/ipp"}, ) - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() - result = await flow.async_step_zeroconf(discovery_info) - - assert flow.discovery_info - assert flow.discovery_info[CONF_HOST] == "EPSON123456.local" - assert flow.discovery_info[CONF_NAME] == "EPSON123456" - - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - result = await flow.async_step_zeroconf_confirm( - user_input={CONF_HOST: "EPSON123456.local"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == RESULT_TYPE_FORM + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "EPSON123456" assert result["data"] @@ -281,22 +259,20 @@ async def test_full_zeroconf_tls_flow_implementation( headers={"Content-Type": "application/ipp"}, ) - flow = config_flow.IPPFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - discovery_info = MOCK_ZEROCONF_IPPS_SERVICE_INFO.copy() - result = await flow.async_step_zeroconf(discovery_info) - - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} - - result = await flow.async_step_zeroconf_confirm( - user_input={CONF_HOST: "EPSON123456.local"} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["step_id"] == "zeroconf_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["description_placeholders"] == {CONF_NAME: "EPSON123456"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY assert result["title"] == "EPSON123456" assert result["data"] From 2cfa0af532340498bf303c9e26199fab3bad20cb Mon Sep 17 00:00:00 2001 From: HomeAssistant Azure Date: Wed, 1 Apr 2020 00:05:04 +0000 Subject: [PATCH 357/431] [ci skip] Translation update --- .../components/directv/.translations/pl.json | 3 +- .../components/doorbird/.translations/en.json | 68 +++++++++---------- .../components/elkm1/.translations/es.json | 18 ++++- .../components/elkm1/.translations/lb.json | 26 +++++++ .../components/elkm1/.translations/ru.json | 18 +++++ .../components/harmony/.translations/pl.json | 38 +++++++++++ .../huawei_lte/.translations/pl.json | 4 +- .../components/ipp/.translations/de.json | 32 +++++++++ .../components/ipp/.translations/es.json | 32 +++++++++ .../components/ipp/.translations/lb.json | 31 +++++++++ .../components/ipp/.translations/ru.json | 32 +++++++++ .../components/ipp/.translations/zh-Hant.json | 32 +++++++++ .../konnected/.translations/es.json | 1 + .../konnected/.translations/ru.json | 1 + .../konnected/.translations/zh-Hant.json | 2 +- .../components/rachio/.translations/pl.json | 31 +++++++++ .../components/vizio/.translations/pl.json | 6 +- 17 files changed, 332 insertions(+), 43 deletions(-) create mode 100644 homeassistant/components/elkm1/.translations/lb.json create mode 100644 homeassistant/components/harmony/.translations/pl.json create mode 100644 homeassistant/components/ipp/.translations/de.json create mode 100644 homeassistant/components/ipp/.translations/es.json create mode 100644 homeassistant/components/ipp/.translations/lb.json create mode 100644 homeassistant/components/ipp/.translations/ru.json create mode 100644 homeassistant/components/ipp/.translations/zh-Hant.json create mode 100644 homeassistant/components/rachio/.translations/pl.json diff --git a/homeassistant/components/directv/.translations/pl.json b/homeassistant/components/directv/.translations/pl.json index 81305324f5e..c02e69601c8 100644 --- a/homeassistant/components/directv/.translations/pl.json +++ b/homeassistant/components/directv/.translations/pl.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany." + "already_configured": "Odbiornik DirecTV jest ju\u017c skonfigurowany.", + "unknown": "Niespodziewany b\u0142\u0105d." }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index 9b2c95dd7c9..27c16fac3a1 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -1,36 +1,36 @@ { - "options" : { - "step" : { - "init" : { - "data" : { - "events" : "Comma separated list of events." - }, - "description" : "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" - } - } - }, - "config" : { - "step" : { - "user" : { - "title" : "Connect to the DoorBird", - "data" : { - "password" : "Password", - "host" : "Host (IP Address)", - "name" : "Device Name", - "username" : "Username" + "config": { + "abort": { + "already_configured": "This DoorBird is already configured", + "link_local_address": "Link local addresses are not supported", + "not_doorbird_device": "This device is not a DoorBird" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "host": "Host (IP Address)", + "name": "Device Name", + "password": "Password", + "username": "Username" + }, + "title": "Connect to the DoorBird" } - } - }, - "abort" : { - "already_configured" : "This DoorBird is already configured", - "link_local_address": "Link local addresses are not supported", - "not_doorbird_device": "This device is not a DoorBird" - }, - "title" : "DoorBird", - "error" : { - "invalid_auth" : "Invalid authentication", - "unknown" : "Unexpected error", - "cannot_connect" : "Failed to connect, please try again" - } - } -} + }, + "title": "DoorBird" + }, + "options": { + "step": { + "init": { + "data": { + "events": "Comma separated list of events." + }, + "description": "Add an comma separated event name for each event you wish to track. After entering them here, use the DoorBird app to assign them to a specific event. See the documentation at https://www.home-assistant.io/integrations/doorbird/#events. Example: somebody_pressed_the_button, motion" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/es.json b/homeassistant/components/elkm1/.translations/es.json index 6602ff3da2e..8fdce004c41 100644 --- a/homeassistant/components/elkm1/.translations/es.json +++ b/homeassistant/components/elkm1/.translations/es.json @@ -1,5 +1,9 @@ { "config": { + "abort": { + "address_already_configured": "Ya est\u00e1 configurado un Elk-M1 con esta direcci\u00f3n", + "already_configured": "Ya est\u00e1 configurado un Elk-M1 con este prefijo" + }, "error": { "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", @@ -8,9 +12,17 @@ "step": { "user": { "data": { - "protocol": "Protocolo" - } + "address": "La direcci\u00f3n IP o dominio o puerto serie si se conecta a trav\u00e9s de serie.", + "password": "Contrase\u00f1a (s\u00f3lo seguro)", + "prefix": "Un prefijo \u00fanico (d\u00e9jalo en blanco si s\u00f3lo tienes un Elk-M1).", + "protocol": "Protocolo", + "temperature_unit": "La temperatura que usa la unidad Elk-M1", + "username": "Usuario (s\u00f3lo seguro)" + }, + "description": "La cadena de direcci\u00f3n debe estar en el formato 'direcci\u00f3n[:puerto]' para 'seguro' y 'no-seguro'. Ejemplo: '192.168.1.1'. El puerto es opcional y el valor predeterminado es 2101 para 'no-seguro' y 2601 para 'seguro'. Para el protocolo serie, la direcci\u00f3n debe tener la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. Los baudios son opcionales y el valor predeterminado es 115200.", + "title": "Conectar con Control Elk-M1" } - } + }, + "title": "Control Elk-M1" } } \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/lb.json b/homeassistant/components/elkm1/.translations/lb.json new file mode 100644 index 00000000000..6f451c94b5f --- /dev/null +++ b/homeassistant/components/elkm1/.translations/lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "address_already_configured": "Een ElkM1 mat d\u00ebser Adress ass scho konfigur\u00e9iert", + "already_configured": "Een ElkM1 mat d\u00ebsem Prefix ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "invalid_auth": "Ong\u00eblteg Authentifikatioun", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "password": "Passwuert (n\u00ebmmen ges\u00e9chert)", + "protocol": "Protokoll", + "temperature_unit": "Temperatur Eenheet d\u00e9i den ElkM1 benotzt.", + "username": "Benotzernumm (n\u00ebmmen ges\u00e9chert)" + }, + "description": "D'Adress muss an der Form 'adress[:port]' fir 'ges\u00e9chert' an 'onges\u00e9chert' sinn. Beispill: '192.168.1.1'. De Port os optionell an ass standardm\u00e9isseg op 2101 fir 'onges\u00e9chert' an op 2601 fir 'ges\u00e9chert' d\u00e9fin\u00e9iert. Fir de serielle Protokoll, muss d'Adress an der Form 'tty[:baud]' sinn. Beispill: '/dev/ttyS1'. Baud Rate ass optionell an ass standardmlisseg op 115200 d\u00e9fin\u00e9iert.", + "title": "Mat Elk-M1 Control verbannen" + } + }, + "title": "Elk-M1 Control" + } +} \ No newline at end of file diff --git a/homeassistant/components/elkm1/.translations/ru.json b/homeassistant/components/elkm1/.translations/ru.json index 1575b47ed68..11e04ad816c 100644 --- a/homeassistant/components/elkm1/.translations/ru.json +++ b/homeassistant/components/elkm1/.translations/ru.json @@ -1,7 +1,25 @@ { "config": { + "abort": { + "address_already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u0430\u0434\u0440\u0435\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0441 \u044d\u0442\u0438\u043c \u043f\u0440\u0435\u0444\u0438\u043a\u0441\u043e\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\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": { + "address": "IP-\u0430\u0434\u0440\u0435\u0441, \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u043e\u0440\u0442.", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'secure')", + "prefix": "\u0423\u043d\u0438\u043a\u0430\u043b\u044c\u043d\u044b\u0439 \u043f\u0440\u0435\u0444\u0438\u043a\u0441 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0435\u0441\u043b\u0438 \u0443 \u0412\u0430\u0441 \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u0438\u043d ElkM1).", + "protocol": "\u041f\u0440\u043e\u0442\u043e\u043a\u043e\u043b", + "temperature_unit": "\u0415\u0434\u0438\u043d\u0438\u0446\u0430 \u0438\u0437\u043c\u0435\u0440\u0435\u043d\u0438\u044f \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b", + "username": "\u041b\u043e\u0433\u0438\u043d (\u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'secure')" + }, + "description": "\u0421\u0442\u0440\u043e\u043a\u0430 \u0430\u0434\u0440\u0435\u0441\u0430 \u0434\u043e\u043b\u0436\u043d\u0430 \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0435 'addres[:port]' \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0439 'secure' \u0438 'non-secure'. \u041f\u0440\u0438\u043c\u0435\u0440: '192.168.1.1'. \u041f\u043e\u0440\u0442 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e 2101 \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'non-secure'\u00bb \u0438 2601 \u0434\u043b\u044f \u043e\u043f\u0446\u0438\u0438 'secure'\u00bb. \u0414\u043b\u044f \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u043e\u0433\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0430 \u0430\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0432 \u0444\u043e\u0440\u043c\u0435 'tty[:baud]'. \u041f\u0440\u0438\u043c\u0435\u0440: '/dev/ttyS1'. Baud \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u043c \u0438 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e \u0440\u0430\u0432\u0435\u043d 115200.", "title": "Elk-M1 Control" } }, diff --git a/homeassistant/components/harmony/.translations/pl.json b/homeassistant/components/harmony/.translations/pl.json new file mode 100644 index 00000000000..a9f611d0f35 --- /dev/null +++ b/homeassistant/components/harmony/.translations/pl.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane" + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "Czy chcesz skonfigurowa\u0107 {name} ({host})?", + "title": "Konfiguracja Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nazwa hosta lub adres IP", + "name": "Nazwa huba" + }, + "title": "Konfiguracja Logitech Harmony Hub" + } + }, + "title": "Logitech Harmony Hub" + }, + "options": { + "step": { + "init": { + "data": { + "activity": "Domy\u015blna aktywno\u015b\u0107 do wykonania, gdy \u017cadnej nie okre\u015blono.", + "delay_secs": "Op\u00f3\u017anienie mi\u0119dzy wysy\u0142aniem polece\u0144." + }, + "description": "Dostosuj opcje huba Harmony" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/huawei_lte/.translations/pl.json b/homeassistant/components/huawei_lte/.translations/pl.json index 4029b24df3f..17e4f7b8ef2 100644 --- a/homeassistant/components/huawei_lte/.translations/pl.json +++ b/homeassistant/components/huawei_lte/.translations/pl.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", - "already_in_progress": "To urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", + "already_in_progress": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "not_huawei_lte": "To nie jest urz\u0105dzenie Huawei LTE" }, "error": { diff --git a/homeassistant/components/ipp/.translations/de.json b/homeassistant/components/ipp/.translations/de.json new file mode 100644 index 00000000000..7e72fb2403f --- /dev/null +++ b/homeassistant/components/ipp/.translations/de.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Dieser Drucker ist bereits konfiguriert", + "connection_error": "Verbindung zum Drucker fehlgeschlagen.", + "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen, da ein Verbindungsupgrade erforderlich ist." + }, + "error": { + "connection_error": "Verbindung zum Drucker fehlgeschlagen.", + "connection_upgrade": "Verbindung zum Drucker fehlgeschlagen. Bitte versuchen Sie es erneut mit aktivierter SSL / TLS-Option." + }, + "flow_title": "Drucker: {name}", + "step": { + "user": { + "data": { + "base_path": "Relativer Pfad zum Drucker", + "host": "Host oder IP-Adresse", + "port": "Port", + "ssl": "Der Drucker unterst\u00fctzt die Kommunikation \u00fcber SSL / TLS", + "verify_ssl": "Der Drucker verwendet ein ordnungsgem\u00e4\u00dfes SSL-Zertifikat" + }, + "description": "Richten Sie Ihren Drucker \u00fcber das Internet Printing Protocol (IPP) f\u00fcr die Integration in Home Assistant ein.", + "title": "Verbinden Sie Ihren Drucker" + }, + "zeroconf_confirm": { + "description": "M\u00f6chten Sie den Drucker mit dem Namen \"{name}\" zu Home Assistant hinzuf\u00fcgen?", + "title": "Entdeckter Drucker" + } + }, + "title": "Internet-Druckprotokoll (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/es.json b/homeassistant/components/ipp/.translations/es.json new file mode 100644 index 00000000000..6e86f702902 --- /dev/null +++ b/homeassistant/components/ipp/.translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "connection_error": "No se pudo conectar con la impresora.", + "connection_upgrade": "No se pudo conectar con la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n." + }, + "error": { + "connection_error": "No se pudo conectar con la impresora.", + "connection_upgrade": "No se pudo conectar con la impresora. Int\u00e9ntalo de nuevo con la opci\u00f3n SSL/TLS marcada." + }, + "flow_title": "Impresora: {name}", + "step": { + "user": { + "data": { + "base_path": "Ruta relativa a la impresora", + "host": "Host o direcci\u00f3n IP", + "port": "Puerto", + "ssl": "La impresora admite la comunicaci\u00f3n a trav\u00e9s de SSL/TLS", + "verify_ssl": "La impresora usa un certificado SSL adecuado" + }, + "description": "Configura tu impresora a trav\u00e9s del Protocolo de Impresi\u00f3n de Internet (IPP) para integrarla con Home Assistant.", + "title": "Vincula tu impresora" + }, + "zeroconf_confirm": { + "description": "\u00bfQuieres a\u00f1adir la impresora llamada `{name}` a Home Assistant?", + "title": "Impresora encontrada" + } + }, + "title": "Protocolo de Impresi\u00f3n de Internet (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/lb.json b/homeassistant/components/ipp/.translations/lb.json new file mode 100644 index 00000000000..fa8e2407696 --- /dev/null +++ b/homeassistant/components/ipp/.translations/lb.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebse Printer ass scho konfigur\u00e9iert.", + "connection_error": "Feeler beim verbannen mam Printer." + }, + "error": { + "connection_error": "Feeler beim verbannen mam Printer.", + "connection_upgrade": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol mat aktiv\u00e9ierter SSL/TLS Optioun." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relative Pad zum Printer", + "host": "Numm oder IP Adresse", + "port": "Port", + "ssl": "Printer \u00ebnnerst\u00ebtze Kommunikatioun iwwer SSL/TLS", + "verify_ssl": "Printer benotzt ee g\u00ebltegen SSL Zertifikat" + }, + "description": "Konfigur\u00e9ier d\u00e4in Printer mat Internet Printing Protocol (IPP) fir en am Home Assistant z'int\u00e9gr\u00e9ieren.", + "title": "\u00c4re Printer verbannen" + }, + "zeroconf_confirm": { + "description": "W\u00ebllt dir de Printer mam Numm `{name}` am Home Assistant dob\u00e4isetzen?", + "title": "Entdeckte Printer" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/ru.json b/homeassistant/components/ipp/.translations/ru.json new file mode 100644 index 00000000000..902289b2e8f --- /dev/null +++ b/homeassistant/components/ipp/.translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443.", + "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443 \u0438\u0437-\u0437\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f." + }, + "error": { + "connection_error": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443.", + "connection_upgrade": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443. \u041f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0447\u0435\u0440\u0435\u0437 SSL/TLS." + }, + "flow_title": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440: {name}", + "step": { + "user": { + "data": { + "base_path": "\u041e\u0442\u043d\u043e\u0441\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0439 \u043f\u0443\u0442\u044c \u043a \u043f\u0440\u0438\u043d\u0442\u0435\u0440\u0443", + "host": "\u0414\u043e\u043c\u0435\u043d\u043d\u043e\u0435 \u0438\u043c\u044f \u0438\u043b\u0438 IP-\u0430\u0434\u0440\u0435\u0441", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442 \u0441\u0432\u044f\u0437\u044c \u043f\u043e SSL/TLS", + "verify_ssl": "\u041f\u0440\u0438\u043d\u0442\u0435\u0440 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442 \u0441\u043e\u0431\u0441\u0442\u0432\u0435\u043d\u043d\u044b\u0439 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" + }, + "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 \u043f\u043e \u043f\u0440\u043e\u0442\u043e\u043a\u043e\u043b\u0443 IPP \u0434\u043b\u044f \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438 \u0441 Home Assistant.", + "title": "Internet Printing Protocol (IPP)" + }, + "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 \u043f\u0440\u0438\u043d\u0442\u0435\u0440 `{name}`?", + "title": "\u041e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u043d\u044b\u0439 \u043f\u0440\u0438\u043d\u0442\u0435\u0440" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/zh-Hant.json b/homeassistant/components/ipp/.translations/zh-Hant.json new file mode 100644 index 00000000000..fe79b4b88cd --- /dev/null +++ b/homeassistant/components/ipp/.translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u5370\u8868\u6a5f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "connection_error": "\u5370\u8868\u6a5f\u9023\u7dda\u5931\u6557\u3002", + "connection_upgrade": "\u7531\u65bc\u9700\u8981\u5148\u5347\u7d1a\u9023\u7dda\u3001\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002" + }, + "error": { + "connection_error": "\u5370\u8868\u6a5f\u9023\u7dda\u5931\u6557\u3002", + "connection_upgrade": "\u9023\u7dda\u81f3\u5370\u8868\u6a5f\u5931\u6557\u3002\u8acb\u52fe\u9078 SSL/TLS \u9078\u9805\u5f8c\u518d\u8a66\u4e00\u6b21\u3002" + }, + "flow_title": "\u5370\u8868\u6a5f\uff1a{name}", + "step": { + "user": { + "data": { + "base_path": "\u5370\u8868\u6a5f\u76f8\u5c0d\u8def\u5f91", + "host": "\u4e3b\u6a5f\u6216 IP \u4f4d\u5740", + "port": "\u901a\u8a0a\u57e0", + "ssl": "\u5370\u8868\u6a5f\u652f\u63f4 SSL/TLS \u901a\u8a0a", + "verify_ssl": "\u5370\u8868\u6a5f\u4f7f\u7528\u5c0d\u61c9\u8a8d\u8b49" + }, + "description": "\u900f\u904e\u7db2\u969b\u7db2\u8def\u5217\u5370\u5354\u5b9a\uff08IPP\uff09\u8a2d\u5b9a\u5370\u8868\u6a5f\u4ee5\u6574\u5408\u81f3 Home Assistant\u3002", + "title": "\u9023\u7d50\u5370\u8868\u6a5f" + }, + "zeroconf_confirm": { + "description": "\u662f\u5426\u8981\u65b0\u589e\u540d\u7a31 `{name}` \u5370\u8868\u6a5f\u81f3 Home Assistant\uff1f", + "title": "\u81ea\u52d5\u641c\u7d22\u5230\u7684\u5370\u8868\u6a5f" + } + }, + "title": "\u7db2\u969b\u7db2\u8def\u5217\u5370\u5354\u5b9a\uff08IPP\uff09" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json index b6591b03d33..cfd05320e35 100644 --- a/homeassistant/components/konnected/.translations/es.json +++ b/homeassistant/components/konnected/.translations/es.json @@ -95,6 +95,7 @@ "data": { "activation": "Salida cuando est\u00e1 activada", "momentary": "Duraci\u00f3n del pulso (ms) (opcional)", + "more_states": "Configurar estados adicionales para esta zona", "name": "Nombre (opcional)", "pause": "Pausa entre pulsos (ms) (opcional)", "repeat": "Tiempos de repetici\u00f3n (-1 = infinito) (opcional)" diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json index ba1b3c6abc9..75a879832a4 100644 --- a/homeassistant/components/konnected/.translations/ru.json +++ b/homeassistant/components/konnected/.translations/ru.json @@ -91,6 +91,7 @@ "data": { "activation": "\u0412\u044b\u0445\u043e\u0434 \u043f\u0440\u0438 \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438", "momentary": "\u0414\u043b\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "more_states": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f \u0434\u043b\u044f \u044d\u0442\u043e\u0439 \u0437\u043e\u043d\u044b", "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json index f3aa89fe877..851dac76195 100644 --- a/homeassistant/components/konnected/.translations/zh-Hant.json +++ b/homeassistant/components/konnected/.translations/zh-Hant.json @@ -96,7 +96,7 @@ "pause": "\u66ab\u505c\u9593\u8ddd\uff08ms\uff09\uff08\u9078\u9805\uff09", "repeat": "\u91cd\u8907\u6642\u9593\uff08-1=\u7121\u9650\uff09\uff08\u9078\u9805\uff09" }, - "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805", + "description": "\u8acb\u9078\u64c7 {zone}\u8f38\u51fa\u9078\u9805\uff1a\u72c0\u614b {state}", "title": "\u8a2d\u5b9a Switchable \u8f38\u51fa" } }, diff --git a/homeassistant/components/rachio/.translations/pl.json b/homeassistant/components/rachio/.translations/pl.json new file mode 100644 index 00000000000..b186a764cd1 --- /dev/null +++ b/homeassistant/components/rachio/.translations/pl.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "invalid_auth": "Niepoprawne uwierzytelnienie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "api_key": "Klucz API dla konta Rachio." + }, + "description": "B\u0119dziesz potrzebowa\u0142 klucza API ze strony https://app.rach.io/. Wybierz 'Account Settings', a nast\u0119pnie kliknij 'GET API KEY'.", + "title": "Po\u0142\u0105cz si\u0119 z urz\u0105dzeniem Rachio" + } + }, + "title": "Rachio" + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Jak d\u0142ugo, w minutach, nale\u017cy w\u0142\u0105czy\u0107 stacj\u0119, gdy prze\u0142\u0105cznik jest w\u0142\u0105czony." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index c5b26042799..91ed0b2dd53 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -33,7 +33,7 @@ "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji" }, - "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", + "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Konfigurowanie aplikacji dla smart TV" }, "user": { @@ -50,7 +50,7 @@ "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", "include_or_exclude": "Do\u0142\u0105czy\u0107 czy wykluczy\u0107 aplikacje?" }, - "description": "Je\u015bli masz telewizor Smart TV, mo\u017cesz opcjonalnie filtrowa\u0107 list\u0119 \u017ar\u00f3de\u0142, wybieraj\u0105c aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone na li\u015bcie \u017ar\u00f3d\u0142owej. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", + "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Skonfiguruj aplikacje dla Smart TV" } }, @@ -61,9 +61,11 @@ "init": { "data": { "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", + "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji", "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", "volume_step": "Skok g\u0142o\u015bno\u015bci" }, + "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", "title": "Aktualizacja opcji Vizo SmartCast" } }, From cc443ff37a52c5f6b1a24cb7c724ff492435f2e8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 31 Mar 2020 21:08:27 -0500 Subject: [PATCH 358/431] Add config flow for nut (#33457) * Convert nut to config flow * Add a test for importing * lint * Address review items (part 1) * Address review items (part 1) * Cleanup unique id handling * Update tests for new naming scheme * No unique id, no device_info * Remove sensor types * Update tests to use resources that still exist --- CODEOWNERS | 1 + .../components/nut/.translations/en.json | 38 +++ homeassistant/components/nut/__init__.py | 209 ++++++++++++ homeassistant/components/nut/config_flow.py | 143 +++++++++ homeassistant/components/nut/const.py | 125 ++++++++ homeassistant/components/nut/manifest.json | 7 +- homeassistant/components/nut/sensor.py | 301 +++++------------- homeassistant/components/nut/strings.json | 38 +++ homeassistant/generated/config_flows.py | 1 + requirements_test_all.txt | 3 + tests/components/nut/__init__.py | 1 + tests/components/nut/test_config_flow.py | 122 +++++++ 12 files changed, 774 insertions(+), 215 deletions(-) create mode 100644 homeassistant/components/nut/.translations/en.json create mode 100644 homeassistant/components/nut/config_flow.py create mode 100644 homeassistant/components/nut/const.py create mode 100644 homeassistant/components/nut/strings.json create mode 100644 tests/components/nut/__init__.py create mode 100644 tests/components/nut/test_config_flow.py diff --git a/CODEOWNERS b/CODEOWNERS index 4598c6f049d..4d4c7d3d900 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -261,6 +261,7 @@ homeassistant/components/nsw_fuel_station/* @nickw444 homeassistant/components/nsw_rural_fire_service_feed/* @exxamalte homeassistant/components/nuheat/* @bdraco homeassistant/components/nuki/* @pvizeli +homeassistant/components/nut/* @bdraco homeassistant/components/nws/* @MatthewFlamm homeassistant/components/nzbget/* @chriscla homeassistant/components/obihai/* @dshokouhi diff --git a/homeassistant/components/nut/.translations/en.json b/homeassistant/components/nut/.translations/en.json new file mode 100644 index 00000000000..e37a019af78 --- /dev/null +++ b/homeassistant/components/nut/.translations/en.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Network UPS Tools (NUT)", + "step": { + "user": { + "title": "Connect to the NUT server", + "description": "If there are multiple UPSs attached to the NUT server, enter the name UPS to query in the 'Alias' field.", + "data": { + "name": "Name", + "host": "Host", + "port": "Port", + "alias": "Alias", + "username": "Username", + "password": "Password", + "resources": "Resources" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "description": "Choose Sensor Resources", + "data": { + "resources": "Resources" + } + } + } + } + +} \ No newline at end of file diff --git a/homeassistant/components/nut/__init__.py b/homeassistant/components/nut/__init__.py index e51145c8eaa..a990cdf94b8 100644 --- a/homeassistant/components/nut/__init__.py +++ b/homeassistant/components/nut/__init__.py @@ -1 +1,210 @@ """The nut component.""" +import asyncio +import logging + +from pynut2.nut2 import PyNUTClient, PyNUTError + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import ( + CONF_ALIAS, + CONF_HOST, + CONF_PASSWORD, + CONF_PORT, + CONF_RESOURCES, + CONF_USERNAME, +) +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import ( + DOMAIN, + PLATFORMS, + PYNUT_DATA, + PYNUT_FIRMWARE, + PYNUT_MANUFACTURER, + PYNUT_MODEL, + PYNUT_STATUS, + PYNUT_UNIQUE_ID, +) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the Network UPS Tools (NUT) component.""" + hass.data.setdefault(DOMAIN, {}) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Network UPS Tools (NUT) from a config entry.""" + + config = entry.data + host = config[CONF_HOST] + port = config[CONF_PORT] + + alias = config.get(CONF_ALIAS) + username = config.get(CONF_USERNAME) + password = config.get(CONF_PASSWORD) + + data = PyNUTData(host, port, alias, username, password) + + status = await hass.async_add_executor_job(pynutdata_status, data) + + if not status: + _LOGGER.error("NUT Sensor has no data, unable to set up") + raise ConfigEntryNotReady + + _LOGGER.debug("NUT Sensors Available: %s", status) + + hass.data[DOMAIN][entry.entry_id] = { + PYNUT_DATA: data, + PYNUT_STATUS: status, + PYNUT_UNIQUE_ID: _unique_id_from_status(status), + PYNUT_MANUFACTURER: _manufacturer_from_status(status), + PYNUT_MODEL: _model_from_status(status), + PYNUT_FIRMWARE: _firmware_from_status(status), + } + + entry.add_update_listener(_async_update_listener) + + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + return True + + +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + +def _manufacturer_from_status(status): + """Find the best manufacturer value from the status.""" + return ( + status.get("device.mfr") + or status.get("ups.mfr") + or status.get("ups.vendorid") + or status.get("driver.version.data") + ) + + +def _model_from_status(status): + """Find the best model value from the status.""" + return ( + status.get("device.model") + or status.get("ups.model") + or status.get("ups.productid") + ) + + +def _firmware_from_status(status): + """Find the best firmware value from the status.""" + return status.get("ups.firmware") or status.get("ups.firmware.aux") + + +def _serial_from_status(status): + """Find the best serialvalue from the status.""" + serial = status.get("device.serial") or status.get("ups.serial") + if serial and serial == "unknown": + return None + return serial + + +def _unique_id_from_status(status): + """Find the best unique id value from the status.""" + serial = _serial_from_status(status) + # We must have a serial for this to be unique + if not serial: + return None + + manufacturer = _manufacturer_from_status(status) + model = _model_from_status(status) + + unique_id_group = [] + if manufacturer: + unique_id_group.append(manufacturer) + if model: + unique_id_group.append(model) + if serial: + unique_id_group.append(serial) + return "_".join(unique_id_group) + + +def find_resources_in_config_entry(config_entry): + """Find the configured resources in the config entry.""" + if CONF_RESOURCES in config_entry.options: + return config_entry.options[CONF_RESOURCES] + return config_entry.data[CONF_RESOURCES] + + +def pynutdata_status(data): + """Wrap for data update as a callable.""" + return data.status + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +class PyNUTData: + """Stores the data retrieved from NUT. + + For each entity to use, acts as the single point responsible for fetching + updates from the server. + """ + + def __init__(self, host, port, alias, username, password): + """Initialize the data object.""" + + self._host = host + self._alias = alias + + # Establish client with persistent=False to open/close connection on + # each update call. This is more reliable with async. + self._client = PyNUTClient(self._host, port, username, password, 5, False) + self._status = None + + @property + def status(self): + """Get latest update if throttle allows. Return status.""" + self.update() + return self._status + + def _get_alias(self): + """Get the ups alias from NUT.""" + try: + return next(iter(self._client.list_ups())) + except PyNUTError as err: + _LOGGER.error("Failure getting NUT ups alias, %s", err) + return None + + def _get_status(self): + """Get the ups status from NUT.""" + if self._alias is None: + self._alias = self._get_alias() + + try: + return self._client.list_vars(self._alias) + except (PyNUTError, ConnectionResetError) as err: + _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) + return None + + def update(self, **kwargs): + """Fetch the latest status from NUT.""" + self._status = self._get_status() diff --git a/homeassistant/components/nut/config_flow.py b/homeassistant/components/nut/config_flow.py new file mode 100644 index 00000000000..04889bb3f3f --- /dev/null +++ b/homeassistant/components/nut/config_flow.py @@ -0,0 +1,143 @@ +"""Config flow for Network UPS Tools (NUT) integration.""" +import logging + +import voluptuous as vol + +from homeassistant import config_entries, core, exceptions +from homeassistant.const import ( + CONF_ALIAS, + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_RESOURCES, + CONF_USERNAME, +) +from homeassistant.core import callback +import homeassistant.helpers.config_validation as cv + +from . import PyNUTData, find_resources_in_config_entry, pynutdata_status +from .const import DEFAULT_HOST, DEFAULT_NAME, DEFAULT_PORT, SENSOR_TYPES +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + + +SENSOR_DICT = {sensor_id: SENSOR_TYPES[sensor_id][0] for sensor_id in SENSOR_TYPES} + +DATA_SCHEMA = vol.Schema( + { + vol.Optional(CONF_NAME, default=DEFAULT_NAME): str, + vol.Required(CONF_RESOURCES): cv.multi_select(SENSOR_DICT), + vol.Optional(CONF_HOST, default=DEFAULT_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): int, + vol.Optional(CONF_ALIAS): str, + vol.Optional(CONF_USERNAME): str, + vol.Optional(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows us to connect. + + Data has the keys from DATA_SCHEMA with values provided by the user. + """ + + host = data[CONF_HOST] + port = data[CONF_PORT] + alias = data.get(CONF_ALIAS) + username = data.get(CONF_USERNAME) + password = data.get(CONF_PASSWORD) + + data = PyNUTData(host, port, alias, username, password) + + status = await hass.async_add_executor_job(pynutdata_status, data) + + if not status: + raise CannotConnect + + return {"title": _format_host_port_alias(host, port, alias)} + + +def _format_host_port_alias(host, port, alias): + """Format a host, port, and alias so it can be used for comparison or display.""" + if alias: + return f"{alias}@{host}:{port}" + return f"{host}:{port}" + + +class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Network UPS Tools (NUT).""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + if self._host_port_alias_already_configured( + user_input[CONF_HOST], user_input[CONF_PORT], user_input.get(CONF_ALIAS) + ): + return self.async_abort(reason="already_configured") + 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" + + if "base" not in errors: + 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 + ) + + def _host_port_alias_already_configured(self, host, port, alias): + """See if we already have a nut entry matching user input configured.""" + existing_host_port_aliases = { + _format_host_port_alias(host, port, alias) + for entry in self._async_current_entries() + } + return _format_host_port_alias(host, port, alias) in existing_host_port_aliases + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + @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 nut.""" + + def __init__(self, config_entry: config_entries.ConfigEntry): + """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) + + resources = find_resources_in_config_entry(self.config_entry) + + data_schema = vol.Schema( + { + vol.Required(CONF_RESOURCES, default=resources): cv.multi_select( + SENSOR_DICT + ), + } + ) + return self.async_show_form(step_id="init", data_schema=data_schema) + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" diff --git a/homeassistant/components/nut/const.py b/homeassistant/components/nut/const.py new file mode 100644 index 00000000000..ea164e70b93 --- /dev/null +++ b/homeassistant/components/nut/const.py @@ -0,0 +1,125 @@ +"""The nut component.""" +from homeassistant.const import POWER_WATT, TEMP_CELSIUS, TIME_SECONDS, UNIT_PERCENTAGE + +DOMAIN = "nut" + +PLATFORMS = ["sensor"] + + +DEFAULT_NAME = "NUT UPS" +DEFAULT_HOST = "localhost" +DEFAULT_PORT = 3493 + +KEY_STATUS = "ups.status" +KEY_STATUS_DISPLAY = "ups.status.display" + +PYNUT_DATA = "data" +PYNUT_STATUS = "status" +PYNUT_UNIQUE_ID = "unique_id" +PYNUT_MANUFACTURER = "manufacturer" +PYNUT_MODEL = "model" +PYNUT_FIRMWARE = "firmware" + +SENSOR_TYPES = { + "ups.status.display": ["Status", "", "mdi:information-outline"], + "ups.status": ["Status Data", "", "mdi:information-outline"], + "ups.alarm": ["Alarms", "", "mdi:alarm"], + "ups.temperature": ["UPS Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge"], + "ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge"], + "ups.id": ["System identifier", "", "mdi:information-outline"], + "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer"], + "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer"], + "ups.delay.shutdown": ["UPS Shutdown Delay", TIME_SECONDS, "mdi:timer"], + "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer"], + "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer"], + "ups.timer.shutdown": ["Load Shutdown Timer", TIME_SECONDS, "mdi:timer"], + "ups.test.interval": ["Self-Test Interval", TIME_SECONDS, "mdi:timer"], + "ups.test.result": ["Self-Test Result", "", "mdi:information-outline"], + "ups.test.date": ["Self-Test Date", "", "mdi:calendar"], + "ups.display.language": ["Language", "", "mdi:information-outline"], + "ups.contacts": ["External Contacts", "", "mdi:information-outline"], + "ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge"], + "ups.power": ["Current Apparent Power", "VA", "mdi:flash"], + "ups.power.nominal": ["Nominal Power", "VA", "mdi:flash"], + "ups.realpower": ["Current Real Power", POWER_WATT, "mdi:flash"], + "ups.realpower.nominal": ["Nominal Real Power", POWER_WATT, "mdi:flash"], + "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline"], + "ups.type": ["UPS Type", "", "mdi:information-outline"], + "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline"], + "ups.start.auto": ["Start on AC", "", "mdi:information-outline"], + "ups.start.battery": ["Start on Battery", "", "mdi:information-outline"], + "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline"], + "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline"], + "battery.charge": ["Battery Charge", UNIT_PERCENTAGE, "mdi:gauge"], + "battery.charge.low": ["Low Battery Setpoint", UNIT_PERCENTAGE, "mdi:gauge"], + "battery.charge.restart": [ + "Minimum Battery to Start", + UNIT_PERCENTAGE, + "mdi:gauge", + ], + "battery.charge.warning": [ + "Warning Battery Setpoint", + UNIT_PERCENTAGE, + "mdi:gauge", + ], + "battery.charger.status": ["Charging Status", "", "mdi:information-outline"], + "battery.voltage": ["Battery Voltage", "V", "mdi:flash"], + "battery.voltage.nominal": ["Nominal Battery Voltage", "V", "mdi:flash"], + "battery.voltage.low": ["Low Battery Voltage", "V", "mdi:flash"], + "battery.voltage.high": ["High Battery Voltage", "V", "mdi:flash"], + "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash"], + "battery.current": ["Battery Current", "A", "mdi:flash"], + "battery.current.total": ["Total Battery Current", "A", "mdi:flash"], + "battery.temperature": ["Battery Temperature", TEMP_CELSIUS, "mdi:thermometer"], + "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer"], + "battery.runtime.low": ["Low Battery Runtime", TIME_SECONDS, "mdi:timer"], + "battery.runtime.restart": [ + "Minimum Battery Runtime to Start", + TIME_SECONDS, + "mdi:timer", + ], + "battery.alarm.threshold": [ + "Battery Alarm Threshold", + "", + "mdi:information-outline", + ], + "battery.date": ["Battery Date", "", "mdi:calendar"], + "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar"], + "battery.packs": ["Number of Batteries", "", "mdi:information-outline"], + "battery.packs.bad": ["Number of Bad Batteries", "", "mdi:information-outline"], + "battery.type": ["Battery Chemistry", "", "mdi:information-outline"], + "input.sensitivity": ["Input Power Sensitivity", "", "mdi:information-outline"], + "input.transfer.low": ["Low Voltage Transfer", "V", "mdi:flash"], + "input.transfer.high": ["High Voltage Transfer", "V", "mdi:flash"], + "input.transfer.reason": ["Voltage Transfer Reason", "", "mdi:information-outline"], + "input.voltage": ["Input Voltage", "V", "mdi:flash"], + "input.voltage.nominal": ["Nominal Input Voltage", "V", "mdi:flash"], + "input.frequency": ["Input Line Frequency", "hz", "mdi:flash"], + "input.frequency.nominal": ["Nominal Input Line Frequency", "hz", "mdi:flash"], + "input.frequency.status": ["Input Frequency Status", "", "mdi:information-outline"], + "output.current": ["Output Current", "A", "mdi:flash"], + "output.current.nominal": ["Nominal Output Current", "A", "mdi:flash"], + "output.voltage": ["Output Voltage", "V", "mdi:flash"], + "output.voltage.nominal": ["Nominal Output Voltage", "V", "mdi:flash"], + "output.frequency": ["Output Frequency", "hz", "mdi:flash"], + "output.frequency.nominal": ["Nominal Output Frequency", "hz", "mdi:flash"], +} + +STATE_TYPES = { + "OL": "Online", + "OB": "On Battery", + "LB": "Low Battery", + "HB": "High Battery", + "RB": "Battery Needs Replaced", + "CHRG": "Battery Charging", + "DISCHRG": "Battery Discharging", + "BYPASS": "Bypass Active", + "CAL": "Runtime Calibration", + "OFF": "Offline", + "OVER": "Overloaded", + "TRIM": "Trimming Voltage", + "BOOST": "Boosting Voltage", + "FSD": "Forced Shutdown", + "ALARM": "Alarm", +} diff --git a/homeassistant/components/nut/manifest.json b/homeassistant/components/nut/manifest.json index a44e70f9aa9..26accb5edb8 100644 --- a/homeassistant/components/nut/manifest.json +++ b/homeassistant/components/nut/manifest.json @@ -2,7 +2,10 @@ "domain": "nut", "name": "Network UPS Tools (NUT)", "documentation": "https://www.home-assistant.io/integrations/nut", - "requirements": ["pynut2==2.1.2"], + "requirements": [ + "pynut2==2.1.2" + ], "dependencies": [], - "codeowners": [] + "codeowners": ["@bdraco"], + "config_flow": true } diff --git a/homeassistant/components/nut/sensor.py b/homeassistant/components/nut/sensor.py index 1b602954414..a611c8d4268 100644 --- a/homeassistant/components/nut/sensor.py +++ b/homeassistant/components/nut/sensor.py @@ -2,10 +2,10 @@ from datetime import timedelta import logging -from pynut2.nut2 import PyNUTClient, PyNUTError import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_STATE, CONF_ALIAS, @@ -15,140 +15,33 @@ from homeassistant.const import ( CONF_PORT, CONF_RESOURCES, CONF_USERNAME, - POWER_WATT, STATE_UNKNOWN, - TEMP_CELSIUS, - TIME_SECONDS, - UNIT_PERCENTAGE, ) -from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import Entity +from .const import ( + DEFAULT_HOST, + DEFAULT_NAME, + DEFAULT_PORT, + DOMAIN, + KEY_STATUS, + KEY_STATUS_DISPLAY, + PYNUT_DATA, + PYNUT_FIRMWARE, + PYNUT_MANUFACTURER, + PYNUT_MODEL, + PYNUT_STATUS, + PYNUT_UNIQUE_ID, + SENSOR_TYPES, + STATE_TYPES, +) + _LOGGER = logging.getLogger(__name__) -DEFAULT_NAME = "NUT UPS" -DEFAULT_HOST = "localhost" -DEFAULT_PORT = 3493 - -KEY_STATUS = "ups.status" -KEY_STATUS_DISPLAY = "ups.status.display" SCAN_INTERVAL = timedelta(seconds=60) -SENSOR_TYPES = { - "ups.status.display": ["Status", "", "mdi:information-outline"], - "ups.status": ["Status Data", "", "mdi:information-outline"], - "ups.alarm": ["Alarms", "", "mdi:alarm"], - "ups.time": ["Internal Time", "", "mdi:calendar-clock"], - "ups.date": ["Internal Date", "", "mdi:calendar"], - "ups.model": ["Model", "", "mdi:information-outline"], - "ups.mfr": ["Manufacturer", "", "mdi:information-outline"], - "ups.mfr.date": ["Manufacture Date", "", "mdi:calendar"], - "ups.serial": ["Serial Number", "", "mdi:information-outline"], - "ups.vendorid": ["Vendor ID", "", "mdi:information-outline"], - "ups.productid": ["Product ID", "", "mdi:information-outline"], - "ups.firmware": ["Firmware Version", "", "mdi:information-outline"], - "ups.firmware.aux": ["Firmware Version 2", "", "mdi:information-outline"], - "ups.temperature": ["UPS Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "ups.load": ["Load", UNIT_PERCENTAGE, "mdi:gauge"], - "ups.load.high": ["Overload Setting", UNIT_PERCENTAGE, "mdi:gauge"], - "ups.id": ["System identifier", "", "mdi:information-outline"], - "ups.delay.start": ["Load Restart Delay", TIME_SECONDS, "mdi:timer"], - "ups.delay.reboot": ["UPS Reboot Delay", TIME_SECONDS, "mdi:timer"], - "ups.delay.shutdown": ["UPS Shutdown Delay", TIME_SECONDS, "mdi:timer"], - "ups.timer.start": ["Load Start Timer", TIME_SECONDS, "mdi:timer"], - "ups.timer.reboot": ["Load Reboot Timer", TIME_SECONDS, "mdi:timer"], - "ups.timer.shutdown": ["Load Shutdown Timer", TIME_SECONDS, "mdi:timer"], - "ups.test.interval": ["Self-Test Interval", TIME_SECONDS, "mdi:timer"], - "ups.test.result": ["Self-Test Result", "", "mdi:information-outline"], - "ups.test.date": ["Self-Test Date", "", "mdi:calendar"], - "ups.display.language": ["Language", "", "mdi:information-outline"], - "ups.contacts": ["External Contacts", "", "mdi:information-outline"], - "ups.efficiency": ["Efficiency", UNIT_PERCENTAGE, "mdi:gauge"], - "ups.power": ["Current Apparent Power", "VA", "mdi:flash"], - "ups.power.nominal": ["Nominal Power", "VA", "mdi:flash"], - "ups.realpower": ["Current Real Power", POWER_WATT, "mdi:flash"], - "ups.realpower.nominal": ["Nominal Real Power", POWER_WATT, "mdi:flash"], - "ups.beeper.status": ["Beeper Status", "", "mdi:information-outline"], - "ups.type": ["UPS Type", "", "mdi:information-outline"], - "ups.watchdog.status": ["Watchdog Status", "", "mdi:information-outline"], - "ups.start.auto": ["Start on AC", "", "mdi:information-outline"], - "ups.start.battery": ["Start on Battery", "", "mdi:information-outline"], - "ups.start.reboot": ["Reboot on Battery", "", "mdi:information-outline"], - "ups.shutdown": ["Shutdown Ability", "", "mdi:information-outline"], - "battery.charge": ["Battery Charge", UNIT_PERCENTAGE, "mdi:gauge"], - "battery.charge.low": ["Low Battery Setpoint", UNIT_PERCENTAGE, "mdi:gauge"], - "battery.charge.restart": [ - "Minimum Battery to Start", - UNIT_PERCENTAGE, - "mdi:gauge", - ], - "battery.charge.warning": [ - "Warning Battery Setpoint", - UNIT_PERCENTAGE, - "mdi:gauge", - ], - "battery.charger.status": ["Charging Status", "", "mdi:information-outline"], - "battery.voltage": ["Battery Voltage", "V", "mdi:flash"], - "battery.voltage.nominal": ["Nominal Battery Voltage", "V", "mdi:flash"], - "battery.voltage.low": ["Low Battery Voltage", "V", "mdi:flash"], - "battery.voltage.high": ["High Battery Voltage", "V", "mdi:flash"], - "battery.capacity": ["Battery Capacity", "Ah", "mdi:flash"], - "battery.current": ["Battery Current", "A", "mdi:flash"], - "battery.current.total": ["Total Battery Current", "A", "mdi:flash"], - "battery.temperature": ["Battery Temperature", TEMP_CELSIUS, "mdi:thermometer"], - "battery.runtime": ["Battery Runtime", TIME_SECONDS, "mdi:timer"], - "battery.runtime.low": ["Low Battery Runtime", TIME_SECONDS, "mdi:timer"], - "battery.runtime.restart": [ - "Minimum Battery Runtime to Start", - TIME_SECONDS, - "mdi:timer", - ], - "battery.alarm.threshold": [ - "Battery Alarm Threshold", - "", - "mdi:information-outline", - ], - "battery.date": ["Battery Date", "", "mdi:calendar"], - "battery.mfr.date": ["Battery Manuf. Date", "", "mdi:calendar"], - "battery.packs": ["Number of Batteries", "", "mdi:information-outline"], - "battery.packs.bad": ["Number of Bad Batteries", "", "mdi:information-outline"], - "battery.type": ["Battery Chemistry", "", "mdi:information-outline"], - "input.sensitivity": ["Input Power Sensitivity", "", "mdi:information-outline"], - "input.transfer.low": ["Low Voltage Transfer", "V", "mdi:flash"], - "input.transfer.high": ["High Voltage Transfer", "V", "mdi:flash"], - "input.transfer.reason": ["Voltage Transfer Reason", "", "mdi:information-outline"], - "input.voltage": ["Input Voltage", "V", "mdi:flash"], - "input.voltage.nominal": ["Nominal Input Voltage", "V", "mdi:flash"], - "input.frequency": ["Input Line Frequency", "hz", "mdi:flash"], - "input.frequency.nominal": ["Nominal Input Line Frequency", "hz", "mdi:flash"], - "input.frequency.status": ["Input Frequency Status", "", "mdi:information-outline"], - "output.current": ["Output Current", "A", "mdi:flash"], - "output.current.nominal": ["Nominal Output Current", "A", "mdi:flash"], - "output.voltage": ["Output Voltage", "V", "mdi:flash"], - "output.voltage.nominal": ["Nominal Output Voltage", "V", "mdi:flash"], - "output.frequency": ["Output Frequency", "hz", "mdi:flash"], - "output.frequency.nominal": ["Nominal Output Frequency", "hz", "mdi:flash"], -} - -STATE_TYPES = { - "OL": "Online", - "OB": "On Battery", - "LB": "Low Battery", - "HB": "High Battery", - "RB": "Battery Needs Replaced", - "CHRG": "Battery Charging", - "DISCHRG": "Battery Discharging", - "BYPASS": "Bypass Active", - "CAL": "Runtime Calibration", - "OFF": "Offline", - "OVER": "Overloaded", - "TRIM": "Trimming Voltage", - "BOOST": "Boosting Voltage", - "FSD": "Forced Shutdown", - "ALARM": "Alarm", -} PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { @@ -164,34 +57,48 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( def setup_platform(hass, config, add_entities, discovery_info=None): + """Import the platform into a config entry.""" + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_IMPORT}, data=config + ) + ) + + +async def async_setup_entry(hass, config_entry, async_add_entities): """Set up the NUT sensors.""" - name = config[CONF_NAME] - host = config[CONF_HOST] - port = config[CONF_PORT] - alias = config.get(CONF_ALIAS) - username = config.get(CONF_USERNAME) - password = config.get(CONF_PASSWORD) - - data = PyNUTData(host, port, alias, username, password) - - if data.status is None: - _LOGGER.error("NUT Sensor has no data, unable to set up") - raise PlatformNotReady - - _LOGGER.debug("NUT Sensors Available: %s", data.status) + config = config_entry.data + pynut_data = hass.data[DOMAIN][config_entry.entry_id] + data = pynut_data[PYNUT_DATA] + status = pynut_data[PYNUT_STATUS] + unique_id = pynut_data[PYNUT_UNIQUE_ID] + manufacturer = pynut_data[PYNUT_MANUFACTURER] + model = pynut_data[PYNUT_MODEL] + firmware = pynut_data[PYNUT_FIRMWARE] entities = [] - for resource in config[CONF_RESOURCES]: + name = config[CONF_NAME] + if CONF_RESOURCES in config_entry.options: + resources = config_entry.options[CONF_RESOURCES] + else: + resources = config_entry.data[CONF_RESOURCES] + + for resource in resources: sensor_type = resource.lower() # Display status is a special case that falls back to the status value # of the UPS instead. - if sensor_type in data.status or ( - sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in data.status + if sensor_type in status or ( + sensor_type == KEY_STATUS_DISPLAY and KEY_STATUS in status ): - entities.append(NUTSensor(name, data, sensor_type)) + entities.append( + NUTSensor( + name, data, sensor_type, unique_id, manufacturer, model, firmware + ) + ) else: _LOGGER.warning( "Sensor type: %s does not appear in the NUT status " @@ -199,30 +106,53 @@ def setup_platform(hass, config, add_entities, discovery_info=None): sensor_type, ) - try: - data.update(no_throttle=True) - except data.pynuterror as err: - _LOGGER.error( - "Failure while testing NUT status retrieval. Cannot continue setup: %s", err - ) - raise PlatformNotReady - - add_entities(entities, True) + async_add_entities(entities, True) class NUTSensor(Entity): """Representation of a sensor entity for NUT status values.""" - def __init__(self, name, data, sensor_type): + def __init__( + self, name, data, sensor_type, unique_id, manufacturer, model, firmware + ): """Initialize the sensor.""" self._data = data - self.type = sensor_type + self._type = sensor_type + self._manufacturer = manufacturer + self._firmware = firmware + self._model = model + self._device_name = name self._name = "{} {}".format(name, SENSOR_TYPES[sensor_type][0]) self._unit = SENSOR_TYPES[sensor_type][1] self._state = None + self._unique_id = unique_id self._display_state = None self._available = False + @property + def device_info(self): + """Device info for the ups.""" + if not self._unique_id: + return None + device_info = { + "identifiers": {(DOMAIN, self._unique_id)}, + "name": self._device_name, + } + if self._model: + device_info["model"] = self._model + if self._manufacturer: + device_info["manufacturer"] = self._manufacturer + if self._firmware: + device_info["sw_version"] = self._firmware + return device_info + + @property + def unique_id(self): + """Sensor Unique id.""" + if not self._unique_id: + return None + return f"{self._unique_id}_{self._type}" + @property def name(self): """Return the name of the UPS sensor.""" @@ -231,7 +161,7 @@ class NUTSensor(Entity): @property def icon(self): """Icon to use in the frontend, if any.""" - return SENSOR_TYPES[self.type][2] + return SENSOR_TYPES[self._type][2] @property def state(self): @@ -265,12 +195,12 @@ class NUTSensor(Entity): self._display_state = _format_display_state(status) # In case of the display status sensor, keep a human-readable form # as the sensor state. - if self.type == KEY_STATUS_DISPLAY: + if self._type == KEY_STATUS_DISPLAY: self._state = self._display_state - elif self.type not in status: + elif self._type not in status: self._state = None else: - self._state = status[self.type] + self._state = status[self._type] def _format_display_state(status): @@ -281,58 +211,3 @@ def _format_display_state(status): return " ".join(STATE_TYPES[state] for state in status[KEY_STATUS].split()) except KeyError: return STATE_UNKNOWN - - -class PyNUTData: - """Stores the data retrieved from NUT. - - For each entity to use, acts as the single point responsible for fetching - updates from the server. - """ - - def __init__(self, host, port, alias, username, password): - """Initialize the data object.""" - - self._host = host - self._port = port - self._alias = alias - self._username = username - self._password = password - - self.pynuterror = PyNUTError - # Establish client with persistent=False to open/close connection on - # each update call. This is more reliable with async. - self._client = PyNUTClient( - self._host, self._port, self._username, self._password, 5, False - ) - - self._status = None - - @property - def status(self): - """Get latest update if throttle allows. Return status.""" - self.update() - return self._status - - def _get_alias(self): - """Get the ups alias from NUT.""" - try: - return next(iter(self._client.list_ups())) - except self.pynuterror as err: - _LOGGER.error("Failure getting NUT ups alias, %s", err) - return None - - def _get_status(self): - """Get the ups status from NUT.""" - if self._alias is None: - self._alias = self._get_alias() - - try: - return self._client.list_vars(self._alias) - except (self.pynuterror, ConnectionResetError) as err: - _LOGGER.debug("Error getting NUT vars for host %s: %s", self._host, err) - return None - - def update(self, **kwargs): - """Fetch the latest status from NUT.""" - self._status = self._get_status() diff --git a/homeassistant/components/nut/strings.json b/homeassistant/components/nut/strings.json new file mode 100644 index 00000000000..e37a019af78 --- /dev/null +++ b/homeassistant/components/nut/strings.json @@ -0,0 +1,38 @@ +{ + "config": { + "title": "Network UPS Tools (NUT)", + "step": { + "user": { + "title": "Connect to the NUT server", + "description": "If there are multiple UPSs attached to the NUT server, enter the name UPS to query in the 'Alias' field.", + "data": { + "name": "Name", + "host": "Host", + "port": "Port", + "alias": "Alias", + "username": "Username", + "password": "Password", + "resources": "Resources" + } + } + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "abort": { + "already_configured": "Device is already configured" + } + }, + "options": { + "step": { + "init": { + "description": "Choose Sensor Resources", + "data": { + "resources": "Resources" + } + } + } + } + +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 2b96c63f4d7..dd0342a06a3 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -81,6 +81,7 @@ FLOWS = [ "nexia", "notion", "nuheat", + "nut", "opentherm_gw", "openuv", "owntracks", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 84b177eb809..ceb5a9c419a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -554,6 +554,9 @@ pymonoprice==0.3 # homeassistant.components.myq pymyq==2.0.1 +# homeassistant.components.nut +pynut2==2.1.2 + # homeassistant.components.nws pynws==0.10.4 diff --git a/tests/components/nut/__init__.py b/tests/components/nut/__init__.py new file mode 100644 index 00000000000..61ddfb4c07a --- /dev/null +++ b/tests/components/nut/__init__.py @@ -0,0 +1 @@ +"""Tests for the Network UPS Tools (NUT) integration.""" diff --git a/tests/components/nut/test_config_flow.py b/tests/components/nut/test_config_flow.py new file mode 100644 index 00000000000..362f6c0b2ba --- /dev/null +++ b/tests/components/nut/test_config_flow.py @@ -0,0 +1,122 @@ +"""Test the Network UPS Tools (NUT) config flow.""" +from asynctest import MagicMock, patch + +from homeassistant import config_entries, setup +from homeassistant.components.nut.const import DOMAIN + + +def _get_mock_pynutclient(list_vars=None): + pynutclient = MagicMock() + type(pynutclient).list_ups = MagicMock(return_value=["ups1"]) + type(pynutclient).list_vars = MagicMock(return_value=list_vars) + return pynutclient + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_pynut = _get_mock_pynutclient(list_vars={"battery.voltage": "voltage"}) + + with patch( + "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + ), patch( + "homeassistant.components.nut.async_setup", return_value=True + ) as mock_setup, 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"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "port": 2222, + "alias": "ups1", + "resources": ["battery.charge"], + }, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "ups1@1.1.1.1:2222" + assert result2["data"] == { + "alias": "ups1", + "host": "1.1.1.1", + "name": "NUT UPS", + "password": "test-password", + "port": 2222, + "resources": ["battery.charge"], + "username": "test-username", + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_import(hass): + """Test we get the form with import source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_pynut = _get_mock_pynutclient(list_vars={"battery.voltage": "serial"}) + + with patch( + "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + ), patch( + "homeassistant.components.nut.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.nut.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={ + "host": "localhost", + "port": 123, + "name": "name", + "resources": ["battery.charge"], + }, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "localhost:123" + assert result["data"] == { + "host": "localhost", + "port": 123, + "name": "name", + "resources": ["battery.charge"], + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_pynut = _get_mock_pynutclient() + + with patch( + "homeassistant.components.nut.PyNUTClient", return_value=mock_pynut, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + "username": "test-username", + "password": "test-password", + "port": 2222, + "alias": "ups1", + "resources": ["battery.charge"], + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} From 0e6aacb440fae447054a38d05fff44c88a813931 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Tue, 31 Mar 2020 23:33:05 -0400 Subject: [PATCH 359/431] Bump up zha dependencies. (#33488) --- homeassistant/components/zha/manifest.json | 10 +++++----- requirements_all.txt | 10 +++++----- requirements_test_all.txt | 10 +++++----- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index ea1bc1bbb2f..09dcf71d027 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -4,12 +4,12 @@ "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ - "bellows-homeassistant==0.14.0", - "zha-quirks==0.0.37", + "bellows-homeassistant==0.15.1", + "zha-quirks==0.0.38", "zigpy-cc==0.3.1", - "zigpy-deconz==0.7.0", - "zigpy-homeassistant==0.16.0", - "zigpy-xbee-homeassistant==0.10.0", + "zigpy-deconz==0.8.0", + "zigpy-homeassistant==0.18.0", + "zigpy-xbee-homeassistant==0.11.0", "zigpy-zigate==0.5.1" ], "dependencies": [], diff --git a/requirements_all.txt b/requirements_all.txt index 9a420d98d2d..2474aed333d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -317,7 +317,7 @@ beautifulsoup4==4.8.2 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.14.0 +bellows-homeassistant==0.15.1 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.1 @@ -2176,7 +2176,7 @@ zengge==0.2 zeroconf==0.24.5 # homeassistant.components.zha -zha-quirks==0.0.37 +zha-quirks==0.0.38 # homeassistant.components.zhong_hong zhong_hong_hvac==1.0.9 @@ -2188,13 +2188,13 @@ ziggo-mediabox-xl==1.1.0 zigpy-cc==0.3.1 # homeassistant.components.zha -zigpy-deconz==0.7.0 +zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.16.0 +zigpy-homeassistant==0.18.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.10.0 +zigpy-xbee-homeassistant==0.11.0 # homeassistant.components.zha zigpy-zigate==0.5.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index ceb5a9c419a..898b274cec0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.14.0 +bellows-homeassistant==0.15.1 # homeassistant.components.bom bomradarloop==0.1.4 @@ -798,19 +798,19 @@ yahooweather==0.10 zeroconf==0.24.5 # homeassistant.components.zha -zha-quirks==0.0.37 +zha-quirks==0.0.38 # homeassistant.components.zha zigpy-cc==0.3.1 # homeassistant.components.zha -zigpy-deconz==0.7.0 +zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.16.0 +zigpy-homeassistant==0.18.0 # homeassistant.components.zha -zigpy-xbee-homeassistant==0.10.0 +zigpy-xbee-homeassistant==0.11.0 # homeassistant.components.zha zigpy-zigate==0.5.1 From 3d73f166bed0996965be3764ee0f4a9f9de3be92 Mon Sep 17 00:00:00 2001 From: "David F. Mulcahey" Date: Wed, 1 Apr 2020 08:41:16 -0400 Subject: [PATCH 360/431] Correct issue on new device joins in ZHA (#33470) --- homeassistant/components/zha/core/gateway.py | 5 ++++- homeassistant/components/zha/core/store.py | 4 ++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index 21f2f636128..e032de4d94c 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -5,6 +5,7 @@ import collections import itertools import logging import os +import time import traceback from typing import List, Optional @@ -478,7 +479,9 @@ class ZHAGateway: async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): """Handle device joined and basic information discovered (async).""" zha_device = self._async_get_or_create_device(device) - + # This is an active device so set a last seen if it is none + if zha_device.last_seen is None: + zha_device.async_update_last_seen(time.time()) _LOGGER.debug( "device - %s:%s entering async_device_initialized - is_new_join: %s", device.nwk, diff --git a/homeassistant/components/zha/core/store.py b/homeassistant/components/zha/core/store.py index 3838e9b6a50..0171ded67fe 100644 --- a/homeassistant/components/zha/core/store.py +++ b/homeassistant/components/zha/core/store.py @@ -46,8 +46,8 @@ class ZhaStorage: name=device.name, ieee=str(device.ieee), last_seen=device.last_seen ) self.devices[device_entry.ieee] = device_entry - - return self.async_update_device(device) + self.async_schedule_save() + return device_entry @callback def async_get_or_create_device(self, device: ZhaDeviceType) -> ZhaDeviceEntry: From 4dbbf93af9c6cee049f46939aa9616d7cd8d9dd4 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Wed, 1 Apr 2020 17:09:13 +0300 Subject: [PATCH 361/431] Replace asyncio.wait with asyncio.gather since wait ignores exceptions (#33380) * replace asyncio.wait with asyncio.gather since wait ignores exceptions fix for test_entity_platform so it expects the exception * changed to log on failed domains * discovered that this fix actually removes other uncaught exceptions * fix in the list of ignored exceptions * replaced a few ignores on dyson tests that work locally but fail in the CI * two more tests that are failing on the CI and not locally * removed assertion on multiple entries with same unique_id - replaced with log and return reverted test_entity_platform to its original since now there is no exception thrown * entered all the dyson tests. they all pass locally and probabilistically fail in the CI * removed unnecessary str() for exception * added log message for duplicate entity_id / unique_id * removed log in case of False return value * added exc_info * change the use of exc_info --- homeassistant/bootstrap.py | 37 ++++++++++++++--------- homeassistant/helpers/entity_component.py | 4 +-- homeassistant/helpers/entity_platform.py | 13 ++++---- tests/helpers/test_entity_platform.py | 2 +- tests/ignore_uncaught_exceptions.py | 30 ------------------ 5 files changed, 32 insertions(+), 54 deletions(-) diff --git a/homeassistant/bootstrap.py b/homeassistant/bootstrap.py index 000c23e1d96..5d939d4b34e 100644 --- a/homeassistant/bootstrap.py +++ b/homeassistant/bootstrap.py @@ -325,15 +325,30 @@ async def _async_set_up_integrations( hass: core.HomeAssistant, config: Dict[str, Any] ) -> None: """Set up all the integrations.""" + + async def async_setup_multi_components(domains: Set[str]) -> None: + """Set up multiple domains. Log on failure.""" + futures = { + domain: hass.async_create_task(async_setup_component(hass, domain, config)) + for domain in domains + } + await asyncio.wait(futures.values()) + errors = [domain for domain in domains if futures[domain].exception()] + for domain in errors: + exception = futures[domain].exception() + _LOGGER.error( + "Error setting up integration %s - received exception", + domain, + exc_info=(type(exception), exception, exception.__traceback__), + ) + domains = _get_domains(hass, config) # Start up debuggers. Start these first in case they want to wait. debuggers = domains & DEBUGGER_INTEGRATIONS if debuggers: _LOGGER.debug("Starting up debuggers %s", debuggers) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in debuggers) - ) + await async_setup_multi_components(debuggers) domains -= DEBUGGER_INTEGRATIONS # Resolve all dependencies of all components so we can find the logging @@ -358,9 +373,7 @@ async def _async_set_up_integrations( if logging_domains: _LOGGER.info("Setting up %s", logging_domains) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in logging_domains) - ) + await async_setup_multi_components(logging_domains) # Kick off loading the registries. They don't need to be awaited. asyncio.gather( @@ -370,9 +383,7 @@ async def _async_set_up_integrations( ) if stage_1_domains: - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in stage_1_domains) - ) + await async_setup_multi_components(stage_1_domains) # Load all integrations after_dependencies: Dict[str, Set[str]] = {} @@ -401,9 +412,7 @@ async def _async_set_up_integrations( _LOGGER.debug("Setting up %s", domains_to_load) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in domains_to_load) - ) + await async_setup_multi_components(domains_to_load) last_load = domains_to_load stage_2_domains -= domains_to_load @@ -413,9 +422,7 @@ async def _async_set_up_integrations( if stage_2_domains: _LOGGER.debug("Final set up: %s", stage_2_domains) - await asyncio.gather( - *(async_setup_component(hass, domain, config) for domain in stage_2_domains) - ) + await async_setup_multi_components(stage_2_domains) # Wrap up startup await hass.async_block_till_done() diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index a761273fd25..76c2cb9889e 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -128,7 +128,7 @@ class EntityComponent: tasks.append(self.async_setup_platform(p_type, p_config)) if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) # Generic discovery listener for loading platform dynamically # Refer to: homeassistant.components.discovery.load_platform() @@ -263,7 +263,7 @@ class EntityComponent: tasks.append(platform.async_destroy()) if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) self._platforms = {self.domain: self._platforms[self.domain]} self.config = None diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 0aebaff14de..4cbb7a23496 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -183,7 +183,7 @@ class EntityPlatform: self._tasks.clear() if pending: - await asyncio.wait(pending) + await asyncio.gather(*pending) hass.config.components.add(full_name) return True @@ -292,7 +292,7 @@ class EntityPlatform: if not tasks: return - await asyncio.wait(tasks) + await asyncio.gather(*tasks) if self._async_unsub_polling is not None or not any( entity.should_poll for entity in self.entities.values() @@ -431,10 +431,11 @@ class EntityPlatform: already_exists = True if already_exists: - msg = f"Entity id already exists: {entity.entity_id}" + msg = f"Entity id already exists - ignoring: {entity.entity_id}" if entity.unique_id is not None: msg += f". Platform {self.platform_name} does not generate unique IDs" - raise HomeAssistantError(msg) + self.logger.error(msg) + return entity_id = entity.entity_id self.entities[entity_id] = entity @@ -459,7 +460,7 @@ class EntityPlatform: tasks = [self.async_remove_entity(entity_id) for entity_id in self.entities] - await asyncio.wait(tasks) + await asyncio.gather(*tasks) if self._async_unsub_polling is not None: self._async_unsub_polling() @@ -548,7 +549,7 @@ class EntityPlatform: tasks.append(entity.async_update_ha_state(True)) # type: ignore if tasks: - await asyncio.wait(tasks) + await asyncio.gather(*tasks) current_platform: ContextVar[Optional[EntityPlatform]] = ContextVar( diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 199284c680b..df247d82d5c 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -168,7 +168,7 @@ async def test_adding_entities_with_generator_and_thread_callback(hass): def create_entity(number): """Create entity helper.""" - entity = MockEntity() + entity = MockEntity(unique_id=f"unique{number}") entity.entity_id = async_generate_entity_id(DOMAIN + ".{}", "Number", hass=hass) return entity diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 26170ac2b86..428de1a683c 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -4,7 +4,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.cast.test_media_player", "test_entry_setup_single_config"), ("tests.components.cast.test_media_player", "test_entry_setup_list_config"), ("tests.components.cast.test_media_player", "test_entry_setup_platform_not_ready"), - ("tests.components.config.test_automation", "test_delete_automation"), ("tests.components.config.test_group", "test_update_device_config"), ("tests.components.default_config.test_init", "test_setup"), ("tests.components.demo.test_init", "test_setting_up_demo"), @@ -46,20 +45,9 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.dyson.test_fan", "test_purecool_update_state_filter_inv"), ("tests.components.dyson.test_fan", "test_purecool_component_setup_only_once"), ("tests.components.dyson.test_sensor", "test_purecool_component_setup_only_once"), - ("test_homeassistant_bridge", "test_homeassistant_bridge_fan_setup"), ("tests.components.ios.test_init", "test_creating_entry_sets_up_sensor"), ("tests.components.ios.test_init", "test_not_configuring_ios_not_creates_entry"), ("tests.components.local_file.test_camera", "test_file_not_readable"), - ("tests.components.meteo_france.test_config_flow", "test_user"), - ("tests.components.meteo_france.test_config_flow", "test_import"), - ("tests.components.mikrotik.test_device_tracker", "test_restoring_devices"), - ("tests.components.mikrotik.test_hub", "test_arp_ping"), - ("tests.components.mqtt.test_alarm_control_panel", "test_unique_id"), - ("tests.components.mqtt.test_binary_sensor", "test_unique_id"), - ("tests.components.mqtt.test_camera", "test_unique_id"), - ("tests.components.mqtt.test_climate", "test_unique_id"), - ("tests.components.mqtt.test_cover", "test_unique_id"), - ("tests.components.mqtt.test_fan", "test_unique_id"), ( "tests.components.mqtt.test_init", "test_setup_uses_certificate_on_certificate_set_to_auto", @@ -80,22 +68,14 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ "tests.components.mqtt.test_init", "test_setup_with_tls_config_of_v1_under_python36_only_uses_v1", ), - ("tests.components.mqtt.test_legacy_vacuum", "test_unique_id"), - ("tests.components.mqtt.test_light", "test_unique_id"), ("tests.components.mqtt.test_light", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_light_json", "test_unique_id"), ("tests.components.mqtt.test_light_json", "test_entity_device_info_remove"), ("tests.components.mqtt.test_light_template", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_lock", "test_unique_id"), - ("tests.components.mqtt.test_sensor", "test_unique_id"), - ("tests.components.mqtt.test_state_vacuum", "test_unique_id"), - ("tests.components.mqtt.test_switch", "test_unique_id"), ("tests.components.mqtt.test_switch", "test_entity_device_info_remove"), ("tests.components.qwikswitch.test_init", "test_binary_sensor_device"), ("tests.components.qwikswitch.test_init", "test_sensor_device"), ("tests.components.rflink.test_init", "test_send_command_invalid_arguments"), ("tests.components.samsungtv.test_media_player", "test_update_connection_failure"), - ("tests.components.tplink.test_init", "test_configuring_device_types"), ( "tests.components.tplink.test_init", "test_configuring_devices_from_multiple_sources", @@ -108,18 +88,8 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.unifi_direct.test_device_tracker", "test_get_scanner"), ("tests.components.upnp.test_init", "test_async_setup_entry_default"), ("tests.components.upnp.test_init", "test_async_setup_entry_port_mapping"), - ("tests.components.vera.test_init", "test_init"), - ("tests.components.wunderground.test_sensor", "test_fails_because_of_unique_id"), ("tests.components.yr.test_sensor", "test_default_setup"), ("tests.components.yr.test_sensor", "test_custom_setup"), ("tests.components.yr.test_sensor", "test_forecast_setup"), ("tests.components.zwave.test_init", "test_power_schemes"), - ( - "tests.helpers.test_entity_platform", - "test_adding_entities_with_generator_and_thread_callback", - ), - ( - "tests.helpers.test_entity_platform", - "test_not_adding_duplicate_entities_with_unique_id", - ), ] From eff9b2a1a0bfffbb3da14934b9851119de6c28c9 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Wed, 1 Apr 2020 11:35:48 -0400 Subject: [PATCH 362/431] Clean up ZHA channel reporting configuration (#33497) --- .../components/zha/core/channels/base.py | 65 ++++++++----------- 1 file changed, 26 insertions(+), 39 deletions(-) diff --git a/homeassistant/components/zha/core/channels/base.py b/homeassistant/components/zha/core/channels/base.py index e718e688c50..a5255e7f756 100644 --- a/homeassistant/components/zha/core/channels/base.py +++ b/homeassistant/components/zha/core/channels/base.py @@ -20,9 +20,6 @@ from ..const import ( ATTR_UNIQUE_ID, ATTR_VALUE, CHANNEL_ZDO, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_RPT_CHANGE, SIGNAL_ATTR_UPDATED, ) from ..helpers import LogMixin, safe_read @@ -149,57 +146,47 @@ class ZigbeeChannel(LogMixin): "Failed to bind '%s' cluster: %s", self.cluster.ep_attribute, str(ex) ) - async def configure_reporting( - self, - attr, - report_config=( - REPORT_CONFIG_MIN_INT, - REPORT_CONFIG_MAX_INT, - REPORT_CONFIG_RPT_CHANGE, - ), - ): + async def configure_reporting(self) -> None: """Configure attribute reporting for a cluster. This also swallows DeliveryError exceptions that are thrown when devices are unreachable. """ - attr_name = self.cluster.attributes.get(attr, [attr])[0] - kwargs = {} if self.cluster.cluster_id >= 0xFC00 and self._ch_pool.manufacturer_code: kwargs["manufacturer"] = self._ch_pool.manufacturer_code - min_report_int, max_report_int, reportable_change = report_config - try: - res = await self.cluster.configure_reporting( - attr, min_report_int, max_report_int, reportable_change, **kwargs - ) - self.debug( - "reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", - attr_name, - self.cluster.ep_attribute, - min_report_int, - max_report_int, - reportable_change, - res, - ) - except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: - self.debug( - "failed to set reporting for '%s' attr on '%s' cluster: %s", - attr_name, - self.cluster.ep_attribute, - str(ex), - ) + for report in self._report_config: + attr = report["attr"] + attr_name = self.cluster.attributes.get(attr, [attr])[0] + min_report_int, max_report_int, reportable_change = report["config"] + try: + res = await self.cluster.configure_reporting( + attr, min_report_int, max_report_int, reportable_change, **kwargs + ) + self.debug( + "reporting '%s' attr on '%s' cluster: %d/%d/%d: Result: '%s'", + attr_name, + self.cluster.ep_attribute, + min_report_int, + max_report_int, + reportable_change, + res, + ) + except (zigpy.exceptions.DeliveryError, asyncio.TimeoutError) as ex: + self.debug( + "failed to set reporting for '%s' attr on '%s' cluster: %s", + attr_name, + self.cluster.ep_attribute, + str(ex), + ) async def async_configure(self): """Set cluster binding and attribute reporting.""" if not self._ch_pool.skip_configuration: await self.bind() if self.cluster.is_server: - for report_config in self._report_config: - await self.configure_reporting( - report_config["attr"], report_config["config"] - ) + await self.configure_reporting() self.debug("finished channel configuration") else: self.debug("skipping channel configuration") From fb93b79b126469f379de912874115906534beb27 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Wed, 1 Apr 2020 19:00:40 +0200 Subject: [PATCH 363/431] Add MQTT debug info (#33461) * Add MQTT debug info * Tweaks * Tweaks --- homeassistant/components/mqtt/__init__.py | 24 +- homeassistant/components/mqtt/const.py | 1 + homeassistant/components/mqtt/debug_info.py | 146 +++++++++++ .../components/mqtt/device_trigger.py | 7 + homeassistant/components/mqtt/discovery.py | 3 +- homeassistant/components/mqtt/sensor.py | 2 + homeassistant/components/mqtt/subscription.py | 10 + tests/components/mqtt/test_common.py | 230 ++++++++++++++++++ tests/components/mqtt/test_device_trigger.py | 43 +++- tests/components/mqtt/test_init.py | 46 ++++ tests/components/mqtt/test_sensor.py | 38 +++ 11 files changed, 547 insertions(+), 3 deletions(-) create mode 100644 homeassistant/components/mqtt/debug_info.py diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index ee14fe432b5..bc59be0d1f3 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -45,7 +45,8 @@ from homeassistant.util.async_ import run_callback_threadsafe from homeassistant.util.logging import catch_log_exception # Loading the config flow file will register the flow -from . import config_flow, discovery, server # noqa: F401 pylint: disable=unused-import +from . import config_flow # noqa: F401 pylint: disable=unused-import +from . import debug_info, discovery, server from .const import ( ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC, @@ -56,6 +57,7 @@ from .const import ( DEFAULT_QOS, PROTOCOL_311, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash, set_discovery_hash from .models import Message, MessageCallbackType, PublishPayloadType from .subscription import async_subscribe_topics, async_unsubscribe_topics @@ -513,6 +515,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: websocket_api.async_register_command(hass, websocket_subscribe) websocket_api.async_register_command(hass, websocket_remove_device) + websocket_api.async_register_command(hass, websocket_mqtt_info) if conf is None: # If we have a config entry, setup is done by that config entry. @@ -1058,6 +1061,7 @@ class MqttAttributes(Entity): attr_tpl.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def attributes_message_received(msg: Message) -> None: try: payload = msg.payload @@ -1122,6 +1126,7 @@ class MqttAvailability(Entity): """(Re)Subscribe to topics.""" @callback + @log_messages(self.hass, self.entity_id) def availability_message_received(msg: Message) -> None: """Handle a new received MQTT availability message.""" if msg.payload == self._avail_config[CONF_PAYLOAD_AVAILABLE]: @@ -1207,6 +1212,7 @@ class MqttDiscoveryUpdate(Entity): _LOGGER.info( "Got update for entity with hash: %s '%s'", discovery_hash, payload, ) + debug_info.update_entity_discovery_data(self.hass, payload, self.entity_id) if not payload: # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) @@ -1219,6 +1225,9 @@ class MqttDiscoveryUpdate(Entity): await self._discovery_update(payload) if discovery_hash: + debug_info.add_entity_discovery_data( + self.hass, self._discovery_data, self.entity_id + ) # Set in case the entity has been removed and is re-added set_discovery_hash(self.hass, discovery_hash) self._remove_signal = async_dispatcher_connect( @@ -1242,6 +1251,7 @@ class MqttDiscoveryUpdate(Entity): def _cleanup_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" if self._discovery_data and not self._removed_from_hass: + debug_info.remove_entity_data(self.hass, self.entity_id) clear_discovery_hash(self.hass, self._discovery_data[ATTR_DISCOVERY_HASH]) self._removed_from_hass = True @@ -1303,6 +1313,18 @@ class MqttEntityDeviceInfo(Entity): return device_info_from_config(self._device_config) +@websocket_api.websocket_command( + {vol.Required("type"): "mqtt/device/debug_info", vol.Required("device_id"): str} +) +@websocket_api.async_response +async def websocket_mqtt_info(hass, connection, msg): + """Get MQTT debug info for device.""" + device_id = msg["device_id"] + mqtt_info = await debug_info.info_for_device(hass, device_id) + + connection.send_result(msg["id"], mqtt_info) + + @websocket_api.websocket_command( {vol.Required("type"): "mqtt/device/remove", vol.Required("device_id"): str} ) diff --git a/homeassistant/components/mqtt/const.py b/homeassistant/components/mqtt/const.py index 6044ec2af6e..5d1fe2e2505 100644 --- a/homeassistant/components/mqtt/const.py +++ b/homeassistant/components/mqtt/const.py @@ -4,6 +4,7 @@ CONF_DISCOVERY = "discovery" DEFAULT_DISCOVERY = False ATTR_DISCOVERY_HASH = "discovery_hash" +ATTR_DISCOVERY_PAYLOAD = "discovery_payload" ATTR_DISCOVERY_TOPIC = "discovery_topic" CONF_STATE_TOPIC = "state_topic" PROTOCOL_311 = "3.1.1" diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py new file mode 100644 index 00000000000..ec4ff1676bb --- /dev/null +++ b/homeassistant/components/mqtt/debug_info.py @@ -0,0 +1,146 @@ +"""Helper to handle a set of topics to subscribe to.""" +from collections import deque +from functools import wraps +import logging +from typing import Any + +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC +from .models import MessageCallbackType + +_LOGGER = logging.getLogger(__name__) + +DATA_MQTT_DEBUG_INFO = "mqtt_debug_info" +STORED_MESSAGES = 10 + + +def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType: + """Wrap an MQTT message callback to support message logging.""" + + def _log_message(msg): + """Log message.""" + debug_info = hass.data[DATA_MQTT_DEBUG_INFO] + messages = debug_info["entities"][entity_id]["topics"][msg.topic] + messages.append(msg.payload) + + def _decorator(msg_callback: MessageCallbackType): + @wraps(msg_callback) + def wrapper(msg: Any) -> None: + """Log message.""" + _log_message(msg) + msg_callback(msg) + + setattr(wrapper, "__entity_id", entity_id) + return wrapper + + return _decorator + + +def add_topic(hass, message_callback, topic): + """Prepare debug data for topic.""" + entity_id = getattr(message_callback, "__entity_id", None) + if entity_id: + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + entity_info = debug_info["entities"].setdefault( + entity_id, {"topics": {}, "discovery_data": {}} + ) + entity_info["topics"][topic] = deque([], STORED_MESSAGES) + + +def remove_topic(hass, message_callback, topic): + """Remove debug data for topic.""" + entity_id = getattr(message_callback, "__entity_id", None) + if entity_id and entity_id in hass.data[DATA_MQTT_DEBUG_INFO]["entities"]: + hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id]["topics"].pop(topic) + + +def add_entity_discovery_data(hass, discovery_data, entity_id): + """Add discovery data.""" + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + entity_info = debug_info["entities"].setdefault( + entity_id, {"topics": {}, "discovery_data": {}} + ) + entity_info["discovery_data"] = discovery_data + + +def update_entity_discovery_data(hass, discovery_payload, entity_id): + """Update discovery data.""" + entity_info = hass.data[DATA_MQTT_DEBUG_INFO]["entities"][entity_id] + entity_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + + +def remove_entity_data(hass, entity_id): + """Remove discovery data.""" + hass.data[DATA_MQTT_DEBUG_INFO]["entities"].pop(entity_id) + + +def add_trigger_discovery_data(hass, discovery_hash, discovery_data, device_id): + """Add discovery data.""" + debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + debug_info["triggers"][discovery_hash] = { + "device_id": device_id, + "discovery_data": discovery_data, + } + + +def update_trigger_discovery_data(hass, discovery_hash, discovery_payload): + """Update discovery data.""" + trigger_info = hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash] + trigger_info["discovery_data"][ATTR_DISCOVERY_PAYLOAD] = discovery_payload + + +def remove_trigger_discovery_data(hass, discovery_hash): + """Remove discovery data.""" + hass.data[DATA_MQTT_DEBUG_INFO]["triggers"][discovery_hash]["discovery_data"] = None + + +async def info_for_device(hass, device_id): + """Get debug info for a device.""" + mqtt_info = {"entities": [], "triggers": []} + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + entries = hass.helpers.entity_registry.async_entries_for_device( + entity_registry, device_id + ) + mqtt_debug_info = hass.data.setdefault( + DATA_MQTT_DEBUG_INFO, {"entities": {}, "triggers": {}} + ) + for entry in entries: + if entry.entity_id not in mqtt_debug_info["entities"]: + continue + + entity_info = mqtt_debug_info["entities"][entry.entity_id] + topics = [ + {"topic": topic, "messages": list(messages)} + for topic, messages in entity_info["topics"].items() + ] + discovery_data = { + "topic": entity_info["discovery_data"].get(ATTR_DISCOVERY_TOPIC, ""), + "payload": entity_info["discovery_data"].get(ATTR_DISCOVERY_PAYLOAD, ""), + } + mqtt_info["entities"].append( + { + "entity_id": entry.entity_id, + "topics": topics, + "discovery_data": discovery_data, + } + ) + + for trigger in mqtt_debug_info["triggers"].values(): + if trigger["device_id"] != device_id: + continue + + discovery_data = { + "topic": trigger["discovery_data"][ATTR_DISCOVERY_TOPIC], + "payload": trigger["discovery_data"][ATTR_DISCOVERY_PAYLOAD], + } + mqtt_info["triggers"].append({"discovery_data": discovery_data}) + + return mqtt_info diff --git a/homeassistant/components/mqtt/device_trigger.py b/homeassistant/components/mqtt/device_trigger.py index 88c635ae3a8..3b65243d078 100644 --- a/homeassistant/components/mqtt/device_trigger.py +++ b/homeassistant/components/mqtt/device_trigger.py @@ -26,6 +26,7 @@ from . import ( CONF_QOS, DOMAIN, cleanup_device_registry, + debug_info, ) from .discovery import MQTT_DISCOVERY_UPDATED, clear_discovery_hash @@ -183,6 +184,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): if not payload: # Empty payload: Remove trigger _LOGGER.info("Removing trigger: %s", discovery_hash) + debug_info.remove_trigger_discovery_data(hass, discovery_hash) if discovery_id in hass.data[DEVICE_TRIGGERS]: device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] device_trigger.detach_trigger() @@ -192,6 +194,7 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): else: # Non-empty payload: Update trigger _LOGGER.info("Updating trigger: %s", discovery_hash) + debug_info.update_trigger_discovery_data(hass, discovery_hash, payload) config = TRIGGER_DISCOVERY_SCHEMA(payload) await _update_device(hass, config_entry, config) device_trigger = hass.data[DEVICE_TRIGGERS][discovery_id] @@ -230,6 +233,9 @@ async def async_setup_trigger(hass, config, config_entry, discovery_data): await hass.data[DEVICE_TRIGGERS][discovery_id].update_trigger( config, discovery_hash, remove_signal ) + debug_info.add_trigger_discovery_data( + hass, discovery_hash, discovery_data, device.id + ) async def async_device_removed(hass: HomeAssistant, device_id: str): @@ -241,6 +247,7 @@ async def async_device_removed(hass: HomeAssistant, device_id: str): discovery_hash = device_trigger.discovery_data[ATTR_DISCOVERY_HASH] discovery_topic = device_trigger.discovery_data[ATTR_DISCOVERY_TOPIC] + debug_info.remove_trigger_discovery_data(hass, discovery_hash) device_trigger.detach_trigger() clear_discovery_hash(hass, discovery_hash) device_trigger.remove_signal() diff --git a/homeassistant/components/mqtt/discovery.py b/homeassistant/components/mqtt/discovery.py index 3bcd8594ebe..689b279c5e7 100644 --- a/homeassistant/components/mqtt/discovery.py +++ b/homeassistant/components/mqtt/discovery.py @@ -11,7 +11,7 @@ from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.typing import HomeAssistantType from .abbreviations import ABBREVIATIONS, DEVICE_ABBREVIATIONS -from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_TOPIC +from .const import ATTR_DISCOVERY_HASH, ATTR_DISCOVERY_PAYLOAD, ATTR_DISCOVERY_TOPIC _LOGGER = logging.getLogger(__name__) @@ -135,6 +135,7 @@ async def async_start( setattr(payload, "__configuration_source__", f"MQTT (topic: '{topic}')") discovery_data = { ATTR_DISCOVERY_HASH: discovery_hash, + ATTR_DISCOVERY_PAYLOAD: payload, ATTR_DISCOVERY_TOPIC: topic, } setattr(payload, "discovery_data", discovery_data) diff --git a/homeassistant/components/mqtt/sensor.py b/homeassistant/components/mqtt/sensor.py index 7be923927ca..2704c5ae3a1 100644 --- a/homeassistant/components/mqtt/sensor.py +++ b/homeassistant/components/mqtt/sensor.py @@ -35,6 +35,7 @@ from . import ( MqttEntityDeviceInfo, subscription, ) +from .debug_info import log_messages from .discovery import MQTT_DISCOVERY_NEW, clear_discovery_hash _LOGGER = logging.getLogger(__name__) @@ -137,6 +138,7 @@ class MqttSensor( template.hass = self.hass @callback + @log_messages(self.hass, self.entity_id) def message_received(msg): """Handle new MQTT messages.""" payload = msg.payload diff --git a/homeassistant/components/mqtt/subscription.py b/homeassistant/components/mqtt/subscription.py index be48a769a23..b4793a49dca 100644 --- a/homeassistant/components/mqtt/subscription.py +++ b/homeassistant/components/mqtt/subscription.py @@ -8,6 +8,7 @@ from homeassistant.components import mqtt from homeassistant.helpers.typing import HomeAssistantType from homeassistant.loader import bind_hass +from . import debug_info from .const import DEFAULT_QOS from .models import MessageCallbackType @@ -18,6 +19,7 @@ _LOGGER = logging.getLogger(__name__) class EntitySubscription: """Class to hold data about an active entity topic subscription.""" + hass = attr.ib(type=HomeAssistantType) topic = attr.ib(type=str) message_callback = attr.ib(type=MessageCallbackType) unsubscribe_callback = attr.ib(type=Optional[Callable[[], None]]) @@ -31,11 +33,16 @@ class EntitySubscription: if other is not None and other.unsubscribe_callback is not None: other.unsubscribe_callback() + # Clear debug data if it exists + debug_info.remove_topic(self.hass, other.message_callback, other.topic) if self.topic is None: # We were asked to remove the subscription or not to create it return + # Prepare debug data + debug_info.add_topic(self.hass, self.message_callback, self.topic) + self.unsubscribe_callback = await mqtt.async_subscribe( hass, self.topic, self.message_callback, self.qos, self.encoding ) @@ -77,6 +84,7 @@ async def async_subscribe_topics( unsubscribe_callback=None, qos=value.get("qos", DEFAULT_QOS), encoding=value.get("encoding", "utf-8"), + hass=hass, ) # Get the current subscription state current = current_subscriptions.pop(key, None) @@ -87,6 +95,8 @@ async def async_subscribe_topics( for remaining in current_subscriptions.values(): if remaining.unsubscribe_callback is not None: remaining.unsubscribe_callback() + # Clear debug data if it exists + debug_info.remove_topic(hass, remaining.message_callback, remaining.topic) return new_state diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 2f437174299..0d1b892611d 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -4,6 +4,7 @@ import json from unittest.mock import ANY from homeassistant.components import mqtt +from homeassistant.components.mqtt import debug_info from homeassistant.components.mqtt.discovery import async_start from homeassistant.const import ATTR_ASSUMED_STATE, STATE_UNAVAILABLE @@ -519,3 +520,232 @@ async def help_test_entity_id_update_discovery_update( async_fire_mqtt_message(hass, f"{topic}_2", "online") state = hass.states.get(f"{domain}.milk") assert state.state != STATE_UNAVAILABLE + + +async def help_test_entity_debug_info(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + + +async def help_test_entity_debug_info_max_messages(hass, mqtt_mock, domain, config): + """Test debug_info message overflow. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + + for i in range(0, debug_info.STORED_MESSAGES + 1): + async_fire_mqtt_message(hass, "test-topic", f"{i}") + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert ( + len(debug_info_data["entities"][0]["topics"][0]["messages"]) + == debug_info.STORED_MESSAGES + ) + messages = [f"{i}" for i in range(1, debug_info.STORED_MESSAGES + 1)] + assert {"topic": "test-topic", "messages": messages} in debug_info_data["entities"][ + 0 + ]["topics"] + + +async def help_test_entity_debug_info_message( + hass, mqtt_mock, domain, config, topic=None, payload=None +): + """Test debug_info message overflow. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + if topic is None: + # Add default topic to config + config["state_topic"] = "state-topic" + topic = "state-topic" + + if payload is None: + payload = "ON" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) >= 1 + assert {"topic": topic, "messages": []} in debug_info_data["entities"][0]["topics"] + + async_fire_mqtt_message(hass, topic, payload) + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"][0]["topics"]) >= 1 + assert {"topic": topic, "messages": [payload]} in debug_info_data["entities"][0][ + "topics" + ] + + +async def help_test_entity_debug_info_remove(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + entity_id = debug_info_data["entities"][0]["entity_id"] + + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", "") + await hass.async_block_till_done() + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 0 + assert entity_id not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + + +async def help_test_entity_debug_info_update_entity_id(hass, mqtt_mock, domain, config): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + # Add device settings to config + config = copy.deepcopy(config[domain]) + config["device"] = copy.deepcopy(DEFAULT_CONFIG_DEVICE_INFO_ID) + config["unique_id"] = "veryunique" + + entry = MockConfigEntry(domain=mqtt.DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + dev_registry = await hass.helpers.device_registry.async_get_registry() + ent_registry = mock_registry(hass, {}) + + data = json.dumps(config) + async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) + await hass.async_block_till_done() + + device = dev_registry.async_get_device({("mqtt", "helloworld")}, set()) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.test" + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + + ent_registry.async_update_entity(f"{domain}.test", new_entity_id=f"{domain}.milk") + await hass.async_block_till_done() + await hass.async_block_till_done() + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 1 + assert ( + debug_info_data["entities"][0]["discovery_data"]["topic"] + == f"homeassistant/{domain}/bla/config" + ) + assert debug_info_data["entities"][0]["discovery_data"]["payload"] == config + assert debug_info_data["entities"][0]["entity_id"] == f"{domain}.milk" + assert len(debug_info_data["entities"][0]["topics"]) == 1 + assert {"topic": "test-topic", "messages": []} in debug_info_data["entities"][0][ + "topics" + ] + assert len(debug_info_data["triggers"]) == 0 + assert ( + f"{domain}.test" not in hass.data[debug_info.DATA_MQTT_DEBUG_INFO]["entities"] + ) diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 6766002717d..7274badbed9 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -4,7 +4,7 @@ import json import pytest import homeassistant.components.automation as automation -from homeassistant.components.mqtt import DOMAIN +from homeassistant.components.mqtt import DOMAIN, debug_info from homeassistant.components.mqtt.device_trigger import async_attach_trigger from homeassistant.components.mqtt.discovery import async_start from homeassistant.setup import async_setup_component @@ -1104,3 +1104,44 @@ async def test_cleanup_device_with_entity2(hass, device_reg, entity_reg, mqtt_mo # Verify device registry entry is cleared device_entry = device_reg.async_get_device({("mqtt", "helloworld")}, set()) assert device_entry is None + + +async def test_trigger_debug_info(hass, mqtt_mock): + """Test debug_info. + + This is a test helper for MQTT debug_info. + """ + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, entry) + registry = await hass.helpers.device_registry.async_get_registry() + + config = { + "platform": "mqtt", + "automation_type": "trigger", + "topic": "test-topic", + "type": "foo", + "subtype": "bar", + "device": { + "connections": [["mac", "02:5b:26:a8:dc:12"]], + "manufacturer": "Whatever", + "name": "Beer", + "model": "Glass", + "sw_version": "0.1-beta", + }, + } + data = json.dumps(config) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + await hass.async_block_till_done() + + device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + assert device is not None + + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 1 + assert ( + debug_info_data["triggers"][0]["discovery_data"]["topic"] + == "homeassistant/device_automation/bla/config" + ) + assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 7d06c62b915..7aa185c2c39 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -1,5 +1,6 @@ """The tests for the MQTT component.""" from datetime import timedelta +import json import ssl import unittest from unittest import mock @@ -934,3 +935,48 @@ async def test_mqtt_ws_remove_non_mqtt_device( response = await client.receive_json() assert not response["success"] assert response["error"]["code"] == websocket_api.const.ERR_NOT_FOUND + + +async def test_mqtt_ws_get_device_debug_info( + hass, device_reg, hass_ws_client, mqtt_mock +): + """Test MQTT websocket device debug info.""" + config_entry = MockConfigEntry(domain=mqtt.DOMAIN) + config_entry.add_to_hass(hass) + await async_start(hass, "homeassistant", {}, config_entry) + + config = { + "device": {"identifiers": ["0AFFD2"]}, + "platform": "mqtt", + "state_topic": "foobar/sensor", + "unique_id": "unique", + } + data = json.dumps(config) + + async_fire_mqtt_message(hass, "homeassistant/sensor/bla/config", data) + await hass.async_block_till_done() + + # Verify device entry is created + device_entry = device_reg.async_get_device({("mqtt", "0AFFD2")}, set()) + assert device_entry is not None + + client = await hass_ws_client(hass) + await client.send_json( + {"id": 5, "type": "mqtt/device/debug_info", "device_id": device_entry.id} + ) + response = await client.receive_json() + assert response["success"] + expected_result = { + "entities": [ + { + "entity_id": "sensor.mqtt_sensor", + "topics": [{"topic": "foobar/sensor", "messages": []}], + "discovery_data": { + "payload": config, + "topic": "homeassistant/sensor/bla/config", + }, + } + ], + "triggers": [], + } + assert response["result"] == expected_result diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 061e53250cb..34d3c33f8d7 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -19,6 +19,11 @@ from .test_common import ( help_test_discovery_removal, help_test_discovery_update, help_test_discovery_update_attr, + help_test_entity_debug_info, + help_test_entity_debug_info_max_messages, + help_test_entity_debug_info_message, + help_test_entity_debug_info_remove, + help_test_entity_debug_info_update_entity_id, help_test_entity_device_info_remove, help_test_entity_device_info_update, help_test_entity_device_info_with_connection, @@ -437,3 +442,36 @@ async def test_entity_device_info_with_hub(hass, mqtt_mock): device = registry.async_get_device({("mqtt", "helloworld")}, set()) assert device is not None assert device.via_device_id == hub.id + + +async def test_entity_debug_info(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info(hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG) + + +async def test_entity_debug_info_max_messages(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_max_messages( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_message(hass, mqtt_mock): + """Test MQTT debug info.""" + await help_test_entity_debug_info_message( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_remove(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_remove( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) + + +async def test_entity_debug_info_update_entity_id(hass, mqtt_mock): + """Test MQTT sensor debug info.""" + await help_test_entity_debug_info_update_entity_id( + hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG + ) From 0cf9268ca8adb14e4a2396d40a7ea03d4404e958 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Wed, 1 Apr 2020 20:04:44 +0200 Subject: [PATCH 364/431] Fix invalid directory for dnsmasq files in asuswrt (#33503) --- homeassistant/components/asuswrt/__init__.py | 2 +- tests/components/asuswrt/test_device_tracker.py | 6 ++---- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index a0afbed69f1..a0eee38c3f8 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -51,7 +51,7 @@ CONFIG_SCHEMA = vol.Schema( cv.ensure_list, [vol.In(SENSOR_TYPES)] ), vol.Optional(CONF_INTERFACE, default=DEFAULT_INTERFACE): cv.string, - vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.isdir, + vol.Optional(CONF_DNSMASQ, default=DEFAULT_DNSMASQ): cv.string, } ) }, diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index 095b7b76d60..b91b815d58e 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -19,9 +19,7 @@ async def test_password_or_pub_key_required(hass): AsusWrt().connection.async_connect = mock_coro_func() AsusWrt().is_connected = False result = await async_setup_component( - hass, - DOMAIN, - {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}}, + hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} ) assert not result @@ -62,7 +60,7 @@ async def test_specify_non_directory_path_for_dnsmasq(hass): CONF_HOST: "fake_host", CONF_USERNAME: "fake_user", CONF_PASSWORD: "4321", - CONF_DNSMASQ: "?non_directory?", + CONF_DNSMASQ: 1234, } }, ) From 4a32a0f1daf12f7b9f4fb9aef60576be944b6116 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 1 Apr 2020 13:24:30 -0500 Subject: [PATCH 365/431] Expand network util to check for link local addresses (#33499) --- homeassistant/components/axis/config_flow.py | 5 ++- .../components/doorbird/config_flow.py | 4 +- homeassistant/util/network.py | 37 +++++++++++++---- tests/util/test_network.py | 40 +++++++++++++++++++ 4 files changed, 77 insertions(+), 9 deletions(-) create mode 100644 tests/util/test_network.py diff --git a/homeassistant/components/axis/config_flow.py b/homeassistant/components/axis/config_flow.py index 29658c19c5b..37141d6017a 100644 --- a/homeassistant/components/axis/config_flow.py +++ b/homeassistant/components/axis/config_flow.py @@ -1,5 +1,7 @@ """Config flow to configure Axis devices.""" +from ipaddress import ip_address + import voluptuous as vol from homeassistant import config_entries @@ -11,6 +13,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.util.network import is_link_local from .const import CONF_MODEL, DOMAIN from .device import get_device @@ -129,7 +132,7 @@ class AxisFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if serial_number[:6] not in AXIS_OUI: return self.async_abort(reason="not_axis_device") - if discovery_info[CONF_HOST].startswith("169.254"): + if is_link_local(ip_address(discovery_info[CONF_HOST])): return self.async_abort(reason="link_local_address") await self.async_set_unique_id(serial_number) diff --git a/homeassistant/components/doorbird/config_flow.py b/homeassistant/components/doorbird/config_flow.py index 410fb13a212..aa712a63ed0 100644 --- a/homeassistant/components/doorbird/config_flow.py +++ b/homeassistant/components/doorbird/config_flow.py @@ -1,4 +1,5 @@ """Config flow for DoorBird integration.""" +from ipaddress import ip_address import logging import urllib @@ -8,6 +9,7 @@ import voluptuous as vol from homeassistant import config_entries, core, exceptions from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback +from homeassistant.util.network import is_link_local from .const import CONF_EVENTS, DOORBIRD_OUI from .const import DOMAIN # pylint:disable=unused-import @@ -90,7 +92,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if macaddress[:6] != DOORBIRD_OUI: return self.async_abort(reason="not_doorbird_device") - if discovery_info[CONF_HOST].startswith("169.254"): + if is_link_local(ip_address(discovery_info[CONF_HOST])): return self.async_abort(reason="link_local_address") await self.async_set_unique_id(macaddress) diff --git a/homeassistant/util/network.py b/homeassistant/util/network.py index cc028478e51..e4d376dc487 100644 --- a/homeassistant/util/network.py +++ b/homeassistant/util/network.py @@ -1,18 +1,41 @@ """Network utilities.""" -from ipaddress import IPv4Address, IPv6Address, ip_address, ip_network +from ipaddress import IPv4Address, IPv6Address, ip_network from typing import Union -# IP addresses of loopback interfaces -LOCAL_IPS = (ip_address("127.0.0.1"), ip_address("::1")) +# RFC6890 - IP addresses of loopback interfaces +LOOPBACK_NETWORKS = ( + ip_network("127.0.0.0/8"), + ip_network("::1/128"), + ip_network("::ffff:127.0.0.0/104"), +) -# RFC1918 - Address allocation for Private Internets -LOCAL_NETWORKS = ( +# RFC6890 - Address allocation for Private Internets +PRIVATE_NETWORKS = ( + ip_network("fd00::/8"), ip_network("10.0.0.0/8"), ip_network("172.16.0.0/12"), ip_network("192.168.0.0/16"), ) +# RFC6890 - Link local ranges +LINK_LOCAL_NETWORK = ip_network("169.254.0.0/16") + + +def is_loopback(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is a loopback address.""" + return any(address in network for network in LOOPBACK_NETWORKS) + + +def is_private(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is a private address.""" + return any(address in network for network in PRIVATE_NETWORKS) + + +def is_link_local(address: Union[IPv4Address, IPv6Address]) -> bool: + """Check if an address is link local.""" + return address in LINK_LOCAL_NETWORK + def is_local(address: Union[IPv4Address, IPv6Address]) -> bool: - """Check if an address is local.""" - return address in LOCAL_IPS or any(address in network for network in LOCAL_NETWORKS) + """Check if an address is loopback or private.""" + return is_loopback(address) or is_private(address) diff --git a/tests/util/test_network.py b/tests/util/test_network.py new file mode 100644 index 00000000000..c4c33c8d187 --- /dev/null +++ b/tests/util/test_network.py @@ -0,0 +1,40 @@ +"""Test Home Assistant volume utility functions.""" + +from ipaddress import ip_address + +import homeassistant.util.network as network_util + + +def test_is_loopback(): + """Test loopback addresses.""" + assert network_util.is_loopback(ip_address("127.0.0.2")) + assert network_util.is_loopback(ip_address("127.0.0.1")) + assert network_util.is_loopback(ip_address("::1")) + assert network_util.is_loopback(ip_address("::ffff:127.0.0.0")) + assert network_util.is_loopback(ip_address("0:0:0:0:0:0:0:1")) + assert network_util.is_loopback(ip_address("0:0:0:0:0:ffff:7f00:1")) + assert not network_util.is_loopback(ip_address("104.26.5.238")) + assert not network_util.is_loopback(ip_address("2600:1404:400:1a4::356e")) + + +def test_is_private(): + """Test private addresses.""" + assert network_util.is_private(ip_address("192.168.0.1")) + assert network_util.is_private(ip_address("172.16.12.0")) + assert network_util.is_private(ip_address("10.5.43.3")) + assert network_util.is_private(ip_address("fd12:3456:789a:1::1")) + assert not network_util.is_private(ip_address("127.0.0.1")) + assert not network_util.is_private(ip_address("::1")) + + +def test_is_link_local(): + """Test link local addresses.""" + assert network_util.is_link_local(ip_address("169.254.12.3")) + assert not network_util.is_link_local(ip_address("127.0.0.1")) + + +def test_is_local(): + """Test local addresses.""" + assert network_util.is_local(ip_address("192.168.0.1")) + assert network_util.is_local(ip_address("127.0.0.1")) + assert not network_util.is_local(ip_address("208.5.4.2")) From 400602a8b3f95257b2f48ac6c9356a08a9e08acc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 1 Apr 2020 20:27:01 +0200 Subject: [PATCH 366/431] Updated frontend to 20200401.0 (#33505) --- 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 e1ae4bca255..8a540a96b0e 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==20200330.0" + "home-assistant-frontend==20200401.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 35423033cf9..ca19feba5ef 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200330.0 +home-assistant-frontend==20200401.0 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 2474aed333d..0cbc59f76c4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200330.0 +home-assistant-frontend==20200401.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 898b274cec0..a5d9daf91d9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200330.0 +home-assistant-frontend==20200401.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 71aaf2d809c2165db166aeded0a240ac16f97b67 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Wed, 1 Apr 2020 20:42:22 +0200 Subject: [PATCH 367/431] Add device triggers for Hue remotes (#33476) * Store device_registry entry id in HueEvent so it can be retrieved with that key when using device triggers * Add device_trigger for hue_event from hue remotes * supporting Hue dimmer switch & Hue Tap * State mapping and strings are copied from deCONZ * refactor mock_bridge for hue tests and also share `setup_bridge_for_sensors` for test_sensor_base and test_device_trigger. * Add tests for device triggers with hue remotes * Remove some triggers --- .../components/hue/.translations/en.json | 17 ++ .../components/hue/device_trigger.py | 149 +++++++++++++++ homeassistant/components/hue/hue_event.py | 4 +- homeassistant/components/hue/strings.json | 19 +- tests/components/hue/conftest.py | 86 ++++++++- tests/components/hue/test_device_trigger.py | 169 ++++++++++++++++++ tests/components/hue/test_light.py | 46 ----- tests/components/hue/test_sensor_base.py | 73 +------- 8 files changed, 443 insertions(+), 120 deletions(-) create mode 100644 homeassistant/components/hue/device_trigger.py create mode 100644 tests/components/hue/test_device_trigger.py diff --git a/homeassistant/components/hue/.translations/en.json b/homeassistant/components/hue/.translations/en.json index 350360285af..b16213bfbf8 100644 --- a/homeassistant/components/hue/.translations/en.json +++ b/homeassistant/components/hue/.translations/en.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "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", + "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" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/device_trigger.py b/homeassistant/components/hue/device_trigger.py new file mode 100644 index 00000000000..81877654746 --- /dev/null +++ b/homeassistant/components/hue/device_trigger.py @@ -0,0 +1,149 @@ +"""Provides device automations for Philips Hue events.""" +import logging + +import voluptuous as vol + +import homeassistant.components.automation.event as event +from homeassistant.components.device_automation import TRIGGER_BASE_SCHEMA +from homeassistant.components.device_automation.exceptions import ( + InvalidDeviceAutomationConfig, +) +from homeassistant.const import ( + CONF_DEVICE_ID, + CONF_DOMAIN, + CONF_EVENT, + CONF_PLATFORM, + CONF_TYPE, +) + +from . import DOMAIN +from .hue_event import CONF_HUE_EVENT, CONF_UNIQUE_ID + +_LOGGER = logging.getLogger(__file__) + +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_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" + + +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_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}, +} + +REMOTES = { + HUE_DIMMER_REMOTE_MODEL: HUE_DIMMER_REMOTE, + HUE_TAP_REMOTE_MODEL: HUE_TAP_REMOTE, +} + +TRIGGER_SCHEMA = 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): + """Validate config.""" + config = TRIGGER_SCHEMA(config) + + 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 + or device.model not in REMOTES + or trigger not in REMOTES[device.model] + ): + raise InvalidDeviceAutomationConfig + + return config + + +async def async_attach_trigger(hass, config, action, automation_info): + """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]) + + 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.CONF_PLATFORM: "event", + event.CONF_EVENT_TYPE: CONF_HUE_EVENT, + event.CONF_EVENT_DATA: {CONF_UNIQUE_ID: hue_event.unique_id, **trigger}, + } + + event_config = event.TRIGGER_SCHEMA(event_config) + return await event.async_attach_trigger( + hass, event_config, action, automation_info, platform_type="device" + ) + + +async def async_get_triggers(hass, device_id): + """List device triggers. + + 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) + + if device.model not in REMOTES: + return + + triggers = [] + for trigger, subtype in REMOTES[device.model].keys(): + 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/hue_event.py b/homeassistant/components/hue/hue_event.py index 838d5ead6da..ed1bc1c8f7d 100644 --- a/homeassistant/components/hue/hue_event.py +++ b/homeassistant/components/hue/hue_event.py @@ -28,6 +28,7 @@ class HueEvent(GenericHueDevice): def __init__(self, sensor, name, bridge, primary_sensor=None): """Register callback that will be used for signals.""" super().__init__(sensor, name, bridge, primary_sensor) + self.device_registry_id = None self.event_id = slugify(self.sensor.name) # Use the 'lastupdated' string to detect new remote presses @@ -79,9 +80,10 @@ class HueEvent(GenericHueDevice): entry = device_registry.async_get_or_create( config_entry_id=self.bridge.config_entry.entry_id, **self.device_info ) + self.device_registry_id = entry.id _LOGGER.debug( "Event registry with entry_id: %s and device_id: %s", - entry.id, + self.device_registry_id, self.device_id, ) diff --git a/homeassistant/components/hue/strings.json b/homeassistant/components/hue/strings.json index 78b990d5f42..0f70c49ff2e 100644 --- a/homeassistant/components/hue/strings.json +++ b/homeassistant/components/hue/strings.json @@ -27,5 +27,22 @@ "already_in_progress": "Config flow for bridge is already in progress.", "not_hue_bridge": "Not a Hue bridge" } + }, + "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", + "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" + } } -} +} \ No newline at end of file diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 49cd953a697..fa7c4ac473d 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,11 +1,95 @@ """Test helpers for Hue.""" -from unittest.mock import patch +from collections import deque +from unittest.mock import Mock, patch +from aiohue.groups import Groups +from aiohue.lights import Lights +from aiohue.sensors import Sensors import pytest +from homeassistant import config_entries +from homeassistant.components import hue +from homeassistant.components.hue import sensor_base as hue_sensor_base + @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): yield + + +def create_mock_bridge(hass): + """Create a mock Hue bridge.""" + bridge = Mock( + hass=hass, + available=True, + authorized=True, + allow_unreachable=False, + allow_groups=False, + api=Mock(), + reset_jobs=[], + spec=hue.HueBridge, + ) + bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) + bridge.mock_requests = [] + # We're using a deque so we can schedule multiple responses + # and also means that `popleft()` will blow up if we get more updates + # than expected. + bridge.mock_light_responses = deque() + bridge.mock_group_responses = deque() + bridge.mock_sensor_responses = deque() + + async def mock_request(method, path, **kwargs): + kwargs["method"] = method + kwargs["path"] = path + bridge.mock_requests.append(kwargs) + + if path == "lights": + return bridge.mock_light_responses.popleft() + if path == "groups": + return bridge.mock_group_responses.popleft() + if path == "sensors": + return bridge.mock_sensor_responses.popleft() + return None + + async def async_request_call(task): + await task() + + bridge.async_request_call = async_request_call + bridge.api.config.apiversion = "9.9.9" + bridge.api.lights = Lights({}, mock_request) + bridge.api.groups = Groups({}, mock_request) + bridge.api.sensors = Sensors({}, mock_request) + return bridge + + +@pytest.fixture +def mock_bridge(hass): + """Mock a Hue bridge.""" + return create_mock_bridge(hass) + + +async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): + """Load the Hue platform with the provided bridge for sensor-related platforms.""" + if hostname is None: + hostname = "mock-host" + hass.config.components.add(hue.DOMAIN) + config_entry = config_entries.ConfigEntry( + 1, + hue.DOMAIN, + "Mock Title", + {"host": hostname}, + "test", + config_entries.CONN_CLASS_LOCAL_POLL, + system_options={}, + ) + 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 + hass.config_entries._entries.append(config_entry) + + # and make sure it completes before going further + await hass.async_block_till_done() diff --git a/tests/components/hue/test_device_trigger.py b/tests/components/hue/test_device_trigger.py new file mode 100644 index 00000000000..b6d3f4f2f50 --- /dev/null +++ b/tests/components/hue/test_device_trigger.py @@ -0,0 +1,169 @@ +"""The tests for Philips Hue device triggers.""" +import pytest + +from homeassistant.components import hue +import homeassistant.components.automation as automation +from homeassistant.components.hue 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 tests.common import ( + assert_lists_same, + async_get_device_automations, + async_mock_service, + mock_device_registry, +) + +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): + """Test we get the expected triggers from a hue remote.""" + mock_bridge.mock_sensor_responses.append(REMOTES_RESPONSE) + await setup_bridge(hass, mock_bridge) + + assert len(mock_bridge.mock_requests) == 1 + # 2 remotes, just 1 battery sensor + assert len(hass.states.async_all()) == 1 + + # Get triggers for specific tap switch + hue_tap_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + ) + triggers = await async_get_device_automations(hass, "trigger", hue_tap_device.id) + + expected_triggers = [ + { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_tap_device.id, + "type": t_type, + "subtype": t_subtype, + } + for t_type, t_subtype in device_trigger.HUE_TAP_REMOTE.keys() + ] + assert_lists_same(triggers, expected_triggers) + + # Get triggers for specific dimmer switch + hue_dimmer_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:17:88:01:10:3e:3a:dc")}, connections={} + ) + triggers = await async_get_device_automations(hass, "trigger", hue_dimmer_device.id) + + trigger_batt = { + "platform": "device", + "domain": "sensor", + "device_id": hue_dimmer_device.id, + "type": "battery_level", + "entity_id": "sensor.hue_dimmer_switch_1_battery_level", + } + expected_triggers = [ + trigger_batt, + *[ + { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_dimmer_device.id, + "type": t_type, + "subtype": t_subtype, + } + for t_type, t_subtype in device_trigger.HUE_DIMMER_REMOTE.keys() + ], + ] + assert_lists_same(triggers, expected_triggers) + + +async def test_if_fires_on_state_change(hass, mock_bridge, 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 + assert len(hass.states.async_all()) == 1 + + # Set an automation with a specific tap switch trigger + hue_tap_device = device_reg.async_get_device( + {(hue.DOMAIN, "00:00:00:00:00:44:23:08")}, connections={} + ) + assert await async_setup_component( + hass, + automation.DOMAIN, + { + automation.DOMAIN: [ + { + "trigger": { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": hue_tap_device.id, + "type": "remote_button_short_press", + "subtype": "button_4", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "B4 - {{ trigger.event.data.event }}" + }, + }, + }, + { + "trigger": { + "platform": "device", + "domain": hue.DOMAIN, + "device_id": "mock-device-id", + "type": "remote_button_short_press", + "subtype": "button_1", + }, + "action": { + "service": "test.automation", + "data_template": { + "some": "B1 - {{ trigger.event.data.event }}" + }, + }, + }, + ] + }, + ) + + # Fake that the remote is being pressed. + new_sensor_response = dict(REMOTES_RESPONSE) + new_sensor_response["7"]["state"] = { + "buttonevent": 18, + "lastupdated": "2019-12-28T22:58:02", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + + assert len(calls) == 1 + assert calls[0].data["some"] == "B4 - 18" + + # Fake another button press. + new_sensor_response = dict(REMOTES_RESPONSE) + new_sensor_response["7"]["state"] = { + "buttonevent": 34, + "lastupdated": "2019-12-28T22:58:05", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + await mock_bridge.sensor_manager.coordinator.async_refresh() + await hass.async_block_till_done() + assert len(mock_bridge.mock_requests) == 3 + assert len(calls) == 1 diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index a99b947e48e..998e3cdea50 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -1,13 +1,9 @@ """Philips Hue lights platform tests.""" import asyncio -from collections import deque import logging from unittest.mock import Mock import aiohue -from aiohue.groups import Groups -from aiohue.lights import Lights -import pytest from homeassistant import config_entries from homeassistant.components import hue @@ -175,48 +171,6 @@ LIGHT_GAMUT = color.GamutType( LIGHT_GAMUT_TYPE = "A" -@pytest.fixture -def mock_bridge(hass): - """Mock a Hue bridge.""" - bridge = Mock( - hass=hass, - available=True, - authorized=True, - allow_unreachable=False, - allow_groups=False, - api=Mock(), - reset_jobs=[], - spec=hue.HueBridge, - ) - bridge.mock_requests = [] - # We're using a deque so we can schedule multiple responses - # and also means that `popleft()` will blow up if we get more updates - # than expected. - bridge.mock_light_responses = deque() - bridge.mock_group_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - - if path == "lights": - return bridge.mock_light_responses.popleft() - if path == "groups": - return bridge.mock_group_responses.popleft() - return None - - async def async_request_call(task): - await task() - - bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.lights = Lights({}, mock_request) - bridge.api.groups = Groups({}, mock_request) - - return bridge - - async def setup_bridge(hass, mock_bridge): """Load the Hue light platform with the provided bridge.""" hass.config.components.add(hue.DOMAIN) diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index cf1a4ab7983..576bc365d50 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -1,18 +1,14 @@ """Philips Hue sensors platform tests.""" import asyncio -from collections import deque import logging from unittest.mock import Mock import aiohue -from aiohue.sensors import Sensors -import pytest -from homeassistant import config_entries -from homeassistant.components import hue -from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.components.hue.hue_event import CONF_HUE_EVENT +from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge + _LOGGER = logging.getLogger(__name__) PRESENCE_SENSOR_1_PRESENT = { @@ -281,71 +277,6 @@ SENSOR_RESPONSE = { } -def create_mock_bridge(hass): - """Create a mock Hue bridge.""" - bridge = Mock( - hass=hass, - available=True, - authorized=True, - allow_unreachable=False, - allow_groups=False, - api=Mock(), - reset_jobs=[], - spec=hue.HueBridge, - ) - bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = [] - # We're using a deque so we can schedule multiple responses - # and also means that `popleft()` will blow up if we get more updates - # than expected. - bridge.mock_sensor_responses = deque() - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - - if path == "sensors": - return bridge.mock_sensor_responses.popleft() - return None - - async def async_request_call(task): - await task() - - bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.sensors = Sensors({}, mock_request) - return bridge - - -@pytest.fixture -def mock_bridge(hass): - """Mock a Hue bridge.""" - return create_mock_bridge(hass) - - -async def setup_bridge(hass, mock_bridge, hostname=None): - """Load the Hue platform with the provided bridge.""" - if hostname is None: - hostname = "mock-host" - hass.config.components.add(hue.DOMAIN) - config_entry = config_entries.ConfigEntry( - 1, - hue.DOMAIN, - "Mock Title", - {"host": hostname}, - "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, - ) - 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") - # and make sure it completes before going further - await hass.async_block_till_done() - - async def test_no_sensors(hass, mock_bridge): """Test the update_items function when no sensors are found.""" mock_bridge.allow_groups = True From c63ec698a1706a6ae0f1e76732c30e9ef5130362 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 1 Apr 2020 11:43:30 -0700 Subject: [PATCH 368/431] Update translations --- .../airvisual/.translations/lb.json | 8 +++ .../components/doorbird/.translations/de.json | 4 +- .../components/doorbird/.translations/es.json | 3 +- .../components/doorbird/.translations/lb.json | 4 +- .../components/doorbird/.translations/no.json | 4 +- .../doorbird/.translations/zh-Hant.json | 4 +- .../components/elkm1/.translations/lb.json | 2 + .../components/griddy/.translations/lb.json | 12 ++++ .../components/icloud/.translations/lb.json | 3 +- .../components/ipp/.translations/ca.json | 13 ++++ .../components/ipp/.translations/da.json | 32 +++++++++ .../components/ipp/.translations/lb.json | 3 +- .../components/ipp/.translations/no.json | 32 +++++++++ .../konnected/.translations/ca.json | 2 +- .../konnected/.translations/de.json | 5 ++ .../konnected/.translations/lb.json | 6 +- .../konnected/.translations/no.json | 7 +- .../konnected/.translations/zh-Hant.json | 7 +- .../components/nut/.translations/ca.json | 32 +++++++++ .../components/nut/.translations/da.json | 12 ++++ .../components/nut/.translations/de.json | 35 ++++++++++ .../components/nut/.translations/en.json | 65 +++++++++---------- .../components/nut/.translations/es.json | 37 +++++++++++ .../components/nut/.translations/lb.json | 36 ++++++++++ .../components/nut/.translations/no.json | 37 +++++++++++ .../components/nut/.translations/zh-Hant.json | 37 +++++++++++ .../components/vizio/.translations/lb.json | 6 +- 27 files changed, 402 insertions(+), 46 deletions(-) create mode 100644 homeassistant/components/ipp/.translations/ca.json create mode 100644 homeassistant/components/ipp/.translations/da.json create mode 100644 homeassistant/components/ipp/.translations/no.json create mode 100644 homeassistant/components/nut/.translations/ca.json create mode 100644 homeassistant/components/nut/.translations/da.json create mode 100644 homeassistant/components/nut/.translations/de.json create mode 100644 homeassistant/components/nut/.translations/es.json create mode 100644 homeassistant/components/nut/.translations/lb.json create mode 100644 homeassistant/components/nut/.translations/no.json create mode 100644 homeassistant/components/nut/.translations/zh-Hant.json diff --git a/homeassistant/components/airvisual/.translations/lb.json b/homeassistant/components/airvisual/.translations/lb.json index 0ae807dde52..eb267e793bb 100644 --- a/homeassistant/components/airvisual/.translations/lb.json +++ b/homeassistant/components/airvisual/.translations/lb.json @@ -17,5 +17,13 @@ } }, "title": "AirVisual" + }, + "options": { + "step": { + "init": { + "description": "Verschidden Optioune fir d'AirVisual Integratioun d\u00e9fin\u00e9ieren.", + "title": "Airvisual ariichten" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/de.json b/homeassistant/components/doorbird/.translations/de.json index 8676359e5ca..3709adaa69a 100644 --- a/homeassistant/components/doorbird/.translations/de.json +++ b/homeassistant/components/doorbird/.translations/de.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Dieser DoorBird ist bereits konfiguriert" + "already_configured": "Dieser DoorBird ist bereits konfiguriert", + "link_local_address": "Lokale Linkadressen werden nicht unterst\u00fctzt", + "not_doorbird_device": "Dieses Ger\u00e4t ist kein DoorBird" }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", diff --git a/homeassistant/components/doorbird/.translations/es.json b/homeassistant/components/doorbird/.translations/es.json index e7cf75f38fb..93ab919cc03 100644 --- a/homeassistant/components/doorbird/.translations/es.json +++ b/homeassistant/components/doorbird/.translations/es.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "DoorBird ya est\u00e1 configurado" + "already_configured": "DoorBird ya est\u00e1 configurado", + "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { "cannot_connect": "No se pudo conectar, por favor int\u00e9ntalo de nuevo", diff --git a/homeassistant/components/doorbird/.translations/lb.json b/homeassistant/components/doorbird/.translations/lb.json index 936dfddf261..d0b94ed6c59 100644 --- a/homeassistant/components/doorbird/.translations/lb.json +++ b/homeassistant/components/doorbird/.translations/lb.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "D\u00ebse DoorBird ass scho konfigur\u00e9iert" + "already_configured": "D\u00ebse DoorBird ass scho konfigur\u00e9iert", + "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", + "not_doorbird_device": "D\u00ebsen Apparat ass kee DoorBird" }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", diff --git a/homeassistant/components/doorbird/.translations/no.json b/homeassistant/components/doorbird/.translations/no.json index 91784f0b42a..29fb34672c8 100644 --- a/homeassistant/components/doorbird/.translations/no.json +++ b/homeassistant/components/doorbird/.translations/no.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Denne DoorBird er allerede konfigurert" + "already_configured": "Denne DoorBird er allerede konfigurert", + "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke", + "not_doorbird_device": "Denne enheten er ikke en DoorBird" }, "error": { "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", diff --git a/homeassistant/components/doorbird/.translations/zh-Hant.json b/homeassistant/components/doorbird/.translations/zh-Hant.json index afeded494c6..d8b6330b879 100644 --- a/homeassistant/components/doorbird/.translations/zh-Hant.json +++ b/homeassistant/components/doorbird/.translations/zh-Hant.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "\u6b64 DoorBird \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + "already_configured": "\u6b64 DoorBird \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", + "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", + "not_doorbird_device": "\u6b64\u8a2d\u5099\u4e26\u975e DoorBird" }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", diff --git a/homeassistant/components/elkm1/.translations/lb.json b/homeassistant/components/elkm1/.translations/lb.json index 6f451c94b5f..bb56b4c8154 100644 --- a/homeassistant/components/elkm1/.translations/lb.json +++ b/homeassistant/components/elkm1/.translations/lb.json @@ -12,7 +12,9 @@ "step": { "user": { "data": { + "address": "IP Adress oder Domain oder Serielle Port falls d'Verbindung seriell ass.", "password": "Passwuert (n\u00ebmmen ges\u00e9chert)", + "prefix": "Een eenzegaartege Pr\u00e4fix (eidel lossen wann et n\u00ebmmen 1 ElkM1 g\u00ebtt)", "protocol": "Protokoll", "temperature_unit": "Temperatur Eenheet d\u00e9i den ElkM1 benotzt.", "username": "Benotzernumm (n\u00ebmmen ges\u00e9chert)" diff --git a/homeassistant/components/griddy/.translations/lb.json b/homeassistant/components/griddy/.translations/lb.json index 3ed4a9c550a..c0ee3bc7d5a 100644 --- a/homeassistant/components/griddy/.translations/lb.json +++ b/homeassistant/components/griddy/.translations/lb.json @@ -1,9 +1,21 @@ { "config": { + "abort": { + "already_configured": "D\u00ebs Lued Zon ass scho konfigur\u00e9iert" + }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9iert w.e.g. nach emol.", "unknown": "Onerwaarte Feeler" }, + "step": { + "user": { + "data": { + "loadzone": "Lued Zone (Punkt vum R\u00e9glement)" + }, + "description": "Deng Lued Zon ass an dengem Griddy Kont enner \"Account > Meter > Load Zone.\"", + "title": "Griddy Lued Zon ariichten" + } + }, "title": "Griddy" } } \ No newline at end of file diff --git a/homeassistant/components/icloud/.translations/lb.json b/homeassistant/components/icloud/.translations/lb.json index 8ecc49a5ad9..0aa3c90eff0 100644 --- a/homeassistant/components/icloud/.translations/lb.json +++ b/homeassistant/components/icloud/.translations/lb.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Kont ass scho konfigur\u00e9iert" + "already_configured": "Kont ass scho konfigur\u00e9iert", + "no_device": "Kee vun dengen Apparater huet \"Find my iPhone\" aktiv\u00e9iert" }, "error": { "login": "Feeler beim Login: iwwerpr\u00e9ift \u00e4r E-Mail & Passwuert", diff --git a/homeassistant/components/ipp/.translations/ca.json b/homeassistant/components/ipp/.translations/ca.json new file mode 100644 index 00000000000..5708c8e638d --- /dev/null +++ b/homeassistant/components/ipp/.translations/ca.json @@ -0,0 +1,13 @@ +{ + "config": { + "flow_title": "Impressora: {name}", + "step": { + "user": { + "data": { + "host": "Amfitri\u00f3 o adre\u00e7a IP", + "port": "Port" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/da.json b/homeassistant/components/ipp/.translations/da.json new file mode 100644 index 00000000000..ede4601928e --- /dev/null +++ b/homeassistant/components/ipp/.translations/da.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Denne printer er allerede konfigureret.", + "connection_error": "Kunne ikke oprette forbindelse til printeren.", + "connection_upgrade": "Der kunne ikke oprettes forbindelse til printeren, fordi der kr\u00e6ves opgradering af forbindelsen." + }, + "error": { + "connection_error": "Kunne ikke oprette forbindelse til printeren.", + "connection_upgrade": "Kunne ikke oprette forbindelse til printeren. Pr\u00f8v igen med indstillingen SSL/TLS markeret." + }, + "flow_title": "Printer: {name}", + "step": { + "user": { + "data": { + "base_path": "Relativ sti til printeren", + "host": "V\u00e6rt eller IP-adresse", + "port": "Port", + "ssl": "Printeren underst\u00f8tter kommunikation via SSL/TLS", + "verify_ssl": "Printeren bruger et korrekt SSL-certifikat" + }, + "description": "Konfigurer din printer via Internet Printing Protocol (IPP) til at integrere med Home Assistant.", + "title": "Forbind din printer" + }, + "zeroconf_confirm": { + "description": "Vil du tilf\u00f8je printeren med navnet '{name}' til Home Assistant?", + "title": "Fandt printer" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/lb.json b/homeassistant/components/ipp/.translations/lb.json index fa8e2407696..bdda2cf1c14 100644 --- a/homeassistant/components/ipp/.translations/lb.json +++ b/homeassistant/components/ipp/.translations/lb.json @@ -2,7 +2,8 @@ "config": { "abort": { "already_configured": "D\u00ebse Printer ass scho konfigur\u00e9iert.", - "connection_error": "Feeler beim verbannen mam Printer." + "connection_error": "Feeler beim verbannen mam Printer.", + "connection_upgrade": "Feeler beim verbannen mam Printer well eng Aktualis\u00e9ierung vun der Verbindung erfuerderlech ass." }, "error": { "connection_error": "Feeler beim verbannen mam Printer.", diff --git a/homeassistant/components/ipp/.translations/no.json b/homeassistant/components/ipp/.translations/no.json new file mode 100644 index 00000000000..2357aaaa86d --- /dev/null +++ b/homeassistant/components/ipp/.translations/no.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Denne skriveren er allerede konfigurert.", + "connection_error": "Klarte ikke \u00e5 koble til skriveren.", + "connection_upgrade": "Kunne ikke koble til skriveren fordi tilkoblingsoppgradering var n\u00f8dvendig." + }, + "error": { + "connection_error": "Klarte ikke \u00e5 koble til skriveren.", + "connection_upgrade": "Kunne ikke koble til skriveren. Vennligst pr\u00f8v igjen med alternativet SSL / TLS merket." + }, + "flow_title": "Skriver: {name}", + "step": { + "user": { + "data": { + "base_path": "Relativ bane til skriveren", + "host": "Vert eller IP-adresse", + "port": "Port", + "ssl": "Skriveren st\u00f8tter kommunikasjon over SSL/TLS", + "verify_ssl": "Skriveren bruker et riktig SSL-sertifikat" + }, + "description": "Konfigurer skriveren din via Internet Printing Protocol (IPP) for \u00e5 integrere med Home Assistant.", + "title": "Koble til skriveren din" + }, + "zeroconf_confirm": { + "description": "\u00d8nsker du \u00e5 legge skriveren med navnet {name} til Home Assistant?", + "title": "Oppdaget skriver" + } + }, + "title": "Internet Printing Protocol (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ca.json b/homeassistant/components/konnected/.translations/ca.json index fbfa9183941..d5fbb60ae71 100644 --- a/homeassistant/components/konnected/.translations/ca.json +++ b/homeassistant/components/konnected/.translations/ca.json @@ -95,7 +95,7 @@ "pause": "Pausa entre polsos (ms) (opcional)", "repeat": "Repeticions (-1 = infinit) (opcional)" }, - "description": "Selecciona les opcions de sortida per a {zone}", + "description": "Selecciona les opcions de sortida per a {zone}: estat {state}", "title": "Configuraci\u00f3 de sortida commutable" } }, diff --git a/homeassistant/components/konnected/.translations/de.json b/homeassistant/components/konnected/.translations/de.json index ffd8f3219fe..a0da84fd098 100644 --- a/homeassistant/components/konnected/.translations/de.json +++ b/homeassistant/components/konnected/.translations/de.json @@ -34,6 +34,7 @@ "not_konn_panel": "Kein anerkanntes Konnected.io-Ger\u00e4t" }, "error": { + "bad_host": "Ung\u00fcltige Override-API-Host-URL", "one": "eins", "other": "andere" }, @@ -85,6 +86,10 @@ "title": "Konfigurieren Sie Erweiterte I/O" }, "options_misc": { + "data": { + "api_host": "API-Host-URL \u00fcberschreiben (optional)", + "override_api_host": "\u00dcberschreiben Sie die Standard-Host-Panel-URL der Home Assistant-API" + }, "description": "Bitte w\u00e4hlen Sie das gew\u00fcnschte Verhalten f\u00fcr Ihr Panel" }, "options_switch": { diff --git a/homeassistant/components/konnected/.translations/lb.json b/homeassistant/components/konnected/.translations/lb.json index 12493169691..984e3b79f54 100644 --- a/homeassistant/components/konnected/.translations/lb.json +++ b/homeassistant/components/konnected/.translations/lb.json @@ -34,6 +34,7 @@ "not_konn_panel": "Keen erkannten Konnected.io Apparat" }, "error": { + "bad_host": "Iwwerschriwwen API Host URL ong\u00eblteg", "one": "Ee", "other": "M\u00e9i" }, @@ -86,7 +87,9 @@ }, "options_misc": { "data": { - "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt" + "api_host": "API Host URL iwwerschr\u00e9iwen (optionell)", + "blink": "Blink panel LED un wann Status \u00c4nnerung gesch\u00e9ckt g\u00ebtt", + "override_api_host": "Standard Home Assistant API Host Tableau URL iwwerschr\u00e9iwen" }, "description": "Wielt w.e.g. dat gew\u00ebnschte Verhalen fir \u00c4re Panel aus", "title": "Divers Optioune astellen" @@ -95,6 +98,7 @@ "data": { "activation": "Ausgang wann un", "momentary": "Pulsatiounsdauer (ms) (optional)", + "more_states": "Zous\u00e4tzlesch Zoust\u00e4nn fir d\u00ebs Zon konfigur\u00e9ieren", "name": "Numm (optional)", "pause": "Pausen zw\u00ebscht den Impulser (ms) (optional)", "repeat": "Unzuel vu Widderhuelungen (-1= onendlech) (optional)" diff --git a/homeassistant/components/konnected/.translations/no.json b/homeassistant/components/konnected/.translations/no.json index 71c0fa1de6e..86d9fe877af 100644 --- a/homeassistant/components/konnected/.translations/no.json +++ b/homeassistant/components/konnected/.translations/no.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "Ikke en anerkjent Konnected.io-enhet" }, + "error": { + "bad_host": "Ugyldig overstyr API-vertsadresse" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring" + "api_host": "Overstyre API-vert-URL (valgfritt)", + "blink": "Blink p\u00e5 LED-lampen n\u00e5r du sender statusendring", + "override_api_host": "Overstyre standard Home Assistant API-vertspanel-URL" }, "description": "Vennligst velg \u00f8nsket atferd for din panel", "title": "Konfigurere Diverse" diff --git a/homeassistant/components/konnected/.translations/zh-Hant.json b/homeassistant/components/konnected/.translations/zh-Hant.json index 851dac76195..9c3e818e692 100644 --- a/homeassistant/components/konnected/.translations/zh-Hant.json +++ b/homeassistant/components/konnected/.translations/zh-Hant.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "\u4e26\u975e\u53ef\u8b58\u5225 Konnected.io \u8a2d\u5099" }, + "error": { + "bad_host": "\u7121\u6548\u7684\u8986\u5beb API \u4e3b\u6a5f\u7aef URL" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED" + "api_host": "\u8986\u5beb API \u4e3b\u6a5f\u7aef URL\uff08\u9078\u9805\uff09", + "blink": "\u7576\u50b3\u9001\u72c0\u614b\u8b8a\u66f4\u6642\u3001\u9583\u720d\u9762\u677f LED", + "override_api_host": "\u8986\u5beb\u9810\u8a2d Home Assistant API \u4e3b\u6a5f\u7aef\u9762\u677f URL" }, "description": "\u8acb\u9078\u64c7\u9762\u677f\u671f\u671b\u884c\u70ba", "title": "\u5176\u4ed6\u8a2d\u5b9a" diff --git a/homeassistant/components/nut/.translations/ca.json b/homeassistant/components/nut/.translations/ca.json new file mode 100644 index 00000000000..33d2268be5b --- /dev/null +++ b/homeassistant/components/nut/.translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositiu ja est\u00e0 configurat" + }, + "error": { + "unknown": "Error inesperat" + }, + "step": { + "user": { + "data": { + "alias": "\u00c0lies", + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "resources": "Recursos", + "username": "Nom d'usuari" + } + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Recursos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/da.json b/homeassistant/components/nut/.translations/da.json new file mode 100644 index 00000000000..3e66091d851 --- /dev/null +++ b/homeassistant/components/nut/.translations/da.json @@ -0,0 +1,12 @@ +{ + "config": { + "step": { + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/de.json b/homeassistant/components/nut/.translations/de.json new file mode 100644 index 00000000000..611db3acfd6 --- /dev/null +++ b/homeassistant/components/nut/.translations/de.json @@ -0,0 +1,35 @@ +{ + "config": { + "abort": { + "already_configured": "Ger\u00e4t ist bereits konfiguriert" + }, + "error": { + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", + "unknown": "Unerwarteter Fehler" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Name", + "password": "Passwort", + "port": "Port", + "resources": "Ressourcen", + "username": "Benutzername" + }, + "title": "Stellen Sie eine Verbindung zum NUT-Server her" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Ressourcen" + }, + "description": "W\u00e4hlen Sie Sensorressourcen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/en.json b/homeassistant/components/nut/.translations/en.json index e37a019af78..66ea276eca0 100644 --- a/homeassistant/components/nut/.translations/en.json +++ b/homeassistant/components/nut/.translations/en.json @@ -1,38 +1,37 @@ { - "config": { - "title": "Network UPS Tools (NUT)", - "step": { - "user": { - "title": "Connect to the NUT server", - "description": "If there are multiple UPSs attached to the NUT server, enter the name UPS to query in the 'Alias' field.", - "data": { - "name": "Name", - "host": "Host", - "port": "Port", - "alias": "Alias", - "username": "Username", - "password": "Password", - "resources": "Resources" - } - } + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect, please try again", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port", + "resources": "Resources", + "username": "Username" + }, + "description": "If there are multiple UPSs attached to the NUT server, enter the name UPS to query in the 'Alias' field.", + "title": "Connect to the NUT server" + } + }, + "title": "Network UPS Tools (NUT)" }, - "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" - }, - "abort": { - "already_configured": "Device is already configured" - } - }, - "options": { - "step": { - "init": { - "description": "Choose Sensor Resources", - "data": { - "resources": "Resources" + "options": { + "step": { + "init": { + "data": { + "resources": "Resources" + }, + "description": "Choose Sensor Resources" + } } - } } - } - } \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/es.json b/homeassistant/components/nut/.translations/es.json new file mode 100644 index 00000000000..34944816c81 --- /dev/null +++ b/homeassistant/components/nut/.translations/es.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, por favor, int\u00e9ntalo de nuevo", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "resources": "Recursos", + "username": "Usuario" + }, + "description": "Si hay varios UPS conectados al servidor NUT, introduzca el nombre UPS a buscar en el campo 'Alias'.", + "title": "Conectar con el servidor NUT" + } + }, + "title": "Herramientas de UPS de red (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Recursos" + }, + "description": "Elegir Recursos del Sensor" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/lb.json b/homeassistant/components/nut/.translations/lb.json new file mode 100644 index 00000000000..416d5c49aee --- /dev/null +++ b/homeassistant/components/nut/.translations/lb.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", + "unknown": "Onerwaarte Feeler" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "resources": "Ressourcen", + "username": "Benotzernumm" + }, + "title": "Mam NUT Server verbannen" + } + }, + "title": "Network UPS Tools (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Ressourcen" + }, + "description": "Sensor Ressourcen auswielen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/no.json b/homeassistant/components/nut/.translations/no.json new file mode 100644 index 00000000000..31fc3e513c1 --- /dev/null +++ b/homeassistant/components/nut/.translations/no.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Vert", + "name": "Navn", + "password": "Passord", + "port": "Port", + "resources": "Ressurser", + "username": "Brukernavn" + }, + "description": "Hvis det er flere UPS-er knyttet til NUT-serveren, angir du navnet UPS for \u00e5 sp\u00f8rre i 'Alias' -feltet.", + "title": "Koble til NUT-serveren" + } + }, + "title": "Nettverk UPS-verkt\u00f8y (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Ressurser" + }, + "description": "Velg Sensorressurser" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/zh-Hant.json b/homeassistant/components/nut/.translations/zh-Hant.json new file mode 100644 index 00000000000..760a66ba1a5 --- /dev/null +++ b/homeassistant/components/nut/.translations/zh-Hant.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "alias": "\u5225\u540d", + "host": "\u4e3b\u6a5f\u7aef", + "name": "\u540d\u7a31", + "password": "\u5bc6\u78bc", + "port": "\u901a\u8a0a\u57e0", + "resources": "\u8cc7\u6e90", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + }, + "description": "\u5047\u5982 NUT \u4f3a\u670d\u5668\u4e0b\u64c1\u6709\u591a\u7d44 UPS\uff0c\u65bc\u300c\u5225\u540d\u300d\u6b04\u4f4d\u8f38\u5165 UPS \u540d\u7a31\u3002", + "title": "\u9023\u7dda\u81f3 NUT \u4f3a\u670d\u5668" + } + }, + "title": "Network UPS Tools (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\u8cc7\u6e90" + }, + "description": "\u9078\u64c7\u50b3\u611f\u5668\u8cc7\u6e90" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json index 034e2b3df00..79dfa120db2 100644 --- a/homeassistant/components/vizio/.translations/lb.json +++ b/homeassistant/components/vizio/.translations/lb.json @@ -37,7 +37,8 @@ "data": { "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" - } + }, + "title": "Apps fir Smart TV konfigur\u00e9ieren" }, "user": { "data": { @@ -53,7 +54,8 @@ "data": { "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" - } + }, + "title": "Apps fir Smart TV konfigur\u00e9ieren" } }, "title": "Vizio SmartCast" From e86fb3fc5cd4c3fa0acff466478b1317d358a067 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 1 Apr 2020 20:49:35 +0200 Subject: [PATCH 369/431] Bumped version to 0.108.0b0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 2f1cc75e4a5..0408a891286 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0.dev0" +PATCH_VERSION = "0b0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From ef5f4b2aca295fd8920d1dfdaab961a5844e7f1a Mon Sep 17 00:00:00 2001 From: Jonathan Keljo Date: Thu, 2 Apr 2020 07:33:54 -0700 Subject: [PATCH 370/431] Enable sisyphus to recover from bad DNS without restart (#32846) --- homeassistant/components/sisyphus/__init__.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/sisyphus/__init__.py b/homeassistant/components/sisyphus/__init__.py index 5ad59da5dee..841fbb68178 100644 --- a/homeassistant/components/sisyphus/__init__.py +++ b/homeassistant/components/sisyphus/__init__.py @@ -99,18 +99,23 @@ class TableHolder: async def get_table(self): """Return the Table held by this holder, connecting to it if needed.""" + if self._table: + return self._table + if not self._table_task: self._table_task = self._hass.async_create_task(self._connect_table()) return await self._table_task async def _connect_table(self): - - self._table = await Table.connect(self._host, self._session) - if self._name is None: - self._name = self._table.name - _LOGGER.debug("Connected to %s at %s", self._name, self._host) - return self._table + try: + self._table = await Table.connect(self._host, self._session) + if self._name is None: + self._name = self._table.name + _LOGGER.debug("Connected to %s at %s", self._name, self._host) + return self._table + finally: + self._table_task = None async def close(self): """Close the table held by this holder, if any.""" From 899e7bfb5a7cf441596cbd05b2aa81dbff37601a Mon Sep 17 00:00:00 2001 From: cgtobi Date: Thu, 2 Apr 2020 17:35:25 +0200 Subject: [PATCH 371/431] Fix netatmo device unavailable and services (#33509) * Handle unavailabe entities * Remove some logging * Set valve to lowest temp when turned off * Remove some logging * Address comments * Report entity as connected if update is successful * Fix stupidness * Fix --- homeassistant/components/netatmo/climate.py | 59 ++++++++++++++++----- 1 file changed, 45 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/netatmo/climate.py b/homeassistant/components/netatmo/climate.py index 1f1b7088b29..fe6526a16eb 100644 --- a/homeassistant/components/netatmo/climate.py +++ b/homeassistant/components/netatmo/climate.py @@ -56,6 +56,7 @@ STATE_NETATMO_MAX = "max" STATE_NETATMO_AWAY = PRESET_AWAY STATE_NETATMO_OFF = STATE_OFF STATE_NETATMO_MANUAL = "manual" +STATE_NETATMO_HOME = "home" PRESET_MAP_NETATMO = { PRESET_FROST_GUARD: STATE_NETATMO_HG, @@ -173,8 +174,11 @@ class NetatmoThermostat(ClimateDevice): self._support_flags = SUPPORT_FLAGS self._hvac_mode = None self._battery_level = None + self._connected = None self.update_without_throttle = False - self._module_type = self._data.room_status.get(room_id, {}).get("module_type") + self._module_type = self._data.room_status.get(room_id, {}).get( + "module_type", NA_VALVE + ) if self._module_type == NA_THERM: self._operation_list.append(HVAC_MODE_OFF) @@ -252,25 +256,20 @@ class NetatmoThermostat(ClimateDevice): def set_hvac_mode(self, hvac_mode: str) -> None: """Set new target hvac mode.""" - mode = None - if hvac_mode == HVAC_MODE_OFF: - mode = STATE_NETATMO_OFF + self.turn_off() elif hvac_mode == HVAC_MODE_AUTO: - mode = PRESET_SCHEDULE + if self.hvac_mode == HVAC_MODE_OFF: + self.turn_on() + self.set_preset_mode(PRESET_SCHEDULE) elif hvac_mode == HVAC_MODE_HEAT: - mode = PRESET_BOOST - - self.set_preset_mode(mode) + self.set_preset_mode(PRESET_BOOST) def set_preset_mode(self, preset_mode: str) -> None: """Set new preset mode.""" if self.target_temperature == 0: self._data.homestatus.setroomThermpoint( - self._data.home_id, - self._room_id, - STATE_NETATMO_MANUAL, - DEFAULT_MIN_TEMP, + self._data.home_id, self._room_id, STATE_NETATMO_HOME, ) if ( @@ -283,7 +282,7 @@ class NetatmoThermostat(ClimateDevice): STATE_NETATMO_MANUAL, DEFAULT_MAX_TEMP, ) - elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX, STATE_NETATMO_OFF]: + elif preset_mode in [PRESET_BOOST, STATE_NETATMO_MAX]: self._data.homestatus.setroomThermpoint( self._data.home_id, self._room_id, PRESET_MAP_NETATMO[preset_mode] ) @@ -293,6 +292,7 @@ class NetatmoThermostat(ClimateDevice): ) else: _LOGGER.error("Preset mode '%s' not available", preset_mode) + self.update_without_throttle = True self.schedule_update_ha_state() @@ -328,6 +328,35 @@ class NetatmoThermostat(ClimateDevice): return attr + def turn_off(self): + """Turn the entity off.""" + if self._module_type == NA_VALVE: + self._data.homestatus.setroomThermpoint( + self._data.home_id, + self._room_id, + STATE_NETATMO_MANUAL, + DEFAULT_MIN_TEMP, + ) + elif self.hvac_mode != HVAC_MODE_OFF: + self._data.homestatus.setroomThermpoint( + self._data.home_id, self._room_id, STATE_NETATMO_OFF + ) + self.update_without_throttle = True + self.schedule_update_ha_state() + + def turn_on(self): + """Turn the entity on.""" + self._data.homestatus.setroomThermpoint( + self._data.home_id, self._room_id, STATE_NETATMO_HOME + ) + self.update_without_throttle = True + self.schedule_update_ha_state() + + @property + def available(self) -> bool: + """If the device hasn't been able to connect, mark as unavailable.""" + return bool(self._connected) + def update(self): """Get the latest data from NetAtmo API and updates the states.""" try: @@ -355,12 +384,14 @@ class NetatmoThermostat(ClimateDevice): self._battery_level = self._data.room_status[self._room_id].get( "battery_level" ) + self._connected = True except KeyError as err: - _LOGGER.error( + _LOGGER.debug( "The thermostat in room %s seems to be out of reach. (%s)", self._room_name, err, ) + self._connected = False self._away = self._hvac_mode == HVAC_MAP_NETATMO[STATE_NETATMO_AWAY] From 9c224e051553a251682ce06fcd3d32317280bd93 Mon Sep 17 00:00:00 2001 From: AJ Schmidt Date: Thu, 2 Apr 2020 13:22:54 -0400 Subject: [PATCH 372/431] Remove extraneous parameter from AlarmDecoder services (#33516) --- homeassistant/components/alarmdecoder/services.yaml | 6 ------ 1 file changed, 6 deletions(-) diff --git a/homeassistant/components/alarmdecoder/services.yaml b/homeassistant/components/alarmdecoder/services.yaml index 12268d48bb7..1193f90ff8e 100644 --- a/homeassistant/components/alarmdecoder/services.yaml +++ b/homeassistant/components/alarmdecoder/services.yaml @@ -1,9 +1,6 @@ alarm_keypress: description: Send custom keypresses to the alarm. fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' keypress: description: 'String to send to the alarm panel.' example: '*71' @@ -11,9 +8,6 @@ alarm_keypress: alarm_toggle_chime: description: Send the alarm the toggle chime command. fields: - entity_id: - description: Name of the alarm control panel to trigger. - example: 'alarm_control_panel.downstairs' code: description: A required code to toggle the alarm control panel chime with. example: 1234 From 9b94d128ad32b2236169ed87f64dc3dea01d9347 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 2 Apr 2020 03:09:59 -0500 Subject: [PATCH 373/431] Update to roku==4.1.0 (#33520) * Update manifest.json * Update requirements_test_all.txt * Update requirements_all.txt --- homeassistant/components/roku/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/roku/manifest.json b/homeassistant/components/roku/manifest.json index e9cdb897115..43ccb6b8ad3 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": ["roku==4.0.0"], + "requirements": ["roku==4.1.0"], "dependencies": [], "ssdp": [ { diff --git a/requirements_all.txt b/requirements_all.txt index 0cbc59f76c4..70beb7ec314 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1807,7 +1807,7 @@ rjpl==0.3.5 rocketchat-API==0.6.1 # homeassistant.components.roku -roku==4.0.0 +roku==4.1.0 # homeassistant.components.roomba roombapy==1.4.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a5d9daf91d9..0c0cbe43485 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -668,7 +668,7 @@ rflink==0.0.52 ring_doorbell==0.6.0 # homeassistant.components.roku -roku==4.0.0 +roku==4.1.0 # homeassistant.components.yamaha rxv==0.6.0 From c529bcca9b6155f43059a1a295108f2fa6c14602 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Thu, 2 Apr 2020 13:52:03 +0200 Subject: [PATCH 374/431] Bump brother to 0.1.11 (#33526) --- homeassistant/components/brother/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/fixtures/brother_printer_data.json | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/brother/manifest.json b/homeassistant/components/brother/manifest.json index 24150c513df..7f48c7ee22c 100644 --- a/homeassistant/components/brother/manifest.json +++ b/homeassistant/components/brother/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/brother", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["brother==0.1.9"], + "requirements": ["brother==0.1.11"], "zeroconf": ["_printer._tcp.local."], "config_flow": true, "quality_scale": "platinum" diff --git a/requirements_all.txt b/requirements_all.txt index 70beb7ec314..9f91ded48d0 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -358,7 +358,7 @@ bravia-tv==1.0.1 broadlink==0.13.0 # homeassistant.components.brother -brother==0.1.9 +brother==0.1.11 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 0c0cbe43485..159cb04885e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -140,7 +140,7 @@ bomradarloop==0.1.4 broadlink==0.13.0 # homeassistant.components.brother -brother==0.1.9 +brother==0.1.11 # homeassistant.components.buienradar buienradar==1.0.4 diff --git a/tests/fixtures/brother_printer_data.json b/tests/fixtures/brother_printer_data.json index f4c36d988b1..a70d87673d0 100644 --- a/tests/fixtures/brother_printer_data.json +++ b/tests/fixtures/brother_printer_data.json @@ -71,5 +71,6 @@ "a60100a70100a0" ], "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.5.1.0": "0123456789", - "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING " + "1.3.6.1.4.1.2435.2.3.9.4.2.1.5.4.5.2.0": "WAITING ", + "1.3.6.1.2.1.43.7.1.1.4.1.1": "2004" } \ No newline at end of file From 252c724602cd580b35da5263c27fe150449abd5b Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Thu, 2 Apr 2020 18:52:46 +0200 Subject: [PATCH 375/431] Clarify light reproduce state deprecation warning (#33531) --- homeassistant/components/light/reproduce_state.py | 10 +++++++--- tests/components/light/test_reproduce_state.py | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index 59a4b0306d0..9a6b22b51a2 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -64,7 +64,10 @@ DEPRECATED_GROUP = [ ATTR_TRANSITION, ] -DEPRECATION_WARNING = "The use of other attributes than device state attributes is deprecated and will be removed in a future release. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" +DEPRECATION_WARNING = ( + "The use of other attributes than device state attributes is deprecated and will be removed in a future release. " + "Invalid attributes are %s. Read the logs for further details: https://www.home-assistant.io/integrations/scene/" +) async def _async_reproduce_state( @@ -84,8 +87,9 @@ async def _async_reproduce_state( return # Warn if deprecated attributes are used - if any(attr in DEPRECATED_GROUP for attr in state.attributes): - _LOGGER.warning(DEPRECATION_WARNING) + deprecated_attrs = [attr for attr in state.attributes if attr in DEPRECATED_GROUP] + if deprecated_attrs: + _LOGGER.warning(DEPRECATION_WARNING, deprecated_attrs) # Return if we are already at the right state. if cur_state.state == state.state and all( diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index 250a0fe26a8..1c40f352ff0 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -166,4 +166,4 @@ async def test_deprecation_warning(hass, caplog): [State("light.entity_off", "on", {"brightness_pct": 80})], blocking=True ) assert len(turn_on_calls) == 1 - assert DEPRECATION_WARNING in caplog.text + assert DEPRECATION_WARNING % ["brightness_pct"] in caplog.text From 08b0c1178be1421ba6abea283622efee54a69871 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Thu, 2 Apr 2020 18:52:05 +0200 Subject: [PATCH 376/431] Fix MQTT cleanup regression from #32184. (#33532) --- homeassistant/components/mqtt/__init__.py | 21 +++++++++++++-------- tests/ignore_uncaught_exceptions.py | 4 ---- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index bc59be0d1f3..734f67906ce 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -1162,7 +1162,7 @@ class MqttAvailability(Entity): async def cleanup_device_registry(hass, device_id): - """Remove device registry entry if there are no entities or triggers.""" + """Remove device registry entry if there are no remaining entities or triggers.""" # Local import to avoid circular dependencies from . import device_trigger @@ -1196,8 +1196,12 @@ class MqttDiscoveryUpdate(Entity): self._discovery_data[ATTR_DISCOVERY_HASH] if self._discovery_data else None ) - async def async_remove_from_registry(self) -> None: - """Remove entity from entity registry.""" + async def _async_remove_state_and_registry_entry(self) -> None: + """Remove entity's state and entity registry entry. + + Remove entity from entity registry if it is registered, this also removes the state. + If the entity is not in the entity registry, just remove the state. + """ entity_registry = ( await self.hass.helpers.entity_registry.async_get_registry() ) @@ -1205,6 +1209,8 @@ class MqttDiscoveryUpdate(Entity): entity_entry = entity_registry.async_get(self.entity_id) entity_registry.async_remove(self.entity_id) await cleanup_device_registry(self.hass, entity_entry.device_id) + else: + await self.async_remove() @callback async def discovery_callback(payload): @@ -1216,9 +1222,8 @@ class MqttDiscoveryUpdate(Entity): if not payload: # Empty payload: Remove component _LOGGER.info("Removing component: %s", self.entity_id) - self._cleanup_on_remove() - await async_remove_from_registry(self) - await self.async_remove() + self._cleanup_discovery_on_remove() + await _async_remove_state_and_registry_entry(self) elif self._discovery_update: # Non-empty payload: Notify component _LOGGER.info("Updating component: %s", self.entity_id) @@ -1246,9 +1251,9 @@ class MqttDiscoveryUpdate(Entity): async def async_will_remove_from_hass(self) -> None: """Stop listening to signal and cleanup discovery data..""" - self._cleanup_on_remove() + self._cleanup_discovery_on_remove() - def _cleanup_on_remove(self) -> None: + def _cleanup_discovery_on_remove(self) -> None: """Stop listening to signal and cleanup discovery data.""" if self._discovery_data and not self._removed_from_hass: debug_info.remove_entity_data(self.hass, self.entity_id) diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index 428de1a683c..df623a2fc20 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -68,10 +68,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ "tests.components.mqtt.test_init", "test_setup_with_tls_config_of_v1_under_python36_only_uses_v1", ), - ("tests.components.mqtt.test_light", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_light_json", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_light_template", "test_entity_device_info_remove"), - ("tests.components.mqtt.test_switch", "test_entity_device_info_remove"), ("tests.components.qwikswitch.test_init", "test_binary_sensor_device"), ("tests.components.qwikswitch.test_init", "test_sensor_device"), ("tests.components.rflink.test_init", "test_send_command_invalid_arguments"), From 96dc0319d89441ab4897003886c35285b017d99d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2020 11:46:10 -0500 Subject: [PATCH 377/431] Ensure harmony hub is ready before importing (#33537) If the harmony hub was not ready for connection or was busy when importing from yaml, the import validation would fail would not be retried. To mitigate this scenario we now do the validation in async_setup_platform which allows us to raise PlatformNotReady so we can retry later. --- .../components/harmony/config_flow.py | 58 +++++++------------ homeassistant/components/harmony/remote.py | 27 ++++++++- homeassistant/components/harmony/util.py | 44 +++++++++++++- tests/components/harmony/test_config_flow.py | 17 +++--- 4 files changed, 95 insertions(+), 51 deletions(-) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index ff7b47d6010..9d9c9dfb8e9 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -2,11 +2,9 @@ import logging from urllib.parse import urlparse -import aioharmony.exceptions as harmony_exceptions -from aioharmony.harmonyapi import HarmonyAPI import voluptuous as vol -from homeassistant import config_entries, core, exceptions +from homeassistant import config_entries, exceptions from homeassistant.components import ssdp from homeassistant.components.remote import ( ATTR_ACTIVITY, @@ -17,7 +15,11 @@ from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import callback from .const import DOMAIN, UNIQUE_ID -from .util import find_unique_id_for_remote +from .util import ( + find_best_name_for_remote, + find_unique_id_for_remote, + get_harmony_client_if_available, +) _LOGGER = logging.getLogger(__name__) @@ -26,43 +28,19 @@ DATA_SCHEMA = vol.Schema( ) -async def get_harmony_client_if_available(hass: core.HomeAssistant, ip_address): - """Connect to a harmony hub and fetch info.""" - harmony = HarmonyAPI(ip_address=ip_address) - - try: - if not await harmony.connect(): - await harmony.close() - return None - except harmony_exceptions.TimeOut: - return None - - await harmony.close() - - return harmony - - -async def validate_input(hass: core.HomeAssistant, data): +async def validate_input(data): """Validate the user input allows us to connect. Data has the keys from DATA_SCHEMA with values provided by the user. """ - harmony = await get_harmony_client_if_available(hass, data[CONF_HOST]) + harmony = await get_harmony_client_if_available(data[CONF_HOST]) if not harmony: raise CannotConnect - unique_id = find_unique_id_for_remote(harmony) - - # As a last resort we get the name from the harmony client - # in the event a name was not provided. harmony.name is - # usually the ip address but it can be an empty string. - if CONF_NAME not in data or data[CONF_NAME] is None or data[CONF_NAME] == "": - data[CONF_NAME] = harmony.name - return { - CONF_NAME: data[CONF_NAME], + CONF_NAME: find_best_name_for_remote(data, harmony), CONF_HOST: data[CONF_HOST], - UNIQUE_ID: unique_id, + UNIQUE_ID: find_unique_id_for_remote(harmony), } @@ -82,7 +60,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if user_input is not None: try: - validated = await validate_input(self.hass, user_input) + validated = await validate_input(user_input) except CannotConnect: errors["base"] = "cannot_connect" except Exception: # pylint: disable=broad-except @@ -116,9 +94,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_NAME: friendly_name, } - harmony = await get_harmony_client_if_available( - self.hass, self.harmony_config[CONF_HOST] - ) + harmony = await get_harmony_client_if_available(parsed_url.hostname) if harmony: unique_id = find_unique_id_for_remote(harmony) @@ -150,9 +126,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): }, ) - async def async_step_import(self, user_input): + async def async_step_import(self, validated_input): """Handle import.""" - return await self.async_step_user(user_input) + await self.async_set_unique_id(validated_input[UNIQUE_ID]) + self._abort_if_unique_id_configured() + # Everything was validated in remote async_setup_platform + # all we do now is create. + return await self._async_create_entry_from_valid_input( + validated_input, validated_input + ) @staticmethod @callback diff --git a/homeassistant/components/harmony/remote.py b/homeassistant/components/harmony/remote.py index 7d23e15a4e7..1d0ed66415c 100644 --- a/homeassistant/components/harmony/remote.py +++ b/homeassistant/components/harmony/remote.py @@ -24,6 +24,7 @@ from homeassistant.components.remote import ( from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import PlatformNotReady import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect @@ -33,6 +34,13 @@ from .const import ( HARMONY_OPTIONS_UPDATE, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, + UNIQUE_ID, +) +from .util import ( + find_best_name_for_remote, + find_matching_config_entries_for_host, + find_unique_id_for_remote, + get_harmony_client_if_available, ) _LOGGER = logging.getLogger(__name__) @@ -51,6 +59,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( extra=vol.ALLOW_EXTRA, ) + HARMONY_SYNC_SCHEMA = vol.Schema({vol.Optional(ATTR_ENTITY_ID): cv.entity_ids}) HARMONY_CHANGE_CHANNEL_SCHEMA = vol.Schema( @@ -68,9 +77,25 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= # Now handled by ssdp in the config flow return + if find_matching_config_entries_for_host(hass, config[CONF_HOST]): + return + + # We do the validation to verify we can connect + # so we can raise PlatformNotReady to force + # a retry so we can avoid a scenario where the config + # entry cannot be created via import because hub + # is not yet ready. + harmony = await get_harmony_client_if_available(config[CONF_HOST]) + if not harmony: + raise PlatformNotReady + + validated_config = config.copy() + validated_config[UNIQUE_ID] = find_unique_id_for_remote(harmony) + validated_config[CONF_NAME] = find_best_name_for_remote(config, harmony) + hass.async_create_task( hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_IMPORT}, data=config + DOMAIN, context={"source": SOURCE_IMPORT}, data=validated_config ) ) diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index 5f7e46510f9..69ed44cb7da 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -1,8 +1,13 @@ """The Logitech Harmony Hub integration utils.""" -from aioharmony.harmonyapi import HarmonyAPI as HarmonyClient +import aioharmony.exceptions as harmony_exceptions +from aioharmony.harmonyapi import HarmonyAPI + +from homeassistant.const import CONF_HOST, CONF_NAME + +from .const import DOMAIN -def find_unique_id_for_remote(harmony: HarmonyClient): +def find_unique_id_for_remote(harmony: HarmonyAPI): """Find the unique id for both websocket and xmpp clients.""" websocket_unique_id = harmony.hub_config.info.get("activeRemoteId") if websocket_unique_id is not None: @@ -10,3 +15,38 @@ def find_unique_id_for_remote(harmony: HarmonyClient): # fallback to the xmpp unique id if websocket is not available return harmony.config["global"]["timeStampHash"].split(";")[-1] + + +def find_best_name_for_remote(data: dict, harmony: HarmonyAPI): + """Find the best name from config or fallback to the remote.""" + # As a last resort we get the name from the harmony client + # in the event a name was not provided. harmony.name is + # usually the ip address but it can be an empty string. + if CONF_NAME not in data or data[CONF_NAME] is None or data[CONF_NAME] == "": + return harmony.name + + return data[CONF_NAME] + + +async def get_harmony_client_if_available(ip_address: str): + """Connect to a harmony hub and fetch info.""" + harmony = HarmonyAPI(ip_address=ip_address) + + try: + if not await harmony.connect(): + await harmony.close() + return None + except harmony_exceptions.TimeOut: + return None + + await harmony.close() + + return harmony + + +def find_matching_config_entries_for_host(hass, host): + """Search existing config entries for one matching the host.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if entry.data[CONF_HOST] == host: + return entry + return None diff --git a/tests/components/harmony/test_config_flow.py b/tests/components/harmony/test_config_flow.py index 18c0825b6a1..30421756d22 100644 --- a/tests/components/harmony/test_config_flow.py +++ b/tests/components/harmony/test_config_flow.py @@ -25,8 +25,7 @@ async def test_user_form(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( @@ -53,8 +52,7 @@ async def test_form_import(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( @@ -68,9 +66,11 @@ async def test_form_import(hass): "name": "friend", "activity": "Watch TV", "delay_secs": 0.9, + "unique_id": "555234534543", }, ) + assert result["result"].unique_id == "555234534543" assert result["type"] == "create_entry" assert result["title"] == "friend" assert result["data"] == { @@ -94,8 +94,7 @@ async def test_form_ssdp(hass): harmonyapi = _get_mock_harmonyapi(connect=True) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ): result = await hass.config_entries.flow.async_init( DOMAIN, @@ -114,8 +113,7 @@ async def test_form_ssdp(hass): } with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - return_value=harmonyapi, + "homeassistant.components.harmony.util.HarmonyAPI", return_value=harmonyapi, ), patch( "homeassistant.components.harmony.async_setup", return_value=True ) as mock_setup, patch( @@ -141,8 +139,7 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.harmony.config_flow.HarmonyAPI", - side_effect=CannotConnect, + "homeassistant.components.harmony.util.HarmonyAPI", side_effect=CannotConnect, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From 060c6c89e34789de4681179d552a7f4fa967e266 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2020 16:09:35 -0500 Subject: [PATCH 378/431] Bump HAP-python to 2.8.0 (#33539) --- homeassistant/components/homekit/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index bbbc6561a87..eb8d16d0c0a 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,7 +2,7 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.7.0"], + "requirements": ["HAP-python==2.8.0"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 9f91ded48d0..b13d21a1b9f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.7.0 +HAP-python==2.8.0 # homeassistant.components.mastodon Mastodon.py==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 159cb04885e..1267dad50e7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.7.0 +HAP-python==2.8.0 # homeassistant.components.mobile_app # homeassistant.components.owntracks From 8f233b822fd5caa2fd401a1a1bc13cfef095c1df Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Apr 2020 13:02:59 -0700 Subject: [PATCH 379/431] Mark new gate device class as 2FA (#33541) --- homeassistant/components/google_assistant/trait.py | 6 +++++- tests/components/google_assistant/test_trait.py | 3 ++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/google_assistant/trait.py b/homeassistant/components/google_assistant/trait.py index 1c47be45651..2bc5f5040d4 100644 --- a/homeassistant/components/google_assistant/trait.py +++ b/homeassistant/components/google_assistant/trait.py @@ -1247,7 +1247,11 @@ class OpenCloseTrait(_Trait): """ # Cover device classes that require 2FA - COVER_2FA = (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE) + COVER_2FA = ( + cover.DEVICE_CLASS_DOOR, + cover.DEVICE_CLASS_GARAGE, + cover.DEVICE_CLASS_GATE, + ) name = TRAIT_OPENCLOSE commands = [COMMAND_OPENCLOSE] diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index ec1848bf1ed..d0ed9a9d33c 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1532,7 +1532,8 @@ async def test_openclose_cover_no_position(hass): @pytest.mark.parametrize( - "device_class", (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE) + "device_class", + (cover.DEVICE_CLASS_DOOR, cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE), ) async def test_openclose_cover_secure(hass, device_class): """Test OpenClose trait support for cover domain.""" From a8da03912eb3238ff67afea5ffa613855e45839e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Apr 2020 18:47:58 -0500 Subject: [PATCH 380/431] Temporary Plex play_media workaround (#33542) * Temporary playMedia() workaround on plexapi 3.3.0 * Use constants for strings * Style cleanup --- homeassistant/components/plex/media_player.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 1be06876baf..5325544bf15 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -11,6 +11,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, + MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -575,9 +576,11 @@ class PlexMediaPlayer(MediaPlayerDevice): shuffle = src.get("shuffle", 0) media = None + command_media_type = MEDIA_TYPE_VIDEO if media_type == "MUSIC": media = self._get_music_media(library, src) + command_media_type = MEDIA_TYPE_MUSIC elif media_type == "EPISODE": media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": @@ -591,7 +594,7 @@ class PlexMediaPlayer(MediaPlayerDevice): playqueue = self.plex_server.create_playqueue(media, shuffle=shuffle) try: - self.device.playMedia(playqueue) + self.device.playMedia(playqueue, type=command_media_type) except ParseError: # Temporary workaround for Plexamp / plexapi issue pass From d47cef4ba29628314c6fc28c228af126acff8cb0 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 2 Apr 2020 15:46:31 -0500 Subject: [PATCH 381/431] Bump pyipp to 0.8.2 (#33544) --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index beb6679e308..2eae581bdc7 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.8.1"], + "requirements": ["pyipp==0.8.2"], "dependencies": [], "codeowners": ["@ctalkington"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index b13d21a1b9f..e9c6e902465 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyintesishome==1.7.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.1 +pyipp==0.8.2 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 1267dad50e7..4302a522eb5 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -519,7 +519,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.1 +pyipp==0.8.2 # homeassistant.components.iqvia pyiqvia==0.2.1 From 6f449cd383edbd4d3630297cb725f923732b2906 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Thu, 2 Apr 2020 19:09:38 -0500 Subject: [PATCH 382/431] Update to pyipp==0.8.3 (#33554) * Update manifest.json * Update requirements_all.txt * Update requirements_test_all.txt --- homeassistant/components/ipp/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 2eae581bdc7..0cb788eeee7 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,7 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.8.2"], + "requirements": ["pyipp==0.8.3"], "dependencies": [], "codeowners": ["@ctalkington"], "config_flow": true, diff --git a/requirements_all.txt b/requirements_all.txt index e9c6e902465..fa917932fbe 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyintesishome==1.7.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.2 +pyipp==0.8.3 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4302a522eb5..a4cf4e0176e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -519,7 +519,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.2 +pyipp==0.8.3 # homeassistant.components.iqvia pyiqvia==0.2.1 From e27d5cd9fbd4efd53b4681f4260f1c8af189ed60 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Apr 2020 17:12:52 -0700 Subject: [PATCH 383/431] Bumped version to 0.108.0b1 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0408a891286..1bf7a4f6441 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b0" +PATCH_VERSION = "0b1" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From e4e0c37a8c4a2b5735cce379568e4e7129b6d8f5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2020 20:06:13 -0500 Subject: [PATCH 384/431] Use homekit service callbacks for lights to resolve out of sync states (#32348) * Switch homekit lights to use service callbacks Service callbacks allow us to get the on/off, brightness, etc all in one call so we remove all the complexity that was previously needed to handle the out of sync states We now get the on event and brightness event at the same time which allows us to prevent lights from flashing up to 100% before the requested brightness. * Fix STATE_OFF -> STATE_ON,brightness:0 --- .../components/homekit/type_lights.py | 159 ++++----- tests/components/homekit/test_type_lights.py | 312 ++++++++++++++++-- 2 files changed, 343 insertions(+), 128 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 734568606b2..1720c2c58c8 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -25,7 +25,7 @@ from homeassistant.const import ( ) from . import TYPES -from .accessories import HomeAccessory, debounce +from .accessories import HomeAccessory from .const import ( CHAR_BRIGHTNESS, CHAR_COLOR_TEMPERATURE, @@ -52,15 +52,6 @@ class Light(HomeAccessory): def __init__(self, *args): """Initialize a new Light accessory object.""" super().__init__(*args, category=CATEGORY_LIGHTBULB) - self._flag = { - CHAR_ON: False, - CHAR_BRIGHTNESS: False, - CHAR_HUE: False, - CHAR_SATURATION: False, - CHAR_COLOR_TEMPERATURE: False, - RGB_COLOR: False, - } - self._state = 0 self.chars = [] self._features = self.hass.states.get(self.entity_id).attributes.get( @@ -82,17 +73,14 @@ class Light(HomeAccessory): self.chars.append(CHAR_COLOR_TEMPERATURE) serv_light = self.add_preload_service(SERV_LIGHTBULB, self.chars) - self.char_on = serv_light.configure_char( - CHAR_ON, value=self._state, setter_callback=self.set_state - ) + + self.char_on = serv_light.configure_char(CHAR_ON, value=0) if CHAR_BRIGHTNESS in self.chars: # Initial value is set to 100 because 0 is a special value (off). 100 is # an arbitrary non-zero value. It is updated immediately by update_state # to set to the correct initial value. - self.char_brightness = serv_light.configure_char( - CHAR_BRIGHTNESS, value=100, setter_callback=self.set_brightness - ) + self.char_brightness = serv_light.configure_char(CHAR_BRIGHTNESS, value=100) if CHAR_COLOR_TEMPERATURE in self.chars: min_mireds = self.hass.states.get(self.entity_id).attributes.get( @@ -105,133 +93,98 @@ class Light(HomeAccessory): CHAR_COLOR_TEMPERATURE, value=min_mireds, properties={PROP_MIN_VALUE: min_mireds, PROP_MAX_VALUE: max_mireds}, - setter_callback=self.set_color_temperature, ) if CHAR_HUE in self.chars: - self.char_hue = serv_light.configure_char( - CHAR_HUE, value=0, setter_callback=self.set_hue - ) + self.char_hue = serv_light.configure_char(CHAR_HUE, value=0) if CHAR_SATURATION in self.chars: - self.char_saturation = serv_light.configure_char( - CHAR_SATURATION, value=75, setter_callback=self.set_saturation - ) + self.char_saturation = serv_light.configure_char(CHAR_SATURATION, value=75) - def set_state(self, value): - """Set state if call came from HomeKit.""" - if self._state == value: - return + serv_light.setter_callback = self._set_chars - _LOGGER.debug("%s: Set state to %d", self.entity_id, value) - self._flag[CHAR_ON] = True + def _set_chars(self, char_values): + _LOGGER.debug("_set_chars: %s", char_values) + events = [] + service = SERVICE_TURN_ON params = {ATTR_ENTITY_ID: self.entity_id} - service = SERVICE_TURN_ON if value == 1 else SERVICE_TURN_OFF - self.call_service(DOMAIN, service, params) + if CHAR_ON in char_values: + if not char_values[CHAR_ON]: + service = SERVICE_TURN_OFF + events.append(f"Set state to {char_values[CHAR_ON]}") - @debounce - def set_brightness(self, value): - """Set brightness if call came from HomeKit.""" - _LOGGER.debug("%s: Set brightness to %d", self.entity_id, value) - self._flag[CHAR_BRIGHTNESS] = True - if value == 0: - self.set_state(0) # Turn off light - return - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_BRIGHTNESS_PCT: value} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"brightness at {value}%") + if CHAR_BRIGHTNESS in char_values: + if char_values[CHAR_BRIGHTNESS] == 0: + events[-1] = f"Set state to 0" + service = SERVICE_TURN_OFF + else: + params[ATTR_BRIGHTNESS_PCT] = char_values[CHAR_BRIGHTNESS] + events.append(f"brightness at {char_values[CHAR_BRIGHTNESS]}%") - def set_color_temperature(self, value): - """Set color temperature if call came from HomeKit.""" - _LOGGER.debug("%s: Set color temp to %s", self.entity_id, value) - self._flag[CHAR_COLOR_TEMPERATURE] = True - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_COLOR_TEMP: value} - self.call_service( - DOMAIN, SERVICE_TURN_ON, params, f"color temperature at {value}" - ) + if CHAR_COLOR_TEMPERATURE in char_values: + params[ATTR_COLOR_TEMP] = char_values[CHAR_COLOR_TEMPERATURE] + events.append(f"color temperature at {char_values[CHAR_COLOR_TEMPERATURE]}") - def set_saturation(self, value): - """Set saturation if call came from HomeKit.""" - _LOGGER.debug("%s: Set saturation to %d", self.entity_id, value) - self._flag[CHAR_SATURATION] = True - self._saturation = value - self.set_color() - - def set_hue(self, value): - """Set hue if call came from HomeKit.""" - _LOGGER.debug("%s: Set hue to %d", self.entity_id, value) - self._flag[CHAR_HUE] = True - self._hue = value - self.set_color() - - def set_color(self): - """Set color if call came from HomeKit.""" if ( self._features & SUPPORT_COLOR - and self._flag[CHAR_HUE] - and self._flag[CHAR_SATURATION] + and CHAR_HUE in char_values + and CHAR_SATURATION in char_values ): - color = (self._hue, self._saturation) + color = (char_values[CHAR_HUE], char_values[CHAR_SATURATION]) _LOGGER.debug("%s: Set hs_color to %s", self.entity_id, color) - self._flag.update( - {CHAR_HUE: False, CHAR_SATURATION: False, RGB_COLOR: True} - ) - params = {ATTR_ENTITY_ID: self.entity_id, ATTR_HS_COLOR: color} - self.call_service(DOMAIN, SERVICE_TURN_ON, params, f"set color at {color}") + params[ATTR_HS_COLOR] = color + events.append(f"set color at {color}") + + self.call_service(DOMAIN, service, params, ", ".join(events)) def update_state(self, new_state): """Update light after state change.""" # Handle State state = new_state.state - if state in (STATE_ON, STATE_OFF): - self._state = 1 if state == STATE_ON else 0 - if not self._flag[CHAR_ON] and self.char_on.value != self._state: - self.char_on.set_value(self._state) - self._flag[CHAR_ON] = False + if state == STATE_ON and self.char_on.value != 1: + self.char_on.set_value(1) + elif state == STATE_OFF and self.char_on.value != 0: + self.char_on.set_value(0) # Handle Brightness if CHAR_BRIGHTNESS in self.chars: brightness = new_state.attributes.get(ATTR_BRIGHTNESS) - if not self._flag[CHAR_BRIGHTNESS] and isinstance(brightness, int): + if isinstance(brightness, int): brightness = round(brightness / 255 * 100, 0) + # The homeassistant component might report its brightness as 0 but is + # not off. But 0 is a special value in homekit. When you turn on a + # homekit accessory it will try to restore the last brightness state + # which will be the last value saved by char_brightness.set_value. + # But if it is set to 0, HomeKit will update the brightness to 100 as + # it thinks 0 is off. + # + # Therefore, if the the brightness is 0 and the device is still on, + # the brightness is mapped to 1 otherwise the update is ignored in + # order to avoid this incorrect behavior. + if brightness == 0 and state == STATE_ON: + brightness = 1 if self.char_brightness.value != brightness: - # The homeassistant component might report its brightness as 0 but is - # not off. But 0 is a special value in homekit. When you turn on a - # homekit accessory it will try to restore the last brightness state - # which will be the last value saved by char_brightness.set_value. - # But if it is set to 0, HomeKit will update the brightness to 100 as - # it thinks 0 is off. - # - # Therefore, if the the brightness is 0 and the device is still on, - # the brightness is mapped to 1 otherwise the update is ignored in - # order to avoid this incorrect behavior. - if brightness == 0: - if state == STATE_ON: - self.char_brightness.set_value(1) - else: - self.char_brightness.set_value(brightness) - self._flag[CHAR_BRIGHTNESS] = False + self.char_brightness.set_value(brightness) # Handle color temperature if CHAR_COLOR_TEMPERATURE in self.chars: color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) if ( - not self._flag[CHAR_COLOR_TEMPERATURE] - and isinstance(color_temperature, int) + isinstance(color_temperature, int) and self.char_color_temperature.value != color_temperature ): self.char_color_temperature.set_value(color_temperature) - self._flag[CHAR_COLOR_TEMPERATURE] = False # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None)) if ( - not self._flag[RGB_COLOR] - and (hue != self._hue or saturation != self._saturation) - and isinstance(hue, (int, float)) + isinstance(hue, (int, float)) and isinstance(saturation, (int, float)) + and ( + hue != self.char_hue.value + or saturation != self.char_saturation.value + ) ): self.char_hue.set_value(hue) self.char_saturation.set_value(saturation) - self._hue, self._saturation = (hue, saturation) - self._flag[RGB_COLOR] = False diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 8834f730bce..888ad87a848 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -1,6 +1,9 @@ """Test different accessory types: Lights.""" from collections import namedtuple +from asynctest import patch +from pyhap.accessory_driver import AccessoryDriver +from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest from homeassistant.components.homekit.const import ATTR_VALUE @@ -30,6 +33,15 @@ from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce +@pytest.fixture +def driver(): + """Patch AccessoryDriver without zeroconf or HAPServer.""" + with patch("pyhap.accessory_driver.HAPServer"), patch( + "pyhap.accessory_driver.Zeroconf" + ), patch("pyhap.accessory_driver.AccessoryDriver.persist"): + yield AccessoryDriver() + + @pytest.fixture(scope="module") def cls(): """Patch debounce decorator during import of type_lights.""" @@ -43,15 +55,16 @@ def cls(): patcher.stop() -async def test_light_basic(hass, hk_driver, cls, events): +async def test_light_basic(hass, hk_driver, cls, events, driver): """Test light with char state.""" entity_id = "light.demo" hass.states.async_set(entity_id, STATE_ON, {ATTR_SUPPORTED_FEATURES: 0}) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) - assert acc.aid == 2 + assert acc.aid == 1 assert acc.category == 5 # Lightbulb assert acc.char_on.value == 0 @@ -75,25 +88,43 @@ async def test_light_basic(hass, hk_driver, cls, events): call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") call_turn_off = async_mock_service(hass, DOMAIN, "turn_off") + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1} + ] + }, + "mock_addr", + ) + await hass.async_add_job(acc.char_on.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 + assert events[-1].data[ATTR_VALUE] == "Set state to 1" hass.states.async_set(entity_id, STATE_ON) await hass.async_block_till_done() - await hass.async_add_job(acc.char_on.client_update_value, 0) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 0} + ] + }, + "mock_addr", + ) 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 + assert events[-1].data[ATTR_VALUE] == "Set state to 0" -async def test_light_brightness(hass, hk_driver, cls, events): +async def test_light_brightness(hass, hk_driver, cls, events, driver): """Test light with brightness.""" entity_id = "light.demo" @@ -103,11 +134,14 @@ async def test_light_brightness(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS, ATTR_BRIGHTNESS: 255}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the # brightness to 100 when turning on a light on a freshly booted up server. assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] await hass.async_add_job(acc.run) await hass.async_block_till_done() @@ -121,34 +155,88 @@ async def test_light_brightness(hass, hk_driver, cls, 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_job(acc.char_brightness.client_update_value, 20) - await hass.async_add_job(acc.char_on.client_update_value, 1) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_on[0] assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 assert len(events) == 1 - assert events[-1].data[ATTR_VALUE] == f"brightness at 20{UNIT_PERCENTAGE}" + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}" + ) - await hass.async_add_job(acc.char_on.client_update_value, 1) - await hass.async_add_job(acc.char_brightness.client_update_value, 40) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 40, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_on[1] assert call_turn_on[1].data[ATTR_ENTITY_ID] == entity_id assert call_turn_on[1].data[ATTR_BRIGHTNESS_PCT] == 40 assert len(events) == 2 - assert events[-1].data[ATTR_VALUE] == f"brightness at 40{UNIT_PERCENTAGE}" + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 40{UNIT_PERCENTAGE}" + ) - await hass.async_add_job(acc.char_on.client_update_value, 1) - await hass.async_add_job(acc.char_brightness.client_update_value, 0) + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 0, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_off assert call_turn_off[0].data[ATTR_ENTITY_ID] == entity_id assert len(events) == 3 - assert events[-1].data[ATTR_VALUE] is None + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 0, brightness at 0{UNIT_PERCENTAGE}" + ) + + # 0 is a special case for homekit, see "Handle Brightness" + # in update_state + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 1 + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 255}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 1 -async def test_light_color_temperature(hass, hk_driver, cls, events): +async def test_light_color_temperature(hass, hk_driver, cls, events, driver): """Test light with color temperature.""" entity_id = "light.demo" @@ -158,7 +246,8 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR_TEMP, ATTR_COLOR_TEMP: 190}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) assert acc.char_color_temperature.value == 153 @@ -169,6 +258,20 @@ async def test_light_color_temperature(hass, hk_driver, cls, events): # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_VALUE: 250, + } + ] + }, + "mock_addr", + ) await hass.async_add_job(acc.char_color_temperature.client_update_value, 250) await hass.async_block_till_done() assert call_turn_on @@ -197,7 +300,7 @@ async def test_light_color_temperature_and_rgb_color(hass, hk_driver, cls, event assert not hasattr(acc, "char_color_temperature") -async def test_light_rgb_color(hass, hk_driver, cls, events): +async def test_light_rgb_color(hass, hk_driver, cls, events, driver): """Test light with rgb_color.""" entity_id = "light.demo" @@ -207,7 +310,8 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): {ATTR_SUPPORTED_FEATURES: SUPPORT_COLOR, ATTR_HS_COLOR: (260, 90)}, ) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", entity_id, 2, None) + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) assert acc.char_hue.value == 0 assert acc.char_saturation.value == 75 @@ -220,8 +324,26 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") - await hass.async_add_job(acc.char_hue.client_update_value, 145) - await hass.async_add_job(acc.char_saturation.client_update_value, 75) + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) await hass.async_block_till_done() assert call_turn_on assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id @@ -230,7 +352,7 @@ async def test_light_rgb_color(hass, hk_driver, cls, events): assert events[-1].data[ATTR_VALUE] == "set color at (145, 75)" -async def test_light_restore(hass, hk_driver, cls, events): +async def test_light_restore(hass, hk_driver, cls, events, driver): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running @@ -250,7 +372,9 @@ async def test_light_restore(hass, hk_driver, cls, events): hass.bus.async_fire(EVENT_HOMEASSISTANT_START, {}) await hass.async_block_till_done() - acc = cls.light(hass, hk_driver, "Light", "light.simple", 2, None) + acc = cls.light(hass, hk_driver, "Light", "light.simple", 1, None) + driver.add_accessory(acc) + assert acc.category == 5 # Lightbulb assert acc.chars == [] assert acc.char_on.value == 0 @@ -259,3 +383,141 @@ async def test_light_restore(hass, hk_driver, cls, events): assert acc.category == 5 # Lightbulb assert acc.chars == ["Brightness"] assert acc.char_on.value == 0 + + +async def test_light_set_brightness_and_color(hass, hk_driver, cls, events, driver): + """Test light with all chars in one go.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, + ATTR_BRIGHTNESS: 255, + }, + ) + await hass.async_block_till_done() + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) + + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_hue_iid = acc.char_hue.to_HAP()[HAP_REPR_IID] + char_saturation_iid = acc.char_saturation.to_HAP()[HAP_REPR_IID] + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_hue_iid, + HAP_REPR_VALUE: 145, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_saturation_iid, + HAP_REPR_VALUE: 75, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert call_turn_on[0].data[ATTR_HS_COLOR] == (145, 75) + + assert len(events) == 1 + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, set color at (145, 75)" + ) + + +async def test_light_set_brightness_and_color_temp( + hass, hk_driver, cls, events, driver +): + """Test light with all chars in one go.""" + entity_id = "light.demo" + + hass.states.async_set( + entity_id, + STATE_ON, + { + ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP, + ATTR_BRIGHTNESS: 255, + }, + ) + await hass.async_block_till_done() + acc = cls.light(hass, hk_driver, "Light", entity_id, 1, None) + driver.add_accessory(acc) + + # Initial value can be anything but 0. If it is 0, it might cause HomeKit to set the + # brightness to 100 when turning on a light on a freshly booted up server. + assert acc.char_brightness.value != 0 + char_on_iid = acc.char_on.to_HAP()[HAP_REPR_IID] + char_brightness_iid = acc.char_brightness.to_HAP()[HAP_REPR_IID] + char_color_temperature_iid = acc.char_color_temperature.to_HAP()[HAP_REPR_IID] + + await hass.async_add_job(acc.run) + await hass.async_block_till_done() + assert acc.char_brightness.value == 100 + + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 102}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 40 + + # Set from HomeKit + call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") + + driver.set_characteristics( + { + HAP_REPR_CHARS: [ + {HAP_REPR_AID: acc.aid, HAP_REPR_IID: char_on_iid, HAP_REPR_VALUE: 1}, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_brightness_iid, + HAP_REPR_VALUE: 20, + }, + { + HAP_REPR_AID: acc.aid, + HAP_REPR_IID: char_color_temperature_iid, + HAP_REPR_VALUE: 250, + }, + ] + }, + "mock_addr", + ) + await hass.async_block_till_done() + assert call_turn_on[0] + assert call_turn_on[0].data[ATTR_ENTITY_ID] == entity_id + assert call_turn_on[0].data[ATTR_BRIGHTNESS_PCT] == 20 + assert call_turn_on[0].data[ATTR_COLOR_TEMP] == 250 + + assert len(events) == 1 + assert ( + events[-1].data[ATTR_VALUE] + == f"Set state to 1, brightness at 20{UNIT_PERCENTAGE}, color temperature at 250" + ) From 254394ecab3411242c71a8d52b90ad8ad40dc03f Mon Sep 17 00:00:00 2001 From: ollo69 <60491700+ollo69@users.noreply.github.com> Date: Fri, 3 Apr 2020 15:02:48 +0200 Subject: [PATCH 385/431] Fix asuswrt network failure startup (#33485) * Fix network failure startup Fix for issue ##33284 - Asuswrt component fail at startup after power failure * Removed comment * Removed bare except * is_connected moved out try-catch * Removed pointless-string-statement * Raise PlatformNotReady on "not is_connected" * Removed unnecessary check * Revert "Removed unnecessary check" This reverts commit a2ccddab2c4b1ba441f1d7482d802d9774527a26. * Implemented custom retry mechanism * Fix new line missing * Fix formatting * Fix indent * Reviewed check * Recoded based on tibber implementation * Formatting review * Changes requested * Fix tests for setup retry * Updated missing test * Fixed check on Tests * Return false if not exception * Format correction --- homeassistant/components/asuswrt/__init__.py | 30 +++++++++++++++++-- .../components/asuswrt/test_device_tracker.py | 12 ++++++++ 2 files changed, 39 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/asuswrt/__init__.py b/homeassistant/components/asuswrt/__init__.py index a0eee38c3f8..446fe898aaa 100644 --- a/homeassistant/components/asuswrt/__init__.py +++ b/homeassistant/components/asuswrt/__init__.py @@ -14,6 +14,7 @@ from homeassistant.const import ( ) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.discovery import async_load_platform +from homeassistant.helpers.event import async_call_later _LOGGER = logging.getLogger(__name__) @@ -31,6 +32,9 @@ DEFAULT_SSH_PORT = 22 DEFAULT_INTERFACE = "eth0" DEFAULT_DNSMASQ = "/var/lib/misc" +FIRST_RETRY_TIME = 60 +MAX_RETRY_TIME = 900 + SECRET_GROUP = "Password or SSH Key" SENSOR_TYPES = ["upload_speed", "download_speed", "download", "upload"] @@ -59,7 +63,7 @@ CONFIG_SCHEMA = vol.Schema( ) -async def async_setup(hass, config): +async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): """Set up the asuswrt component.""" conf = config[DOMAIN] @@ -77,9 +81,29 @@ async def async_setup(hass, config): dnsmasq=conf[CONF_DNSMASQ], ) - await api.connection.async_connect() + try: + await api.connection.async_connect() + except OSError as ex: + _LOGGER.warning( + "Error [%s] connecting %s to %s. Will retry in %s seconds...", + str(ex), + DOMAIN, + conf[CONF_HOST], + retry_delay, + ) + + async def retry_setup(now): + """Retry setup if a error happens on asuswrt API.""" + await async_setup( + hass, config, retry_delay=min(2 * retry_delay, MAX_RETRY_TIME) + ) + + async_call_later(hass, retry_delay, retry_setup) + + return True + if not api.is_connected: - _LOGGER.error("Unable to setup component") + _LOGGER.error("Error connecting %s to %s.", DOMAIN, conf[CONF_HOST]) return False hass.data[DATA_ASUSWRT] = api diff --git a/tests/components/asuswrt/test_device_tracker.py b/tests/components/asuswrt/test_device_tracker.py index b91b815d58e..62e5ed891ff 100644 --- a/tests/components/asuswrt/test_device_tracker.py +++ b/tests/components/asuswrt/test_device_tracker.py @@ -24,6 +24,18 @@ async def test_password_or_pub_key_required(hass): assert not result +async def test_network_unreachable(hass): + """Test creating an AsusWRT scanner without a pass or pubkey.""" + with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: + AsusWrt().connection.async_connect = mock_coro_func(exception=OSError) + AsusWrt().is_connected = False + result = await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_HOST: "fake_host", CONF_USERNAME: "fake_user"}} + ) + assert result + assert hass.data.get(DATA_ASUSWRT, None) is None + + async def test_get_scanner_with_password_no_pubkey(hass): """Test creating an AsusWRT scanner with a password and no pubkey.""" with patch("homeassistant.components.asuswrt.AsusWrt") as AsusWrt: From cb5de0e090fb4610675e6aa39bfe4f20a5fe4abd Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 2 Apr 2020 09:55:34 -0700 Subject: [PATCH 386/431] Convert TTS tests to async (#33517) * Convert TTS tests to async * Address comments --- homeassistant/components/tts/__init__.py | 76 +- tests/common.py | 16 +- tests/components/tts/test_init.py | 941 +++++++++++------------ 3 files changed, 501 insertions(+), 532 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index 3a456dec531..d9d513198ce 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -133,7 +133,7 @@ async def async_setup(hass, config): hass, p_config, discovery_info ) else: - provider = await hass.async_add_job( + provider = await hass.async_add_executor_job( platform.get_engine, hass, p_config, discovery_info ) @@ -226,41 +226,17 @@ class SpeechManager: self.time_memory = time_memory self.base_url = base_url - def init_tts_cache_dir(cache_dir): - """Init cache folder.""" - if not os.path.isabs(cache_dir): - cache_dir = self.hass.config.path(cache_dir) - if not os.path.isdir(cache_dir): - _LOGGER.info("Create cache dir %s.", cache_dir) - os.mkdir(cache_dir) - return cache_dir - try: - self.cache_dir = await self.hass.async_add_job( - init_tts_cache_dir, cache_dir + self.cache_dir = await self.hass.async_add_executor_job( + _init_tts_cache_dir, self.hass, cache_dir ) except OSError as err: raise HomeAssistantError(f"Can't init cache dir {err}") - def get_cache_files(): - """Return a dict of given engine files.""" - cache = {} - - folder_data = os.listdir(self.cache_dir) - for file_data in folder_data: - record = _RE_VOICE_FILE.match(file_data) - if record: - key = KEY_PATTERN.format( - record.group(1), - record.group(2), - record.group(3), - record.group(4), - ) - cache[key.lower()] = file_data.lower() - return cache - try: - cache_files = await self.hass.async_add_job(get_cache_files) + cache_files = await self.hass.async_add_executor_job( + _get_cache_files, self.cache_dir + ) except OSError as err: raise HomeAssistantError(f"Can't read cache dir {err}") @@ -273,13 +249,13 @@ class SpeechManager: def remove_files(): """Remove files from filesystem.""" - for _, filename in self.file_cache.items(): + for filename in self.file_cache.values(): try: os.remove(os.path.join(self.cache_dir, filename)) except OSError as err: _LOGGER.warning("Can't remove cache file '%s': %s", filename, err) - await self.hass.async_add_job(remove_files) + await self.hass.async_add_executor_job(remove_files) self.file_cache = {} @callback @@ -312,6 +288,7 @@ class SpeechManager: merged_options.update(options) options = merged_options options = options or provider.default_options + if options is not None: invalid_opts = [ opt_name @@ -378,10 +355,10 @@ class SpeechManager: speech.write(data) try: - await self.hass.async_add_job(save_speech) + await self.hass.async_add_executor_job(save_speech) self.file_cache[key] = filename - except OSError: - _LOGGER.error("Can't write %s", filename) + except OSError as err: + _LOGGER.error("Can't write %s: %s", filename, err) async def async_file_to_mem(self, key): """Load voice from file cache into memory. @@ -400,7 +377,7 @@ class SpeechManager: return speech.read() try: - data = await self.hass.async_add_job(load_speech) + data = await self.hass.async_add_executor_job(load_speech) except OSError: del self.file_cache[key] raise HomeAssistantError(f"Can't read {voice_file}") @@ -506,11 +483,36 @@ class Provider: Return a tuple of file extension and data as bytes. """ - return await self.hass.async_add_job( + return await self.hass.async_add_executor_job( ft.partial(self.get_tts_audio, message, language, options=options) ) +def _init_tts_cache_dir(hass, cache_dir): + """Init cache folder.""" + if not os.path.isabs(cache_dir): + cache_dir = hass.config.path(cache_dir) + if not os.path.isdir(cache_dir): + _LOGGER.info("Create cache dir %s", cache_dir) + os.mkdir(cache_dir) + return cache_dir + + +def _get_cache_files(cache_dir): + """Return a dict of given engine files.""" + cache = {} + + folder_data = os.listdir(cache_dir) + for file_data in folder_data: + record = _RE_VOICE_FILE.match(file_data) + if record: + key = KEY_PATTERN.format( + record.group(1), record.group(2), record.group(3), record.group(4), + ) + cache[key.lower()] = file_data.lower() + return cache + + class TextToSpeechUrlView(HomeAssistantView): """TTS view to get a url to a generated speech file.""" diff --git a/tests/common.py b/tests/common.py index 8fdcc9b8f86..9790a8a7131 100644 --- a/tests/common.py +++ b/tests/common.py @@ -14,6 +14,8 @@ import threading from unittest.mock import MagicMock, Mock, patch import uuid +from aiohttp.test_utils import unused_port as get_test_instance_port # noqa + from homeassistant import auth, config_entries, core as ha, loader from homeassistant.auth import ( auth_store, @@ -37,7 +39,6 @@ from homeassistant.const import ( EVENT_PLATFORM_DISCOVERED, EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, - SERVER_PORT, STATE_OFF, STATE_ON, ) @@ -59,7 +60,6 @@ import homeassistant.util.dt as date_util from homeassistant.util.unit_system import METRIC_SYSTEM import homeassistant.util.yaml.loader as yaml_loader -_TEST_INSTANCE_PORT = SERVER_PORT _LOGGER = logging.getLogger(__name__) INSTANCES = [] CLIENT_ID = "https://example.com/app" @@ -217,18 +217,6 @@ async def async_test_home_assistant(loop): return hass -def get_test_instance_port(): - """Return unused port for running test instance. - - The socket that holds the default port does not get released when we stop - HA in a different test case. Until I have figured out what is going on, - let's run each test on a different port. - """ - global _TEST_INSTANCE_PORT - _TEST_INSTANCE_PORT += 1 - return _TEST_INSTANCE_PORT - - def async_mock_service(hass, domain, service, schema=None): """Set up a fake service & return a calls log list to this service.""" calls = [] diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 62c4bc3a065..ab5d562ffc8 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -1,14 +1,12 @@ """The tests for the TTS component.""" import ctypes import os -import shutil from unittest.mock import PropertyMock, patch import pytest -import requests +import yarl from homeassistant.components.demo.tts import DemoProvider -import homeassistant.components.http as http from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, ATTR_MEDIA_CONTENT_TYPE, @@ -17,15 +15,52 @@ from homeassistant.components.media_player.const import ( SERVICE_PLAY_MEDIA, ) import homeassistant.components.tts as tts -from homeassistant.setup import async_setup_component, setup_component +from homeassistant.components.tts import _get_cache_files +from homeassistant.setup import async_setup_component -from tests.common import ( - assert_setup_component, - get_test_home_assistant, - get_test_instance_port, - mock_service, - mock_storage, -) +from tests.common import assert_setup_component, async_mock_service + + +def relative_url(url): + """Convert an absolute url to a relative one.""" + return str(yarl.URL(url).relative()) + + +@pytest.fixture +def demo_provider(): + """Demo TTS provider.""" + return DemoProvider("en") + + +@pytest.fixture(autouse=True) +def mock_get_cache_files(): + """Mock the list TTS cache function.""" + with patch( + "homeassistant.components.tts._get_cache_files", return_value={} + ) as mock_cache_files: + yield mock_cache_files + + +@pytest.fixture(autouse=True) +def mock_init_cache_dir(): + """Mock the TTS cache dir in memory.""" + with patch( + "homeassistant.components.tts._init_tts_cache_dir", + side_effect=lambda hass, cache_dir: hass.config.path(cache_dir), + ) as mock_cache_dir: + yield mock_cache_dir + + +@pytest.fixture +def empty_cache_dir(tmp_path, mock_init_cache_dir, mock_get_cache_files): + """Mock the TTS cache dir with empty dir.""" + mock_init_cache_dir.side_effect = None + mock_init_cache_dir.return_value = str(tmp_path) + + # Restore original get cache files behavior, we're working with a real dir. + mock_get_cache_files.side_effect = _get_cache_files + + return tmp_path @pytest.fixture(autouse=True) @@ -38,239 +73,209 @@ def mutagen_mock(): yield -class TestTTS: - """Test the Google speech component.""" +async def test_setup_component_demo(hass): + """Set up the demo platform with defaults.""" + config = {tts.DOMAIN: {"platform": "demo"}} - def setup_method(self): - """Set up things to be run when tests are started.""" - self.hass = get_test_home_assistant() - self.demo_provider = DemoProvider("en") - self.default_tts_cache = self.hass.config.path(tts.DEFAULT_CACHE_DIR) - self.mock_storage = mock_storage() - self.mock_storage.__enter__() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - setup_component( - self.hass, - http.DOMAIN, - {http.DOMAIN: {http.CONF_SERVER_PORT: get_test_instance_port()}}, - ) + assert hass.services.has_service(tts.DOMAIN, "demo_say") + assert hass.services.has_service(tts.DOMAIN, "clear_cache") - def teardown_method(self): - """Stop everything that was started.""" - self.hass.stop() - self.mock_storage.__exit__(None, None, None) - if os.path.isdir(self.default_tts_cache): - shutil.rmtree(self.default_tts_cache) +async def test_setup_component_demo_no_access_cache_folder(hass, mock_init_cache_dir): + """Set up the demo platform with defaults.""" + config = {tts.DOMAIN: {"platform": "demo"}} - def test_setup_component_demo(self): - """Set up the demo platform with defaults.""" - config = {tts.DOMAIN: {"platform": "demo"}} + mock_init_cache_dir.side_effect = OSError(2, "No access") + assert not await async_setup_component(hass, tts.DOMAIN, config) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + assert not hass.services.has_service(tts.DOMAIN, "demo_say") + assert not hass.services.has_service(tts.DOMAIN, "clear_cache") - assert self.hass.services.has_service(tts.DOMAIN, "demo_say") - assert self.hass.services.has_service(tts.DOMAIN, "clear_cache") - @patch("os.mkdir", side_effect=OSError(2, "No access")) - def test_setup_component_demo_no_access_cache_folder(self, mock_mkdir): - """Set up the demo platform with defaults.""" - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_service(hass, empty_cache_dir): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - assert not setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - assert not self.hass.services.has_service(tts.DOMAIN, "demo_say") - assert not self.hass.services.has_service(tts.DOMAIN, "clear_cache") + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - def test_setup_component_and_test_service(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) - config = {tts.DOMAIN: {"platform": "demo"}} + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( + hass.config.api.base_url + ) + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() +async def test_setup_component_and_test_service_with_config_language( + hass, empty_cache_dir +): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - self.hass.config.api.base_url - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) + config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} - def test_setup_component_and_test_service_with_config_language(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( + hass.config.api.base_url + ) + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" + ).is_file() - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() +async def test_setup_component_and_test_service_with_wrong_conf_language(hass): + """Set up the demo platform and call service with wrong config.""" + config = {tts.DOMAIN: {"platform": "demo", "language": "ru"}} - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( - self.hass.config.api.base_url - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", - ) - ) + with assert_setup_component(0, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - def test_setup_component_and_test_service_with_wrong_conf_language(self): - """Set up the demo platform and call service with wrong config.""" - config = {tts.DOMAIN: {"platform": "demo", "language": "ru"}} - with assert_setup_component(0, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) +async def test_setup_component_and_test_service_with_service_language( + hass, empty_cache_dir +): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - def test_setup_component_and_test_service_with_service_language(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = {tts.DOMAIN: {"platform": "demo"}} - config = {tts.DOMAIN: {"platform": "demo"}} + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "de", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( + hass.config.api.base_url + ) + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3" + ).is_file() - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "de", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3".format( - self.hass.config.api.base_url - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", - ) - ) +async def test_setup_component_test_service_with_wrong_service_language( + hass, empty_cache_dir +): + """Set up the demo platform and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - def test_setup_component_test_service_with_wrong_service_language(self): - """Set up the demo platform and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = {tts.DOMAIN: {"platform": "demo"}} - config = {tts.DOMAIN: {"platform": "demo"}} + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "lang", + }, + blocking=True, + ) + assert len(calls) == 0 + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3" + ).is_file() - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "lang", - }, - ) - self.hass.block_till_done() - assert len(calls) == 0 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_lang_-_demo.mp3", - ) - ) +async def test_setup_component_and_test_service_with_service_options( + hass, empty_cache_dir +): + """Set up the demo platform and call service with options.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - def test_setup_component_and_test_service_with_service_options(self): - """Set up the demo platform and call service with options.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = {tts.DOMAIN: {"platform": "demo"}} - config = {tts.DOMAIN: {"platform": "demo"}} + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "de", + tts.ATTR_OPTIONS: {"voice": "alex"}, + }, + blocking=True, + ) + opt_hash = ctypes.c_size_t(hash(frozenset({"voice": "alex"}))).value - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "de", - tts.ATTR_OPTIONS: {"voice": "alex"}, - }, - ) - self.hass.block_till_done() + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( + hass.config.api.base_url, opt_hash + ) + assert ( + empty_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" + ).is_file() - opt_hash = ctypes.c_size_t(hash(frozenset({"voice": "alex"}))).value - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( - self.hass.config.api.base_url, opt_hash - ) - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( - opt_hash - ), - ) - ) +async def test_setup_component_and_test_with_service_options_def(hass, empty_cache_dir): + """Set up the demo platform and call service with default options.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - @patch( + config = {tts.DOMAIN: {"platform": "demo"}} + + with assert_setup_component(1, tts.DOMAIN), patch( "homeassistant.components.demo.tts.DemoProvider.default_options", new_callable=PropertyMock(return_value={"voice": "alex"}), - ) - def test_setup_component_and_test_with_service_options_def(self, def_mock): - """Set up the demo platform and call service with default options.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + ): + assert await async_setup_component(hass, tts.DOMAIN, config) - config = {tts.DOMAIN: {"platform": "demo"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( + await hass.services.async_call( tts.DOMAIN, "demo_say", { @@ -278,9 +283,8 @@ class TestTTS: tts.ATTR_MESSAGE: "There is someone at the door.", tts.ATTR_LANGUAGE: "de", }, + blocking=True, ) - self.hass.block_till_done() - opt_hash = ctypes.c_size_t(hash(frozenset({"voice": "alex"}))).value assert len(calls) == 1 @@ -288,362 +292,341 @@ class TestTTS: assert calls[0].data[ ATTR_MEDIA_CONTENT_ID ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{}_demo.mp3".format( - self.hass.config.api.base_url, opt_hash + hass.config.api.base_url, opt_hash ) assert os.path.isfile( os.path.join( - self.default_tts_cache, + empty_cache_dir, "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( opt_hash ), ) ) - def test_setup_component_and_test_service_with_service_options_wrong(self): - """Set up the demo platform and call service with wrong options.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_service_with_service_options_wrong( + hass, empty_cache_dir +): + """Set up the demo platform and call service with wrong options.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_LANGUAGE: "de", - tts.ATTR_OPTIONS: {"speed": 1}, - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_LANGUAGE: "de", + tts.ATTR_OPTIONS: {"speed": 1}, + }, + blocking=True, + ) + opt_hash = ctypes.c_size_t(hash(frozenset({"speed": 1}))).value - assert len(calls) == 0 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{0}_demo.mp3".format( - opt_hash - ), - ) - ) + assert len(calls) == 0 + assert not ( + empty_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de_{opt_hash}_demo.mp3" + ).is_file() - def test_setup_component_and_test_service_with_base_url_set(self): - """Set up the demo platform with ``base_url`` set and call service.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo", "base_url": "http://fnord"}} +async def test_setup_component_and_test_service_with_base_url_set(hass): + """Set up the demo platform with ``base_url`` set and call service.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo", "base_url": "http://fnord"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC - assert ( - calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord" - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - "_en_-_demo.mp3" - ) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MEDIA_TYPE_MUSIC + assert ( + calls[0].data[ATTR_MEDIA_CONTENT_ID] == "http://fnord" + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + "_en_-_demo.mp3" + ) - def test_setup_component_and_test_service_clear_cache(self): - """Set up the demo platform and call service clear cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_service_clear_cache(hass, empty_cache_dir): + """Set up the demo platform and call service clear cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - assert len(calls) == 1 - assert os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + # To make sure the file is persisted + await hass.async_block_till_done() + assert len(calls) == 1 + assert ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - self.hass.services.call(tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}) - self.hass.block_till_done() + await hass.services.async_call( + tts.DOMAIN, tts.SERVICE_CLEAR_CACHE, {}, blocking=True + ) - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - def test_setup_component_and_test_service_with_receive_voice(self): - """Set up the demo platform and call service and receive voice.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - config = {tts.DOMAIN: {"platform": "demo"}} +async def test_setup_component_and_test_service_with_receive_voice( + hass, demo_provider, hass_client +): + """Set up the demo platform and call service and receive voice.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + config = {tts.DOMAIN: {"platform": "demo"}} - self.hass.start() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + client = await hass_client() - assert len(calls) == 1 - req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) - _, demo_data = self.demo_provider.get_tts_audio("bla", "en") - demo_data = tts.SpeechManager.write_tags( - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - demo_data, - self.demo_provider, - "AI person is in front of your door.", - "en", - None, - ) - assert req.status_code == 200 - assert req.content == demo_data + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 - def test_setup_component_and_test_service_with_receive_voice_german(self): - """Set up the demo platform and call service and receive voice.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + req = await client.get(relative_url(calls[0].data[ATTR_MEDIA_CONTENT_ID])) + _, demo_data = demo_provider.get_tts_audio("bla", "en") + demo_data = tts.SpeechManager.write_tags( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", + demo_data, + demo_provider, + "AI person is in front of your door.", + "en", + None, + ) + assert req.status == 200 + assert await req.read() == demo_data - config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) +async def test_setup_component_and_test_service_with_receive_voice_german( + hass, demo_provider, hass_client +): + """Set up the demo platform and call service and receive voice.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - self.hass.start() + config = {tts.DOMAIN: {"platform": "demo", "language": "de"}} - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - assert len(calls) == 1 - req = requests.get(calls[0].data[ATTR_MEDIA_CONTENT_ID]) - _, demo_data = self.demo_provider.get_tts_audio("bla", "de") - demo_data = tts.SpeechManager.write_tags( - "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", - demo_data, - self.demo_provider, - "There is someone at the door.", - "de", - None, - ) - assert req.status_code == 200 - assert req.content == demo_data + client = await hass_client() - def test_setup_component_and_web_view_wrong_file(self): - """Set up the demo platform and receive wrong file from web.""" - config = {tts.DOMAIN: {"platform": "demo"}} + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + req = await client.get(relative_url(calls[0].data[ATTR_MEDIA_CONTENT_ID])) + _, demo_data = demo_provider.get_tts_audio("bla", "de") + demo_data = tts.SpeechManager.write_tags( + "42f18378fd4393d18c8dd11d03fa9563c1e54491_de_-_demo.mp3", + demo_data, + demo_provider, + "There is someone at the door.", + "de", + None, + ) + assert req.status == 200 + assert await req.read() == demo_data - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - self.hass.start() +async def test_setup_component_and_web_view_wrong_file(hass, hass_client): + """Set up the demo platform and receive wrong file from web.""" + config = {tts.DOMAIN: {"platform": "demo"}} - url = ( - "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - ).format(self.hass.config.api.base_url) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - req = requests.get(url) - assert req.status_code == 404 + client = await hass_client() - def test_setup_component_and_web_view_wrong_filename(self): - """Set up the demo platform and receive wrong filename from web.""" - config = {tts.DOMAIN: {"platform": "demo"}} + url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + req = await client.get(url) + assert req.status == 404 - self.hass.start() - url = ( - "{}/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" - ).format(self.hass.config.api.base_url) +async def test_setup_component_and_web_view_wrong_filename(hass, hass_client): + """Set up the demo platform and receive wrong filename from web.""" + config = {tts.DOMAIN: {"platform": "demo"}} - req = requests.get(url) - assert req.status_code == 404 + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - def test_setup_component_test_without_cache(self): - """Set up demo platform without cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + client = await hass_client() - config = {tts.DOMAIN: {"platform": "demo", "cache": False}} + url = "/api/tts_proxy/265944dsk32c1b2a621be5930510bb2cd_en_-_demo.mp3" - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + req = await client.get(url) + assert req.status == 404 - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) +async def test_setup_component_test_without_cache(hass, empty_cache_dir): + """Set up demo platform without cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - def test_setup_component_test_with_cache_call_service_without_cache(self): - """Set up demo platform with cache and call service without cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = {tts.DOMAIN: {"platform": "demo", "cache": False}} - config = {tts.DOMAIN: {"platform": "demo", "cache": True}} + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + }, + blocking=True, + ) + assert len(calls) == 1 + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - tts.ATTR_CACHE: False, - }, - ) - self.hass.block_till_done() - assert len(calls) == 1 - assert not os.path.isfile( - os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) - ) +async def test_setup_component_test_with_cache_call_service_without_cache( + hass, empty_cache_dir +): + """Set up demo platform with cache and call service without cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - def test_setup_component_test_with_cache_dir(self): - """Set up demo platform with cache and call service without cache.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) + config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - _, demo_data = self.demo_provider.get_tts_audio("bla", "en") - cache_file = os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) - os.mkdir(self.default_tts_cache) - with open(cache_file, "wb") as voice_file: - voice_file.write(demo_data) + await hass.services.async_call( + tts.DOMAIN, + "demo_say", + { + "entity_id": "media_player.something", + tts.ATTR_MESSAGE: "There is someone at the door.", + tts.ATTR_CACHE: False, + }, + blocking=True, + ) + assert len(calls) == 1 + assert not ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ).is_file() - config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) +async def test_setup_component_test_with_cache_dir( + hass, empty_cache_dir, demo_provider +): + """Set up demo platform with cache and call service without cache.""" + calls = async_mock_service(hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - with patch( - "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", - return_value=(None, None), - ): - self.hass.services.call( - tts.DOMAIN, - "demo_say", - { - "entity_id": "media_player.something", - tts.ATTR_MESSAGE: "There is someone at the door.", - }, - ) - self.hass.block_till_done() + _, demo_data = demo_provider.get_tts_audio("bla", "en") + cache_file = ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ) - assert len(calls) == 1 - assert calls[0].data[ - ATTR_MEDIA_CONTENT_ID - ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( - self.hass.config.api.base_url - ) + with open(cache_file, "wb") as voice_file: + voice_file.write(demo_data) - @patch( + config = {tts.DOMAIN: {"platform": "demo", "cache": True}} + + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + with patch( "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", return_value=(None, None), - ) - def test_setup_component_test_with_error_on_get_tts(self, tts_mock): - """Set up demo platform with wrong get_tts_audio.""" - calls = mock_service(self.hass, DOMAIN_MP, SERVICE_PLAY_MEDIA) - - config = {tts.DOMAIN: {"platform": "demo"}} - - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) - - self.hass.services.call( + ): + await hass.services.async_call( tts.DOMAIN, "demo_say", { "entity_id": "media_player.something", tts.ATTR_MESSAGE: "There is someone at the door.", }, + blocking=True, ) - self.hass.block_till_done() + assert len(calls) == 1 + assert calls[0].data[ + ATTR_MEDIA_CONTENT_ID + ] == "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3".format( + hass.config.api.base_url + ) - assert len(calls) == 0 - def test_setup_component_load_cache_retrieve_without_mem_cache(self): - """Set up component and load cache and get without mem cache.""" - _, demo_data = self.demo_provider.get_tts_audio("bla", "en") - cache_file = os.path.join( - self.default_tts_cache, - "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3", - ) +async def test_setup_component_test_with_error_on_get_tts(hass): + """Set up demo platform with wrong get_tts_audio.""" + config = {tts.DOMAIN: {"platform": "demo"}} - os.mkdir(self.default_tts_cache) - with open(cache_file, "wb") as voice_file: - voice_file.write(demo_data) + with assert_setup_component(1, tts.DOMAIN), patch( + "homeassistant.components.demo.tts.DemoProvider.get_tts_audio", + return_value=(None, None), + ): + assert await async_setup_component(hass, tts.DOMAIN, config) - config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - with assert_setup_component(1, tts.DOMAIN): - setup_component(self.hass, tts.DOMAIN, config) +async def test_setup_component_load_cache_retrieve_without_mem_cache( + hass, demo_provider, empty_cache_dir, hass_client +): + """Set up component and load cache and get without mem cache.""" + _, demo_data = demo_provider.get_tts_audio("bla", "en") + cache_file = ( + empty_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + ) - self.hass.start() + with open(cache_file, "wb") as voice_file: + voice_file.write(demo_data) - url = ( - "{}/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" - ).format(self.hass.config.api.base_url) + config = {tts.DOMAIN: {"platform": "demo", "cache": True}} - req = requests.get(url) - assert req.status_code == 200 - assert req.content == demo_data + with assert_setup_component(1, tts.DOMAIN): + assert await async_setup_component(hass, tts.DOMAIN, config) + + client = await hass_client() + + url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_demo.mp3" + + req = await client.get(url) + assert req.status == 200 + assert await req.read() == demo_data async def test_setup_component_and_web_get_url(hass, hass_client): @@ -666,10 +649,6 @@ async def test_setup_component_and_web_get_url(hass, hass_client): ) ) - tts_cache = hass.config.path(tts.DEFAULT_CACHE_DIR) - if os.path.isdir(tts_cache): - shutil.rmtree(tts_cache) - async def test_setup_component_and_web_get_url_bad_config(hass, hass_client): """Set up the demo platform and receive wrong file from web.""" From 4eafd8adf73e61373c1410e326efcb0193ba8784 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 2 Apr 2020 23:02:50 -0500 Subject: [PATCH 387/431] Add missing flow_title to doorbird (#33557) When placeholders are in use, flow_title needs to be set in the json to prevent an empty name in the integrations dashboard. This affected doorbirds that were found via ssdp. --- homeassistant/components/doorbird/.translations/en.json | 5 +++-- homeassistant/components/doorbird/strings.json | 1 + 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index 27c16fac3a1..f933b9c9929 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -21,7 +21,8 @@ "title": "Connect to the DoorBird" } }, - "title": "DoorBird" + "title": "DoorBird", + "flow_title" : "DoorBird {name} ({host})" }, "options": { "step": { @@ -33,4 +34,4 @@ } } } -} \ No newline at end of file +} diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index 9b2c95dd7c9..e4fb72db91b 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -27,6 +27,7 @@ "not_doorbird_device": "This device is not a DoorBird" }, "title" : "DoorBird", + "flow_title" : "DoorBird {name} ({host})", "error" : { "invalid_auth" : "Invalid authentication", "unknown" : "Unexpected error", From ff3bfade31a5c2023d808e06333e4551ebefa128 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Thu, 2 Apr 2020 23:02:27 -0500 Subject: [PATCH 388/431] Plex followup to #33542 (#33558) --- homeassistant/components/plex/const.py | 3 +++ homeassistant/components/plex/media_player.py | 7 ++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index d5cb3db3aba..44bb25b3fd9 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -38,3 +38,6 @@ X_PLEX_DEVICE_NAME = "Home Assistant" X_PLEX_PLATFORM = "Home Assistant" X_PLEX_PRODUCT = "Home Assistant" X_PLEX_VERSION = __version__ + +COMMAND_MEDIA_TYPE_MUSIC = "music" +COMMAND_MEDIA_TYPE_VIDEO = "video" diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index 5325544bf15..aea8ecadaff 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -11,7 +11,6 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MOVIE, MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, - MEDIA_TYPE_VIDEO, SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, SUPPORT_PLAY, @@ -28,6 +27,8 @@ from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.util import dt as dt_util from .const import ( + COMMAND_MEDIA_TYPE_MUSIC, + COMMAND_MEDIA_TYPE_VIDEO, COMMON_PLAYERS, CONF_SERVER_IDENTIFIER, DISPATCHERS, @@ -576,11 +577,11 @@ class PlexMediaPlayer(MediaPlayerDevice): shuffle = src.get("shuffle", 0) media = None - command_media_type = MEDIA_TYPE_VIDEO + command_media_type = COMMAND_MEDIA_TYPE_VIDEO if media_type == "MUSIC": media = self._get_music_media(library, src) - command_media_type = MEDIA_TYPE_MUSIC + command_media_type = COMMAND_MEDIA_TYPE_MUSIC elif media_type == "EPISODE": media = self._get_tv_media(library, src) elif media_type == "PLAYLIST": From ef28bcaa9ce4572cd047afdf847689c931ce6f76 Mon Sep 17 00:00:00 2001 From: Eugenio Panadero Date: Fri, 3 Apr 2020 10:11:08 +0200 Subject: [PATCH 389/431] Identify cameras in error logs for generic and mjpeg cameras (#33561) --- homeassistant/components/generic/camera.py | 10 +++++++--- homeassistant/components/mjpeg/camera.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/generic/camera.py b/homeassistant/components/generic/camera.py index 3abeab32262..768ef108969 100644 --- a/homeassistant/components/generic/camera.py +++ b/homeassistant/components/generic/camera.py @@ -132,7 +132,9 @@ class GenericCamera(Camera): ) return response.content except requests.exceptions.RequestException as error: - _LOGGER.error("Error getting camera image: %s", error) + _LOGGER.error( + "Error getting new camera image from %s: %s", self._name, error + ) return self._last_image self._last_image = await self.hass.async_add_job(fetch) @@ -146,10 +148,12 @@ class GenericCamera(Camera): response = await websession.get(url, auth=self._auth) self._last_image = await response.read() except asyncio.TimeoutError: - _LOGGER.error("Timeout getting image from: %s", self._name) + _LOGGER.error("Timeout getting camera image from %s", self._name) return self._last_image except aiohttp.ClientError as err: - _LOGGER.error("Error getting new camera image: %s", err) + _LOGGER.error( + "Error getting new camera image from %s: %s", self._name, err + ) return self._last_image self._last_url = url diff --git a/homeassistant/components/mjpeg/camera.py b/homeassistant/components/mjpeg/camera.py index ab0409694d1..c42901cd6c5 100644 --- a/homeassistant/components/mjpeg/camera.py +++ b/homeassistant/components/mjpeg/camera.py @@ -122,10 +122,10 @@ class MjpegCamera(Camera): return image except asyncio.TimeoutError: - _LOGGER.error("Timeout getting camera image") + _LOGGER.error("Timeout getting camera image from %s", self._name) except aiohttp.ClientError as err: - _LOGGER.error("Error getting new camera image: %s", err) + _LOGGER.error("Error getting new camera image from %s: %s", self._name, err) def camera_image(self): """Return a still image response from the camera.""" From af10cd315e53b1f1496d666e9e44ba6b25834053 Mon Sep 17 00:00:00 2001 From: Fabian Affolter Date: Fri, 3 Apr 2020 11:13:48 +0200 Subject: [PATCH 390/431] Upgrade luftdaten to 0.6.4 (#33564) --- homeassistant/components/luftdaten/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/luftdaten/manifest.json b/homeassistant/components/luftdaten/manifest.json index 13fa67a8b6b..e6e9110b33a 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.3"], + "requirements": ["luftdaten==0.6.4"], "dependencies": [], "codeowners": ["@fabaff"], "quality_scale": "gold" diff --git a/requirements_all.txt b/requirements_all.txt index fa917932fbe..43429a19b78 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -840,7 +840,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.3 +luftdaten==0.6.4 # homeassistant.components.lupusec lupupy==0.0.18 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index a4cf4e0176e..4bb18c8cf7a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -329,7 +329,7 @@ libsoundtouch==0.7.2 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.3 +luftdaten==0.6.4 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 From aa6520cac11a39c011cad371ec4792ab4021d891 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 3 Apr 2020 16:10:14 +0200 Subject: [PATCH 391/431] Fix source name (#33565) --- homeassistant/components/braviatv/media_player.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/braviatv/media_player.py b/homeassistant/components/braviatv/media_player.py index 6dd431aac69..f9362799224 100644 --- a/homeassistant/components/braviatv/media_player.py +++ b/homeassistant/components/braviatv/media_player.py @@ -218,8 +218,8 @@ class BraviaTVDevice(MediaPlayerDevice): self._channel_name = playing_info.get("title") self._program_media_type = playing_info.get("programMediaType") self._channel_number = playing_info.get("dispNum") - self._source = playing_info.get("source") self._content_uri = playing_info.get("uri") + self._source = self._get_source() self._duration = playing_info.get("durationSec") self._start_date_time = playing_info.get("startDateTime") else: @@ -229,6 +229,12 @@ class BraviaTVDevice(MediaPlayerDevice): _LOGGER.error(exception_instance) self._state = STATE_OFF + def _get_source(self): + """Return the name of the source.""" + for key, value in self._content_mapping.items(): + if value == self._content_uri: + return key + def _reset_playing_info(self): self._program_name = None self._channel_name = None From a7e5cc31c37abe802015ecf8eda43af39a278465 Mon Sep 17 00:00:00 2001 From: Maciej Bieniek Date: Fri, 3 Apr 2020 11:58:37 +0200 Subject: [PATCH 392/431] Bump gios library to version 0.1.1 (#33569) --- homeassistant/components/gios/__init__.py | 9 +++++++-- homeassistant/components/gios/config_flow.py | 11 ++--------- homeassistant/components/gios/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 5 files changed, 12 insertions(+), 14 deletions(-) diff --git a/homeassistant/components/gios/__init__.py b/homeassistant/components/gios/__init__.py index 0a7973709c1..c7e708e3207 100644 --- a/homeassistant/components/gios/__init__.py +++ b/homeassistant/components/gios/__init__.py @@ -3,7 +3,7 @@ import logging from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout -from gios import ApiError, Gios, NoStationError +from gios import ApiError, Gios, InvalidSensorsData, NoStationError from homeassistant.core import Config, HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady @@ -63,7 +63,12 @@ class GiosDataUpdateCoordinator(DataUpdateCoordinator): try: with timeout(30): await self.gios.update() - except (ApiError, NoStationError, ClientConnectorError) as error: + except ( + ApiError, + NoStationError, + ClientConnectorError, + InvalidSensorsData, + ) as error: raise UpdateFailed(error) if not self.gios.data: raise UpdateFailed("Invalid sensors data") diff --git a/homeassistant/components/gios/config_flow.py b/homeassistant/components/gios/config_flow.py index 368d610c226..5741af47a07 100644 --- a/homeassistant/components/gios/config_flow.py +++ b/homeassistant/components/gios/config_flow.py @@ -3,10 +3,10 @@ import asyncio from aiohttp.client_exceptions import ClientConnectorError from async_timeout import timeout -from gios import ApiError, Gios, NoStationError +from gios import ApiError, Gios, InvalidSensorsData, NoStationError import voluptuous as vol -from homeassistant import config_entries, exceptions +from homeassistant import config_entries from homeassistant.const import CONF_NAME from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -43,9 +43,6 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): gios = Gios(user_input[CONF_STATION_ID], websession) await gios.update() - if not gios.available: - raise InvalidSensorsData() - return self.async_create_entry( title=user_input[CONF_STATION_ID], data=user_input, ) @@ -59,7 +56,3 @@ class GiosFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=DATA_SCHEMA, errors=errors ) - - -class InvalidSensorsData(exceptions.HomeAssistantError): - """Error to indicate invalid sensors data.""" diff --git a/homeassistant/components/gios/manifest.json b/homeassistant/components/gios/manifest.json index 67fcbebe9a2..3e3d63965d3 100644 --- a/homeassistant/components/gios/manifest.json +++ b/homeassistant/components/gios/manifest.json @@ -4,6 +4,6 @@ "documentation": "https://www.home-assistant.io/integrations/gios", "dependencies": [], "codeowners": ["@bieniu"], - "requirements": ["gios==0.0.5"], + "requirements": ["gios==0.1.1"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 43429a19b78..2ef389a280c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -611,7 +611,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.5 +gios==0.1.1 # homeassistant.components.gitter gitterpy==0.1.7 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4bb18c8cf7a..42e5020eb01 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -243,7 +243,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.1 # homeassistant.components.gios -gios==0.0.5 +gios==0.1.1 # homeassistant.components.glances glances_api==0.2.0 From 43777ace206682e2af5c8d3c59d8dba9e107258f Mon Sep 17 00:00:00 2001 From: Jc2k Date: Fri, 3 Apr 2020 13:36:10 +0100 Subject: [PATCH 393/431] Fix browsing regression (#33572) --- 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 b0808d83d68..3171b8e953b 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.24.5"], + "requirements": ["zeroconf==0.25.0"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ca19feba5ef..025040f72d6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -25,7 +25,7 @@ ruamel.yaml==0.15.100 sqlalchemy==1.3.15 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.24.5 +zeroconf==0.25.0 pycryptodome>=3.6.6 diff --git a/requirements_all.txt b/requirements_all.txt index 2ef389a280c..dd1f96ed1f4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2173,7 +2173,7 @@ youtube_dl==2020.03.24 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.24.5 +zeroconf==0.25.0 # homeassistant.components.zha zha-quirks==0.0.38 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 42e5020eb01..92976a44584 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -795,7 +795,7 @@ ya_ma==0.3.8 yahooweather==0.10 # homeassistant.components.zeroconf -zeroconf==0.24.5 +zeroconf==0.25.0 # homeassistant.components.zha zha-quirks==0.0.38 From 5cf2043c0476d2d5786a5acbd5f3f4ddfd5f7836 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Fri, 3 Apr 2020 15:51:28 +0200 Subject: [PATCH 394/431] Bump adguardhome to 0.4.2 (#33575) --- homeassistant/components/adguard/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/homeassistant/components/adguard/manifest.json b/homeassistant/components/adguard/manifest.json index c77e0b3254d..02b0e2ea455 100644 --- a/homeassistant/components/adguard/manifest.json +++ b/homeassistant/components/adguard/manifest.json @@ -3,7 +3,7 @@ "name": "AdGuard Home", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/adguard", - "requirements": ["adguardhome==0.4.1"], + "requirements": ["adguardhome==0.4.2"], "dependencies": [], "codeowners": ["@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index dd1f96ed1f4..cd31ce36b2e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -122,7 +122,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adb-shell==0.1.1 # homeassistant.components.adguard -adguardhome==0.4.1 +adguardhome==0.4.2 # homeassistant.components.frontier_silicon afsapi==0.0.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 92976a44584..5cd5aea496e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -32,7 +32,7 @@ abodepy==0.18.1 adb-shell==0.1.1 # homeassistant.components.adguard -adguardhome==0.4.1 +adguardhome==0.4.2 # homeassistant.components.geonetnz_quakes aio_geojson_geonetnz_quakes==0.12 From ddddd8566d4d0ee01d2efa861cd9e2754846def6 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Apr 2020 15:39:22 +0200 Subject: [PATCH 395/431] Add default delay to Harmony config entries (#33576) --- homeassistant/components/harmony/__init__.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/harmony/__init__.py b/homeassistant/components/harmony/__init__.py index f7140bdb400..540e39f8f44 100644 --- a/homeassistant/components/harmony/__init__.py +++ b/homeassistant/components/harmony/__init__.py @@ -2,7 +2,11 @@ import asyncio import logging -from homeassistant.components.remote import ATTR_ACTIVITY, ATTR_DELAY_SECS +from homeassistant.components.remote import ( + ATTR_ACTIVITY, + ATTR_DELAY_SECS, + DEFAULT_DELAY_SECS, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST, CONF_NAME from homeassistant.core import HomeAssistant, callback @@ -33,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): address = entry.data[CONF_HOST] name = entry.data[CONF_NAME] activity = entry.options.get(ATTR_ACTIVITY) - delay_secs = entry.options.get(ATTR_DELAY_SECS) + delay_secs = entry.options.get(ATTR_DELAY_SECS, DEFAULT_DELAY_SECS) harmony_conf_file = hass.config.path(f"harmony_{entry.unique_id}.conf") try: From 1634592d902128b2b3fc4ee3ad37b4cc571867cd Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 3 Apr 2020 19:32:00 +0200 Subject: [PATCH 396/431] Updated frontend to 20200403.0 (#33586) --- 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 8a540a96b0e..b0da48ab713 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==20200401.0" + "home-assistant-frontend==20200403.0" ], "dependencies": [ "api", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 025040f72d6..fd8e841d568 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200401.0 +home-assistant-frontend==20200403.0 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index cd31ce36b2e..5569f113d97 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200401.0 +home-assistant-frontend==20200403.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 5cd5aea496e..e0f6ff033ed 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200401.0 +home-assistant-frontend==20200403.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 4ead87270e1791f531d5a4391236a9ea0085fe40 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Apr 2020 10:48:05 -0700 Subject: [PATCH 397/431] Bumped version to 0.108.0b2 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 1bf7a4f6441..e4e08f76356 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b1" +PATCH_VERSION = "0b2" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 0763503151c5fbbae956bd4ee793849dae8468f7 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sat, 4 Apr 2020 00:34:42 -0500 Subject: [PATCH 398/431] Debounce calls to Plex server (#33560) * Debounce calls to Plex server * Simplify debounce by recommendation * Update tests to handle debounce * Test debouncer, fix & optimize tests * Use property instead --- homeassistant/components/plex/const.py | 1 + homeassistant/components/plex/server.py | 37 +++++++++++++-- tests/components/plex/common.py | 20 +++++++++ tests/components/plex/test_config_flow.py | 6 +-- tests/components/plex/test_init.py | 36 +++++++-------- tests/components/plex/test_server.py | 55 +++++++++++++++++++---- 6 files changed, 120 insertions(+), 35 deletions(-) create mode 100644 tests/components/plex/common.py diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 44bb25b3fd9..126c6eb313a 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -9,6 +9,7 @@ DEFAULT_PORT = 32400 DEFAULT_SSL = False DEFAULT_VERIFY_SSL = True +DEBOUNCE_TIMEOUT = 1 DISPATCHERS = "dispatchers" PLATFORMS = frozenset(["media_player", "sensor"]) PLATFORMS_COMPLETED = "platforms_completed" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 196968cc097..f2a4908e119 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -1,4 +1,5 @@ """Shared class to maintain Plex server instances.""" +from functools import partial, wraps import logging import ssl from urllib.parse import urlparse @@ -12,6 +13,7 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.event import async_call_later from .const import ( CONF_CLIENT_IDENTIFIER, @@ -19,6 +21,7 @@ from .const import ( CONF_MONITORED_USERS, CONF_SERVER, CONF_USE_EPISODE_ART, + DEBOUNCE_TIMEOUT, DEFAULT_VERIFY_SSL, PLEX_NEW_MP_SIGNAL, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL, @@ -39,12 +42,37 @@ plexapi.X_PLEX_PRODUCT = X_PLEX_PRODUCT plexapi.X_PLEX_VERSION = X_PLEX_VERSION +def debounce(func): + """Decorate function to debounce callbacks from Plex websocket.""" + + unsub = None + + async def call_later_listener(self, _): + """Handle call_later callback.""" + nonlocal unsub + unsub = None + await self.hass.async_add_executor_job(func, self) + + @wraps(func) + def wrapper(self): + """Schedule async callback.""" + nonlocal unsub + if unsub: + _LOGGER.debug("Throttling update of %s", self.friendly_name) + unsub() # pylint: disable=not-callable + unsub = async_call_later( + self.hass, DEBOUNCE_TIMEOUT, partial(call_later_listener, self), + ) + + return wrapper + + class PlexServer: """Manages a single Plex server connection.""" def __init__(self, hass, server_config, known_server_id=None, options=None): """Initialize a Plex server instance.""" - self._hass = hass + self.hass = hass self._plex_server = None self._known_clients = set() self._known_idle = set() @@ -150,12 +178,13 @@ class PlexServer: unique_id = f"{self.machine_identifier}:{machine_identifier}" _LOGGER.debug("Refreshing %s", unique_id) dispatcher_send( - self._hass, + self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, session, ) + @debounce def update_platforms(self): """Update the platform entities.""" _LOGGER.debug("Updating devices") @@ -239,13 +268,13 @@ class PlexServer: if new_entity_configs: dispatcher_send( - self._hass, + self.hass, PLEX_NEW_MP_SIGNAL.format(self.machine_identifier), new_entity_configs, ) dispatcher_send( - self._hass, + self.hass, PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), sessions, ) diff --git a/tests/components/plex/common.py b/tests/components/plex/common.py new file mode 100644 index 00000000000..adc6f4e0299 --- /dev/null +++ b/tests/components/plex/common.py @@ -0,0 +1,20 @@ +"""Common fixtures and functions for Plex tests.""" +from datetime import timedelta + +from homeassistant.components.plex.const import ( + DEBOUNCE_TIMEOUT, + PLEX_UPDATE_PLATFORMS_SIGNAL, +) +from homeassistant.helpers.dispatcher import async_dispatcher_send +import homeassistant.util.dt as dt_util + +from tests.common import async_fire_time_changed + + +async def trigger_plex_update(hass, server_id): + """Update Plex by sending signal and jumping ahead by debounce timeout.""" + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index d839ccc674b..bd5d45c0246 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -15,14 +15,13 @@ from homeassistant.components.plex.const import ( CONF_USE_EPISODE_ART, DOMAIN, PLEX_SERVER_CONFIG, - PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.config_entries import ENTRY_STATE_LOADED from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN, CONF_URL -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component +from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer @@ -416,8 +415,7 @@ async def test_option_flow_new_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 387ce6cac03..1aef7878df5 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -23,10 +23,10 @@ from homeassistant.const import ( CONF_URL, CONF_VERIFY_SSL, ) -from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS, MOCK_SERVERS, MOCK_TOKEN from .mock_classes import MockPlexAccount, MockPlexServer @@ -74,7 +74,7 @@ async def test_setup_with_config(hass): ) -async def test_setup_with_config_entry(hass): +async def test_setup_with_config_entry(hass, caplog): """Test setup component with config.""" mock_plex_server = MockPlexServer() @@ -109,30 +109,31 @@ async def test_setup_with_config_entry(hass): hass.data[const.DOMAIN][const.PLATFORMS_COMPLETED][server_id] == const.PLATFORMS ) - async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) - async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) with patch.object( mock_plex_server, "clients", side_effect=plexapi.exceptions.BadRequest - ): - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() + ) as patched_clients_bad_request: + await trigger_plex_update(hass, server_id) + + assert patched_clients_bad_request.called + assert "Error requesting Plex client data from server" in caplog.text with patch.object( mock_plex_server, "clients", side_effect=requests.exceptions.RequestException - ): - async_dispatcher_send( - hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id) - ) - await hass.async_block_till_done() + ) as patched_clients_requests_exception: + await trigger_plex_update(hass, server_id) + + assert patched_clients_requests_exception.called + assert ( + f"Could not connect to Plex server: {mock_plex_server.friendlyName}" + in caplog.text + ) async def test_set_config_entry_unique_id(hass): @@ -294,8 +295,7 @@ async def test_setup_with_photo_session(hass): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, const.PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) media_player = hass.states.get("media_player.plex_product_title") assert media_player.state == "idle" diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 646a6ded32e..242c0fe5504 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -1,5 +1,6 @@ """Tests for Plex server.""" import copy +from datetime import timedelta from asynctest import patch @@ -7,16 +8,19 @@ from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.components.plex.const import ( CONF_IGNORE_NEW_SHARED_USERS, CONF_MONITORED_USERS, + DEBOUNCE_TIMEOUT, DOMAIN, PLEX_UPDATE_PLATFORMS_SIGNAL, SERVERS, ) from homeassistant.helpers.dispatcher import async_dispatcher_send +import homeassistant.util.dt as dt_util +from .common import trigger_plex_update from .const import DEFAULT_DATA, DEFAULT_OPTIONS from .mock_classes import MockPlexServer -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_new_users_available(hass): @@ -44,8 +48,7 @@ async def test_new_users_available(hass): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -83,8 +86,7 @@ async def test_new_ignored_users_available(hass, caplog): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) monitored_users = hass.data[DOMAIN][SERVERS][server_id].option_monitored_users @@ -118,8 +120,7 @@ async def test_mark_sessions_idle(hass): server_id = mock_plex_server.machineIdentifier - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) @@ -127,8 +128,44 @@ async def test_mark_sessions_idle(hass): mock_plex_server.clear_clients() mock_plex_server.clear_sessions() - async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) - await hass.async_block_till_done() + await trigger_plex_update(hass, server_id) sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == "0" + + +async def test_debouncer(hass, caplog): + """Test debouncer decorator logic.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=DEFAULT_DATA, + options=DEFAULT_OPTIONS, + unique_id=DEFAULT_DATA["server_id"], + ) + + mock_plex_server = MockPlexServer(config_entry=entry) + + with patch("plexapi.server.PlexServer", return_value=mock_plex_server), patch( + "homeassistant.components.plex.PlexWebsocket.listen" + ): + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + server_id = mock_plex_server.machineIdentifier + + # First two updates are skipped + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + async_dispatcher_send(hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id)) + await hass.async_block_till_done() + + next_update = dt_util.utcnow() + timedelta(seconds=DEBOUNCE_TIMEOUT) + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() + + assert ( + caplog.text.count(f"Throttling update of {mock_plex_server.friendlyName}") == 2 + ) From ab7afbdaf75339f71205ca298576ac8d8b7ab394 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Apr 2020 19:42:06 -0700 Subject: [PATCH 399/431] Hass.io integration do not warn safe mode (#33600) * Hass.io integration do not warn safe mode * Better implementation * Tweak log message --- homeassistant/components/hassio/handler.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/hassio/handler.py b/homeassistant/components/hassio/handler.py index e471bfae543..bb41e5335d7 100644 --- a/homeassistant/components/hassio/handler.py +++ b/homeassistant/components/hassio/handler.py @@ -10,6 +10,7 @@ from homeassistant.components.http import ( CONF_SERVER_HOST, CONF_SERVER_PORT, CONF_SSL_CERTIFICATE, + DEFAULT_SERVER_HOST, ) from homeassistant.const import SERVER_PORT @@ -133,9 +134,14 @@ class HassIO: "refresh_token": refresh_token.token, } - if CONF_SERVER_HOST in http_config: + if ( + http_config.get(CONF_SERVER_HOST, DEFAULT_SERVER_HOST) + != DEFAULT_SERVER_HOST + ): options["watchdog"] = False - _LOGGER.warning("Don't use 'server_host' options with Hass.io") + _LOGGER.warning( + "Found incompatible HTTP option 'server_host'. Watchdog feature disabled" + ) return await self.send_command("/homeassistant/options", payload=options) From 6a297b375819a90cf97334bc8359abab390b4253 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 3 Apr 2020 22:41:08 -0700 Subject: [PATCH 400/431] Use IP addresses instead of mDNS names when wled discovered (#33608) --- homeassistant/components/wled/config_flow.py | 2 +- tests/components/wled/__init__.py | 10 ++--- tests/components/wled/test_config_flow.py | 44 ++++++++++---------- tests/components/wled/test_init.py | 2 +- tests/components/wled/test_light.py | 6 +-- tests/components/wled/test_switch.py | 4 +- 6 files changed, 35 insertions(+), 33 deletions(-) diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index da1193b1a01..ecf8ca6e1e0 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -45,7 +45,7 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 self.context.update( { - CONF_HOST: host, + CONF_HOST: user_input["host"], CONF_NAME: name, CONF_MAC: user_input["properties"].get(CONF_MAC), "title_placeholders": {"name": name}, diff --git a/tests/components/wled/__init__.py b/tests/components/wled/__init__.py index b4b01c66d8a..f6bd0643450 100644 --- a/tests/components/wled/__init__.py +++ b/tests/components/wled/__init__.py @@ -18,31 +18,31 @@ async def init_integration( fixture = "wled/rgb.json" if not rgbw else "wled/rgbw.json" aioclient_mock.get( - "http://example.local:80/json/", + "http://192.168.1.123:80/json/", text=load_fixture(fixture), headers={"Content-Type": "application/json"}, ) aioclient_mock.post( - "http://example.local:80/json/state", + "http://192.168.1.123:80/json/state", json={}, headers={"Content-Type": "application/json"}, ) aioclient_mock.get( - "http://example.local:80/json/info", + "http://192.168.1.123:80/json/info", json={}, headers={"Content-Type": "application/json"}, ) aioclient_mock.get( - "http://example.local:80/json/state", + "http://192.168.1.123:80/json/state", json={}, headers={"Content-Type": "application/json"}, ) entry = MockConfigEntry( - domain=DOMAIN, data={CONF_HOST: "example.local", CONF_MAC: "aabbccddeeff"} + domain=DOMAIN, data={CONF_HOST: "192.168.1.123", CONF_MAC: "aabbccddeeff"} ) entry.add_to_hass(hass) diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 521a7b67a46..6de14a024d4 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -40,7 +40,7 @@ async def test_show_zerconf_form( ) -> None: """Test that the zeroconf confirmation form is served.""" aioclient_mock.get( - "http://example.local:80/json/", + "http://192.168.1.123:80/json/", text=load_fixture("wled/rgb.json"), headers={"Content-Type": "application/json"}, ) @@ -49,10 +49,10 @@ async def test_show_zerconf_form( flow.hass = hass flow.context = {"source": SOURCE_ZEROCONF} result = await flow.async_step_zeroconf( - {"hostname": "example.local.", "properties": {}} + {"host": "192.168.1.123", "hostname": "example.local.", "properties": {}} ) - assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_HOST] == "192.168.1.123" assert flow.context[CONF_NAME] == "example" assert result["description_placeholders"] == {CONF_NAME: "example"} assert result["step_id"] == "zeroconf_confirm" @@ -80,12 +80,12 @@ async def test_zeroconf_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://example.local/json/", exc=aiohttp.ClientError) + aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "properties": {}}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, ) assert result["reason"] == "connection_error" @@ -96,7 +96,7 @@ async def test_zeroconf_confirm_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, @@ -105,7 +105,7 @@ async def test_zeroconf_confirm_connection_error( CONF_HOST: "example.com", CONF_NAME: "test", }, - data={"hostname": "example.com.", "properties": {}}, + data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, ) assert result["reason"] == "connection_error" @@ -133,7 +133,7 @@ async def test_user_device_exists_abort( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_USER}, - data={CONF_HOST: "example.local"}, + data={CONF_HOST: "192.168.1.123"}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -149,7 +149,7 @@ async def test_zeroconf_device_exists_abort( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "properties": {}}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -165,7 +165,11 @@ async def test_zeroconf_with_mac_device_exists_abort( result = await hass.config_entries.flow.async_init( config_flow.DOMAIN, context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "properties": {CONF_MAC: "aabbccddeeff"}}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "aabbccddeeff"}, + }, ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT @@ -177,7 +181,7 @@ async def test_full_user_flow_implementation( ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.get( - "http://example.local:80/json/", + "http://192.168.1.123:80/json/", text=load_fixture("wled/rgb.json"), headers={"Content-Type": "application/json"}, ) @@ -190,12 +194,12 @@ async def test_full_user_flow_implementation( assert result["type"] == data_entry_flow.RESULT_TYPE_FORM result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={CONF_HOST: "example.local"} + result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) - assert result["data"][CONF_HOST] == "example.local" + assert result["data"][CONF_HOST] == "192.168.1.123" assert result["data"][CONF_MAC] == "aabbccddeeff" - assert result["title"] == "example.local" + assert result["title"] == "192.168.1.123" assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY @@ -204,7 +208,7 @@ async def test_full_zeroconf_flow_implementation( ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.get( - "http://example.local:80/json/", + "http://192.168.1.123:80/json/", text=load_fixture("wled/rgb.json"), headers={"Content-Type": "application/json"}, ) @@ -213,19 +217,17 @@ async def test_full_zeroconf_flow_implementation( flow.hass = hass flow.context = {"source": SOURCE_ZEROCONF} result = await flow.async_step_zeroconf( - {"hostname": "example.local.", "properties": {}} + {"host": "192.168.1.123", "hostname": "example.local.", "properties": {}} ) - assert flow.context[CONF_HOST] == "example.local" + assert flow.context[CONF_HOST] == "192.168.1.123" assert flow.context[CONF_NAME] == "example" assert result["description_placeholders"] == {CONF_NAME: "example"} assert result["step_id"] == "zeroconf_confirm" assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - result = await flow.async_step_zeroconf_confirm( - user_input={CONF_HOST: "example.local"} - ) - assert result["data"][CONF_HOST] == "example.local" + result = await flow.async_step_zeroconf_confirm(user_input={}) + assert result["data"][CONF_HOST] == "192.168.1.123" assert result["data"][CONF_MAC] == "aabbccddeeff" assert result["title"] == "example" assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index d287ba6014a..053c5ebaca0 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -13,7 +13,7 @@ async def test_config_entry_not_ready( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the WLED configuration entry not ready.""" - aioclient_mock.get("http://example.local:80/json/", exc=aiohttp.ClientError) + aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) entry = await init_integration(hass, aioclient_mock) assert entry.state == ENTRY_STATE_SETUP_RETRY diff --git a/tests/components/wled/test_light.py b/tests/components/wled/test_light.py index c49ae6a12df..0009677cf18 100644 --- a/tests/components/wled/test_light.py +++ b/tests/components/wled/test_light.py @@ -141,7 +141,7 @@ async def test_light_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog ) -> None: """Test error handling of the WLED lights.""" - aioclient_mock.post("http://example.local:80/json/state", text="", status=400) + aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): @@ -162,7 +162,7 @@ async def test_light_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test error handling of the WLED switches.""" - aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) + aioclient_mock.post("http://192.168.1.123:80/json/state", exc=aiohttp.ClientError) await init_integration(hass, aioclient_mock) with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): @@ -339,7 +339,7 @@ async def test_effect_service_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog ) -> None: """Test error handling of the WLED effect service.""" - aioclient_mock.post("http://example.local:80/json/state", text="", status=400) + aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): diff --git a/tests/components/wled/test_switch.py b/tests/components/wled/test_switch.py index 5b315c87e9e..d140953b948 100644 --- a/tests/components/wled/test_switch.py +++ b/tests/components/wled/test_switch.py @@ -139,7 +139,7 @@ async def test_switch_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog ) -> None: """Test error handling of the WLED switches.""" - aioclient_mock.post("http://example.local:80/json/state", text="", status=400) + aioclient_mock.post("http://192.168.1.123:80/json/state", text="", status=400) await init_integration(hass, aioclient_mock) with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): @@ -160,7 +160,7 @@ async def test_switch_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test error handling of the WLED switches.""" - aioclient_mock.post("http://example.local:80/json/state", exc=aiohttp.ClientError) + aioclient_mock.post("http://192.168.1.123:80/json/state", exc=aiohttp.ClientError) await init_integration(hass, aioclient_mock) with patch("homeassistant.components.wled.WLEDDataUpdateCoordinator.async_refresh"): From 5dae7f84518ae2ef07ee2b91a7a4ea2d6e2f0f4e Mon Sep 17 00:00:00 2001 From: Anders Melchiorsen Date: Sat, 4 Apr 2020 07:41:39 +0200 Subject: [PATCH 401/431] Identify more Sonos radio stations with poor titles (#33609) --- homeassistant/components/sonos/media_player.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index bb1179bb1e7..13484e6901b 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -614,11 +614,11 @@ class SonosEntity(MediaPlayerDevice): except (TypeError, KeyError, AttributeError): pass - # Radios without tagging can have the radio URI as title. Non-playing - # radios will not have a current title. In these cases we try to use - # the radio name instead. + # Radios without tagging can have part of the radio URI as title. + # Non-playing radios will not have a current title. In these cases we + # try to use the radio name instead. try: - if self.soco.is_radio_uri(self._media_title) or self.state != STATE_PLAYING: + if self._media_title in self._uri or self.state != STATE_PLAYING: self._media_title = variables["enqueued_transport_uri_meta_data"].title except (TypeError, KeyError, AttributeError): pass From 38b729b00ab250e462a89943734537ac54dad923 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Sat, 4 Apr 2020 00:36:46 -0500 Subject: [PATCH 402/431] Use IP addresses instead of mDNS names when IPP discovered (#33610) * use discovery resolved host rather than mdns host. * Update __init__.py * Update test_config_flow.py * Update __init__.py * Update test_init.py * Update test_config_flow.py * Update test_config_flow.py * Update __init__.py * Update __init__.py * Update __init__.py * Update test_init.py * Update test_config_flow.py --- homeassistant/components/ipp/config_flow.py | 2 +- tests/components/ipp/__init__.py | 13 ++++--- tests/components/ipp/test_config_flow.py | 38 ++++++++------------- tests/components/ipp/test_init.py | 4 +-- 4 files changed, 23 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index 395a5f0db58..e95267e7803 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -85,7 +85,7 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): self.discovery_info.update( { - CONF_HOST: host, + CONF_HOST: discovery_info[CONF_HOST], CONF_PORT: port, CONF_SSL: tls, CONF_VERIFY_SSL: False, diff --git a/tests/components/ipp/__init__.py b/tests/components/ipp/__init__.py index 6bf162725e1..1c52c557024 100644 --- a/tests/components/ipp/__init__.py +++ b/tests/components/ipp/__init__.py @@ -22,13 +22,13 @@ IPP_ZEROCONF_SERVICE_TYPE = "_ipp._tcp.local." IPPS_ZEROCONF_SERVICE_TYPE = "_ipps._tcp.local." ZEROCONF_NAME = "EPSON123456" -ZEROCONF_HOST = "1.2.3.4" +ZEROCONF_HOST = "192.168.1.31" ZEROCONF_HOSTNAME = "EPSON123456.local." ZEROCONF_PORT = 631 MOCK_USER_INPUT = { - CONF_HOST: "EPSON123456.local", + CONF_HOST: "192.168.1.31", CONF_PORT: 361, CONF_SSL: False, CONF_VERIFY_SSL: False, @@ -37,7 +37,7 @@ MOCK_USER_INPUT = { MOCK_ZEROCONF_IPP_SERVICE_INFO = { CONF_TYPE: IPP_ZEROCONF_SERVICE_TYPE, - CONF_NAME: ZEROCONF_NAME, + CONF_NAME: f"{ZEROCONF_NAME}.{IPP_ZEROCONF_SERVICE_TYPE}", CONF_HOST: ZEROCONF_HOST, ATTR_HOSTNAME: ZEROCONF_HOSTNAME, CONF_PORT: ZEROCONF_PORT, @@ -46,7 +46,7 @@ MOCK_ZEROCONF_IPP_SERVICE_INFO = { MOCK_ZEROCONF_IPPS_SERVICE_INFO = { CONF_TYPE: IPPS_ZEROCONF_SERVICE_TYPE, - CONF_NAME: ZEROCONF_NAME, + CONF_NAME: f"{ZEROCONF_NAME}.{IPPS_ZEROCONF_SERVICE_TYPE}", CONF_HOST: ZEROCONF_HOST, ATTR_HOSTNAME: ZEROCONF_HOSTNAME, CONF_PORT: ZEROCONF_PORT, @@ -65,10 +65,9 @@ async def init_integration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, ) -> MockConfigEntry: """Set up the IPP integration in Home Assistant.""" - fixture = "ipp/get-printer-attributes.bin" aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", + "http://192.168.1.31:631/ipp/print", content=load_fixture_binary(fixture), headers={"Content-Type": "application/ipp"}, ) @@ -77,7 +76,7 @@ async def init_integration( domain=DOMAIN, unique_id="cfe92100-67c4-11d4-a45f-f8d027761251", data={ - CONF_HOST: "EPSON123456.local", + CONF_HOST: "192.168.1.31", CONF_PORT: 631, CONF_SSL: False, CONF_VERIFY_SSL: True, diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 5a2744eac51..0682929b7b8 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -38,7 +38,7 @@ async def test_show_zeroconf_form( ) -> None: """Test that the zeroconf confirmation form is served.""" aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", + "http://192.168.1.31:631/ipp/print", content=load_fixture_binary("ipp/get-printer-attributes.bin"), headers={"Content-Type": "application/ipp"}, ) @@ -57,9 +57,7 @@ async def test_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we show user form on IPP connection error.""" - aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError - ) + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) user_input = MOCK_USER_INPUT.copy() result = await hass.config_entries.flow.async_init( @@ -75,7 +73,7 @@ async def test_zeroconf_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError) + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( @@ -90,17 +88,11 @@ async def test_zeroconf_confirm_connection_error( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort zeroconf flow on IPP connection error.""" - aioclient_mock.post("http://EPSON123456.local/ipp/print", exc=aiohttp.ClientError) + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() result = await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_ZEROCONF, - CONF_HOST: "EPSON123456.local", - CONF_NAME: "EPSON123456", - }, - data=discovery_info, + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info ) assert result["type"] == RESULT_TYPE_ABORT @@ -112,7 +104,7 @@ async def test_user_connection_upgrade_required( ) -> None: """Test we show the user form if connection upgrade required by server.""" aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", exc=IPPConnectionUpgradeRequired + "http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired ) user_input = MOCK_USER_INPUT.copy() @@ -130,7 +122,7 @@ async def test_zeroconf_connection_upgrade_required( ) -> None: """Test we abort zeroconf flow on IPP connection error.""" aioclient_mock.post( - "http://EPSON123456.local/ipp/print", exc=IPPConnectionUpgradeRequired + "http://192.168.1.31:631/ipp/print", exc=IPPConnectionUpgradeRequired ) discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() @@ -193,7 +185,7 @@ async def test_full_user_flow_implementation( ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", + "http://192.168.1.31:631/ipp/print", content=load_fixture_binary("ipp/get-printer-attributes.bin"), headers={"Content-Type": "application/ipp"}, ) @@ -207,14 +199,14 @@ async def test_full_user_flow_implementation( result = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_HOST: "EPSON123456.local", CONF_BASE_PATH: "/ipp/print"}, + user_input={CONF_HOST: "192.168.1.31", CONF_BASE_PATH: "/ipp/print"}, ) assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "EPSON123456.local" + assert result["title"] == "192.168.1.31" assert result["data"] - assert result["data"][CONF_HOST] == "EPSON123456.local" + assert result["data"][CONF_HOST] == "192.168.1.31" assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" @@ -223,7 +215,7 @@ async def test_full_zeroconf_flow_implementation( ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", + "http://192.168.1.31:631/ipp/print", content=load_fixture_binary("ipp/get-printer-attributes.bin"), headers={"Content-Type": "application/ipp"}, ) @@ -244,7 +236,7 @@ async def test_full_zeroconf_flow_implementation( assert result["title"] == "EPSON123456" assert result["data"] - assert result["data"][CONF_HOST] == "EPSON123456.local" + assert result["data"][CONF_HOST] == "192.168.1.31" assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" assert not result["data"][CONF_SSL] @@ -254,7 +246,7 @@ async def test_full_zeroconf_tls_flow_implementation( ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.post( - "https://EPSON123456.local:631/ipp/print", + "https://192.168.1.31:631/ipp/print", content=load_fixture_binary("ipp/get-printer-attributes.bin"), headers={"Content-Type": "application/ipp"}, ) @@ -276,7 +268,7 @@ async def test_full_zeroconf_tls_flow_implementation( assert result["title"] == "EPSON123456" assert result["data"] - assert result["data"][CONF_HOST] == "EPSON123456.local" + assert result["data"][CONF_HOST] == "192.168.1.31" assert result["data"][CONF_NAME] == "EPSON123456" assert result["data"][CONF_UUID] == "cfe92100-67c4-11d4-a45f-f8d027761251" assert result["data"][CONF_SSL] diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index 7d3d0692e28..2ec11a1e937 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -17,9 +17,7 @@ async def test_config_entry_not_ready( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the IPP configuration entry not ready.""" - aioclient_mock.post( - "http://EPSON123456.local:631/ipp/print", exc=aiohttp.ClientError - ) + aioclient_mock.post("http://192.168.1.31:631/ipp/print", exc=aiohttp.ClientError) entry = await init_integration(hass, aioclient_mock) assert entry.state == ENTRY_STATE_SETUP_RETRY From 52f710528f5f199d910ac2ffeb3a56078c405eb5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2020 10:19:58 -0500 Subject: [PATCH 403/431] Handle race condition in harmony setup (#33611) * Handle race condition in harmony setup If the remote was discovered via ssdp before the yaml config import happened, the unique id would already be set and the import would abort. * Update homeassistant/components/harmony/config_flow.py Co-Authored-By: Paulus Schoutsen * reduce * black Co-authored-by: Paulus Schoutsen --- homeassistant/components/harmony/config_flow.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/harmony/config_flow.py b/homeassistant/components/harmony/config_flow.py index 9d9c9dfb8e9..8d43b2d69ca 100644 --- a/homeassistant/components/harmony/config_flow.py +++ b/homeassistant/components/harmony/config_flow.py @@ -128,8 +128,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, validated_input): """Handle import.""" - await self.async_set_unique_id(validated_input[UNIQUE_ID]) + await self.async_set_unique_id( + validated_input[UNIQUE_ID], raise_on_progress=False + ) self._abort_if_unique_id_configured() + # Everything was validated in remote async_setup_platform # all we do now is create. return await self._async_create_entry_from_valid_input( @@ -149,14 +152,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): # Options from yaml are preserved, we will pull them out when # we setup the config entry data.update(_options_from_user_input(user_input)) - return self.async_create_entry(title=validated[CONF_NAME], data=data) - def _host_already_configured(self, user_input): - """See if we already have a harmony matching user input configured.""" - existing_hosts = { - entry.data[CONF_HOST] for entry in self._async_current_entries() - } - return user_input[CONF_HOST] in existing_hosts + return self.async_create_entry(title=validated[CONF_NAME], data=data) def _options_from_user_input(user_input): From f5eafbe7609e72f0fe9a9a3d8be0726afc70ff11 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sat, 4 Apr 2020 14:51:12 +0200 Subject: [PATCH 404/431] Bump twentemilieu to 0.3.0 (#33622) * Bump twentemilieu to 0.3.0 * Fix tests --- homeassistant/components/twentemilieu/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/twentemilieu/test_config_flow.py | 8 ++++---- 4 files changed, 7 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/twentemilieu/manifest.json b/homeassistant/components/twentemilieu/manifest.json index 9444e33700e..da4dc074262 100644 --- a/homeassistant/components/twentemilieu/manifest.json +++ b/homeassistant/components/twentemilieu/manifest.json @@ -3,7 +3,6 @@ "name": "Twente Milieu", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/twentemilieu", - "requirements": ["twentemilieu==0.2.0"], - "dependencies": [], + "requirements": ["twentemilieu==0.3.0"], "codeowners": ["@frenck"] } diff --git a/requirements_all.txt b/requirements_all.txt index 5569f113d97..2b765234e81 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2047,7 +2047,7 @@ transmissionrpc==0.11 tuyaha==0.0.5 # homeassistant.components.twentemilieu -twentemilieu==0.2.0 +twentemilieu==0.3.0 # homeassistant.components.twilio twilio==6.32.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index e0f6ff033ed..414fdf493f7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -744,7 +744,7 @@ toonapilib==3.2.4 transmissionrpc==0.11 # homeassistant.components.twentemilieu -twentemilieu==0.2.0 +twentemilieu==0.3.0 # homeassistant.components.twilio twilio==6.32.0 diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 1ccbe7a58a9..7dd19e755f3 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -34,7 +34,7 @@ async def test_show_set_form(hass): async def test_connection_error(hass, aioclient_mock): """Test we show user form on Twente Milieu connection error.""" aioclient_mock.post( - "https://wasteapi.2go-mobile.com/api/FetchAdress", exc=aiohttp.ClientError + "https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError ) flow = config_flow.TwenteMilieuFlowHandler() @@ -49,7 +49,7 @@ async def test_connection_error(hass, aioclient_mock): async def test_invalid_address(hass, aioclient_mock): """Test we show user form on Twente Milieu invalid address error.""" aioclient_mock.post( - "https://wasteapi.2go-mobile.com/api/FetchAdress", + "https://twentemilieuapi.ximmio.com/api/FetchAdress", json={"dataList": []}, headers={"Content-Type": "application/json"}, ) @@ -70,7 +70,7 @@ async def test_address_already_set_up(hass, aioclient_mock): ) aioclient_mock.post( - "https://wasteapi.2go-mobile.com/api/FetchAdress", + "https://twentemilieuapi.ximmio.com/api/FetchAdress", json={"dataList": [{"UniqueId": "12345"}]}, headers={"Content-Type": "application/json"}, ) @@ -86,7 +86,7 @@ async def test_address_already_set_up(hass, aioclient_mock): async def test_full_flow_implementation(hass, aioclient_mock): """Test registering an integration and finishing flow works.""" aioclient_mock.post( - "https://wasteapi.2go-mobile.com/api/FetchAdress", + "https://twentemilieuapi.ximmio.com/api/FetchAdress", json={"dataList": [{"UniqueId": "12345"}]}, headers={"Content-Type": "application/json"}, ) From 71803cbdef712f480934ca9072497b4274dfae18 Mon Sep 17 00:00:00 2001 From: Alexei Chetroi Date: Sat, 4 Apr 2020 12:58:43 -0400 Subject: [PATCH 405/431] Update zha dependencies (#33639) --- 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 09dcf71d027..66b89724a2f 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-homeassistant==0.15.1", + "bellows-homeassistant==0.15.2", "zha-quirks==0.0.38", "zigpy-cc==0.3.1", "zigpy-deconz==0.8.0", - "zigpy-homeassistant==0.18.0", + "zigpy-homeassistant==0.18.1", "zigpy-xbee-homeassistant==0.11.0", "zigpy-zigate==0.5.1" ], diff --git a/requirements_all.txt b/requirements_all.txt index 2b765234e81..93f8621355f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -317,7 +317,7 @@ beautifulsoup4==4.8.2 beewi_smartclim==0.0.7 # homeassistant.components.zha -bellows-homeassistant==0.15.1 +bellows-homeassistant==0.15.2 # homeassistant.components.bmw_connected_drive bimmer_connected==0.7.1 @@ -2191,7 +2191,7 @@ zigpy-cc==0.3.1 zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.18.0 +zigpy-homeassistant==0.18.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.11.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 414fdf493f7..c4d80a32788 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -131,7 +131,7 @@ av==6.1.2 axis==25 # homeassistant.components.zha -bellows-homeassistant==0.15.1 +bellows-homeassistant==0.15.2 # homeassistant.components.bom bomradarloop==0.1.4 @@ -807,7 +807,7 @@ zigpy-cc==0.3.1 zigpy-deconz==0.8.0 # homeassistant.components.zha -zigpy-homeassistant==0.18.0 +zigpy-homeassistant==0.18.1 # homeassistant.components.zha zigpy-xbee-homeassistant==0.11.0 From 30a391b88b93bfd0eb8ae4c19cfd35525a90e96e Mon Sep 17 00:00:00 2001 From: jjlawren Date: Sun, 5 Apr 2020 01:21:20 -0500 Subject: [PATCH 406/431] Plex logging additions & cleanup (#33681) --- homeassistant/components/plex/server.py | 16 ++++++++----- tests/components/plex/test_init.py | 31 +++++++++++-------------- tests/components/plex/test_server.py | 2 +- 3 files changed, 25 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index f2a4908e119..80e7c92640a 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -159,6 +159,7 @@ class PlexServer: for account in self._plex_server.systemAccounts() if account.name ] + _LOGGER.debug("Linked accounts: %s", self.accounts) owner_account = [ account.name @@ -167,6 +168,7 @@ class PlexServer: ] if owner_account: self._owner_username = owner_account[0] + _LOGGER.debug("Server owner found: '%s'", self._owner_username) self._version = self._plex_server.version @@ -209,11 +211,11 @@ class PlexServer: try: devices = self._plex_server.clients() sessions = self._plex_server.sessions() - except plexapi.exceptions.BadRequest: - _LOGGER.exception("Error requesting Plex client data from server") - return - except requests.exceptions.RequestException as ex: - _LOGGER.warning( + except ( + plexapi.exceptions.BadRequest, + requests.exceptions.RequestException, + ) as ex: + _LOGGER.error( "Could not connect to Plex server: %s (%s)", self.friendly_name, ex ) return @@ -234,7 +236,9 @@ class PlexServer: for player in session.players: if session_username and session_username not in monitored_users: ignored_clients.add(player.machineIdentifier) - _LOGGER.debug("Ignoring Plex client owned by %s", session_username) + _LOGGER.debug( + "Ignoring Plex client owned by '%s'", session_username + ) continue self._known_idle.discard(player.machineIdentifier) available_clients.setdefault( diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index 1aef7878df5..cd1ea8725bd 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -116,24 +116,21 @@ async def test_setup_with_config_entry(hass, caplog): await trigger_plex_update(hass, server_id) - with patch.object( - mock_plex_server, "clients", side_effect=plexapi.exceptions.BadRequest - ) as patched_clients_bad_request: - await trigger_plex_update(hass, server_id) + for test_exception in ( + plexapi.exceptions.BadRequest, + requests.exceptions.RequestException, + ): + with patch.object( + mock_plex_server, "clients", side_effect=test_exception + ) as patched_clients_bad_request: + await trigger_plex_update(hass, server_id) - assert patched_clients_bad_request.called - assert "Error requesting Plex client data from server" in caplog.text - - with patch.object( - mock_plex_server, "clients", side_effect=requests.exceptions.RequestException - ) as patched_clients_requests_exception: - await trigger_plex_update(hass, server_id) - - assert patched_clients_requests_exception.called - assert ( - f"Could not connect to Plex server: {mock_plex_server.friendlyName}" - in caplog.text - ) + assert patched_clients_bad_request.called + assert ( + f"Could not connect to Plex server: {mock_plex_server.friendlyName}" + in caplog.text + ) + caplog.clear() async def test_set_config_entry_unique_id(hass): diff --git a/tests/components/plex/test_server.py b/tests/components/plex/test_server.py index 242c0fe5504..3b70f30189a 100644 --- a/tests/components/plex/test_server.py +++ b/tests/components/plex/test_server.py @@ -94,7 +94,7 @@ async def test_new_ignored_users_available(hass, caplog): assert len(monitored_users) == 1 assert len(ignored_users) == 2 for ignored_user in ignored_users: - assert f"Ignoring Plex client owned by {ignored_user}" in caplog.text + assert f"Ignoring Plex client owned by '{ignored_user}'" in caplog.text sensor = hass.states.get("sensor.plex_plex_server_1") assert sensor.state == str(len(mock_plex_server.accounts)) From dd0fd36049d3be1723c71b7ad7f1965559de2670 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2020 01:21:44 -0500 Subject: [PATCH 407/431] Handle float values for homekit lightning (#33683) * Handle float values for homekit lightning * Empty commit to rerun CI --- .../components/homekit/type_lights.py | 28 ++++++++----------- tests/components/homekit/test_type_lights.py | 20 +++++++++++++ 2 files changed, 32 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/homekit/type_lights.py b/homeassistant/components/homekit/type_lights.py index 1720c2c58c8..e38af1a04eb 100644 --- a/homeassistant/components/homekit/type_lights.py +++ b/homeassistant/components/homekit/type_lights.py @@ -149,7 +149,7 @@ class Light(HomeAccessory): # Handle Brightness if CHAR_BRIGHTNESS in self.chars: brightness = new_state.attributes.get(ATTR_BRIGHTNESS) - if isinstance(brightness, int): + if isinstance(brightness, (int, float)): brightness = round(brightness / 255 * 100, 0) # The homeassistant component might report its brightness as 0 but is # not off. But 0 is a special value in homekit. When you turn on a @@ -169,22 +169,18 @@ class Light(HomeAccessory): # Handle color temperature if CHAR_COLOR_TEMPERATURE in self.chars: color_temperature = new_state.attributes.get(ATTR_COLOR_TEMP) - if ( - isinstance(color_temperature, int) - and self.char_color_temperature.value != color_temperature - ): - self.char_color_temperature.set_value(color_temperature) + if isinstance(color_temperature, (int, float)): + color_temperature = round(color_temperature, 0) + if self.char_color_temperature.value != color_temperature: + self.char_color_temperature.set_value(color_temperature) # Handle Color if CHAR_SATURATION in self.chars and CHAR_HUE in self.chars: hue, saturation = new_state.attributes.get(ATTR_HS_COLOR, (None, None)) - if ( - isinstance(hue, (int, float)) - and isinstance(saturation, (int, float)) - and ( - hue != self.char_hue.value - or saturation != self.char_saturation.value - ) - ): - self.char_hue.set_value(hue) - self.char_saturation.set_value(saturation) + if isinstance(hue, (int, float)) and isinstance(saturation, (int, float)): + hue = round(hue, 0) + saturation = round(saturation, 0) + if hue != self.char_hue.value: + self.char_hue.set_value(hue) + if saturation != self.char_saturation.value: + self.char_saturation.set_value(saturation) diff --git a/tests/components/homekit/test_type_lights.py b/tests/components/homekit/test_type_lights.py index 888ad87a848..3ee2e61cc72 100644 --- a/tests/components/homekit/test_type_lights.py +++ b/tests/components/homekit/test_type_lights.py @@ -235,6 +235,17 @@ async def test_light_brightness(hass, hk_driver, cls, events, driver): await hass.async_block_till_done() assert acc.char_brightness.value == 1 + # Ensure floats are handled + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 55.66}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 22 + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 108.4}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 43 + hass.states.async_set(entity_id, STATE_ON, {ATTR_BRIGHTNESS: 0.0}) + await hass.async_block_till_done() + assert acc.char_brightness.value == 1 + async def test_light_color_temperature(hass, hk_driver, cls, events, driver): """Test light with color temperature.""" @@ -417,6 +428,11 @@ async def test_light_set_brightness_and_color(hass, hk_driver, cls, events, driv await hass.async_block_till_done() assert acc.char_brightness.value == 40 + hass.states.async_set(entity_id, STATE_ON, {ATTR_HS_COLOR: (4.5, 9.2)}) + await hass.async_block_till_done() + assert acc.char_hue.value == 4 + assert acc.char_saturation.value == 9 + # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") @@ -489,6 +505,10 @@ async def test_light_set_brightness_and_color_temp( await hass.async_block_till_done() assert acc.char_brightness.value == 40 + hass.states.async_set(entity_id, STATE_ON, {ATTR_COLOR_TEMP: (224.14)}) + await hass.async_block_till_done() + assert acc.char_color_temperature.value == 224 + # Set from HomeKit call_turn_on = async_mock_service(hass, DOMAIN, "turn_on") From 0f39296251d52c63223a77d6acf30440617374e3 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Sat, 4 Apr 2020 23:54:06 -0700 Subject: [PATCH 408/431] Bumped version to 0.108.0b3 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index e4e08f76356..981d5e41916 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b2" +PATCH_VERSION = "0b3" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 5fd8763c3c4cb0e3ec9ec519b49bfed6c504a2fd Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Apr 2020 12:15:11 -0500 Subject: [PATCH 409/431] Skip parsing Plex session if incomplete (#33534) * Skip parsing session if incomplete * Schedule an update if session data is incomplete * Mark as callback * Remove update() & convert to async, abort if any session is incomplete --- homeassistant/components/plex/sensor.py | 109 ++++++++++++++---------- 1 file changed, 62 insertions(+), 47 deletions(-) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index b1e93aec8c0..65d1ba0371b 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -2,14 +2,16 @@ import logging from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send from homeassistant.helpers.entity import Entity +from homeassistant.helpers.event import async_call_later from .const import ( CONF_SERVER_IDENTIFIER, DISPATCHERS, DOMAIN as PLEX_DOMAIN, NAME_FORMAT, + PLEX_UPDATE_PLATFORMS_SIGNAL, PLEX_UPDATE_SENSOR_SIGNAL, SERVERS, ) @@ -55,11 +57,67 @@ class PlexSensor(Entity): ) self.hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) - @callback - def async_refresh_sensor(self, sessions): + async def async_refresh_sensor(self, sessions): """Set instance object and trigger an entity state update.""" + _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) + self.sessions = sessions - self.async_schedule_update_ha_state(True) + + @callback + def update_plex(_): + dispatcher_send( + self.hass, + PLEX_UPDATE_PLATFORMS_SIGNAL.format(self._server.machine_identifier), + ) + + now_playing = [] + for sess in self.sessions: + if sess.TYPE == "photo": + _LOGGER.debug("Photo session detected, skipping: %s", sess) + continue + if not sess.usernames: + _LOGGER.debug( + "Session temporarily incomplete, will try again: %s", sess + ) + async_call_later(self.hass, 5, update_plex) + return + user = sess.usernames[0] + device = sess.players[0].title + now_playing_user = f"{user} - {device}" + now_playing_title = "" + + if sess.TYPE in ["clip", "episode"]: + # example: + # "Supernatural (2005) - s01e13 - Route 666" + season_title = sess.grandparentTitle + show = await self.hass.async_add_executor_job(sess.show) + if show.year is not None: + season_title += f" ({show.year!s})" + season_episode = sess.seasonEpisode + episode_title = sess.title + now_playing_title = ( + f"{season_title} - {season_episode} - {episode_title}" + ) + elif sess.TYPE == "track": + # example: + # "Billy Talent - Afraid of Heights - Afraid of Heights" + track_artist = sess.grandparentTitle + track_album = sess.parentTitle + track_title = sess.title + now_playing_title = f"{track_artist} - {track_album} - {track_title}" + else: + # example: + # "picture_of_last_summer_camp (2015)" + # "The Incredible Hulk (2008)" + now_playing_title = sess.title + if sess.year is not None: + now_playing_title += f" ({sess.year})" + + now_playing.append((now_playing_user, now_playing_title)) + self._state = len(self.sessions) + self._now_playing = now_playing + + self.async_write_ha_state() @property def name(self): @@ -96,49 +154,6 @@ class PlexSensor(Entity): """Return the state attributes.""" return {content[0]: content[1] for content in self._now_playing} - def update(self): - """Update method for Plex sensor.""" - _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) - now_playing = [] - for sess in self.sessions: - if sess.TYPE == "photo": - _LOGGER.debug("Photo session detected, skipping: %s", sess) - continue - user = sess.usernames[0] - device = sess.players[0].title - now_playing_user = f"{user} - {device}" - now_playing_title = "" - - if sess.TYPE in ["clip", "episode"]: - # example: - # "Supernatural (2005) - s01e13 - Route 666" - season_title = sess.grandparentTitle - if sess.show().year is not None: - season_title += f" ({sess.show().year!s})" - season_episode = sess.seasonEpisode - episode_title = sess.title - now_playing_title = ( - f"{season_title} - {season_episode} - {episode_title}" - ) - elif sess.TYPE == "track": - # example: - # "Billy Talent - Afraid of Heights - Afraid of Heights" - track_artist = sess.grandparentTitle - track_album = sess.parentTitle - track_title = sess.title - now_playing_title = f"{track_artist} - {track_album} - {track_title}" - else: - # example: - # "picture_of_last_summer_camp (2015)" - # "The Incredible Hulk (2008)" - now_playing_title = sess.title - if sess.year is not None: - now_playing_title += f" ({sess.year})" - - now_playing.append((now_playing_user, now_playing_title)) - self._state = len(self.sessions) - self._now_playing = now_playing - @property def device_info(self): """Return a device description for device registry.""" From 49dc7ffb5be5826d08d088c7f944a797c740ac5d Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2020 17:23:20 -0500 Subject: [PATCH 410/431] Fix nuheat response error checking (#33712) This integration was checking request instead of response for the error code. --- homeassistant/components/nuheat/__init__.py | 2 +- .../components/nuheat/config_flow.py | 2 +- tests/components/nuheat/test_config_flow.py | 25 +++++++++++++++++-- 3 files changed, 25 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/nuheat/__init__.py b/homeassistant/components/nuheat/__init__.py index ff90bb26530..ca47f831370 100644 --- a/homeassistant/components/nuheat/__init__.py +++ b/homeassistant/components/nuheat/__init__.py @@ -78,7 +78,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except requests.exceptions.Timeout: raise ConfigEntryNotReady except requests.exceptions.HTTPError as ex: - if ex.request.status_code > 400 and ex.request.status_code < 500: + if ex.response.status_code > 400 and ex.response.status_code < 500: _LOGGER.error("Failed to login to nuheat: %s", ex) return False raise ConfigEntryNotReady diff --git a/homeassistant/components/nuheat/config_flow.py b/homeassistant/components/nuheat/config_flow.py index 082cb899ec5..4f12f590057 100644 --- a/homeassistant/components/nuheat/config_flow.py +++ b/homeassistant/components/nuheat/config_flow.py @@ -34,7 +34,7 @@ async def validate_input(hass: core.HomeAssistant, data): except requests.exceptions.Timeout: raise CannotConnect except requests.exceptions.HTTPError as ex: - if ex.request.status_code > 400 and ex.request.status_code < 500: + if ex.response.status_code > 400 and ex.response.status_code < 500: raise InvalidAuth raise CannotConnect # diff --git a/tests/components/nuheat/test_config_flow.py b/tests/components/nuheat/test_config_flow.py index 95987404e44..d6e10e1dc7c 100644 --- a/tests/components/nuheat/test_config_flow.py +++ b/tests/components/nuheat/test_config_flow.py @@ -1,5 +1,5 @@ """Test the NuHeat config flow.""" -from asynctest import patch +from asynctest import MagicMock, patch import requests from homeassistant import config_entries, setup @@ -100,6 +100,24 @@ async def test_form_invalid_auth(hass): with patch( "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", side_effect=Exception, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_SERIAL_NUMBER: "12345", + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result["type"] == "form" + assert result["errors"] == {"base": "invalid_auth"} + + response_mock = MagicMock() + type(response_mock).status_code = 401 + with patch( + "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", + side_effect=requests.HTTPError(response=response_mock), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -120,12 +138,15 @@ async def test_form_invalid_thermostat(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) + response_mock = MagicMock() + type(response_mock).status_code = 500 + with patch( "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.authenticate", return_value=True, ), patch( "homeassistant.components.nuheat.config_flow.nuheat.NuHeat.get_thermostat", - side_effect=requests.exceptions.HTTPError, + side_effect=requests.HTTPError(response=response_mock), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], From 565b54d8527d6a843a47f2d8c9920f1f23e41bc5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2020 17:25:31 -0500 Subject: [PATCH 411/431] Fix rachio import of run time from yaml (#33723) Importing from yaml would fail for rachio when copying the manual run time to the option flow. --- homeassistant/components/rachio/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/components/rachio/__init__.py b/homeassistant/components/rachio/__init__.py index 9bd3b16d12c..8879bd6965c 100644 --- a/homeassistant/components/rachio/__init__.py +++ b/homeassistant/components/rachio/__init__.py @@ -86,7 +86,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): if not options.get(CONF_MANUAL_RUN_MINS) and config.get(CONF_MANUAL_RUN_MINS): options_copy = options.copy() options_copy[CONF_MANUAL_RUN_MINS] = config[CONF_MANUAL_RUN_MINS] - hass.config_entries.async_update_entry(options=options_copy) + hass.config_entries.async_update_entry(entry, options=options_copy) # Configure API api_key = config[CONF_API_KEY] From 8392406476018b5688be595eab864f6df114c1b1 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Apr 2020 11:57:51 -0500 Subject: [PATCH 412/431] Fix Plex debounce wrapper (#33730) * Fix debounce wrapper by converting to async * Review suggestions --- homeassistant/components/plex/__init__.py | 2 +- homeassistant/components/plex/media_player.py | 8 ------- homeassistant/components/plex/server.py | 21 ++++++++++++------- 3 files changed, 14 insertions(+), 17 deletions(-) diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index a73111793a7..ff36f4f5c32 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -175,7 +175,7 @@ async def async_setup_entry(hass, entry): unsub = async_dispatcher_connect( hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(server_id), - plex_server.update_platforms, + plex_server.async_update_platforms, ) hass.data[PLEX_DOMAIN][DISPATCHERS].setdefault(server_id, []) hass.data[PLEX_DOMAIN][DISPATCHERS][server_id].append(unsub) diff --git a/homeassistant/components/plex/media_player.py b/homeassistant/components/plex/media_player.py index aea8ecadaff..e09244739e9 100644 --- a/homeassistant/components/plex/media_player.py +++ b/homeassistant/components/plex/media_player.py @@ -502,7 +502,6 @@ class PlexMediaPlayer(MediaPlayerDevice): if self.device and "playback" in self._device_protocol_capabilities: self.device.setVolume(int(volume * 100), self._active_media_plexapi_type) self._volume_level = volume # store since we can't retrieve - self.plex_server.update_platforms() @property def volume_level(self): @@ -541,31 +540,26 @@ class PlexMediaPlayer(MediaPlayerDevice): """Send play command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.play(self._active_media_plexapi_type) - self.plex_server.update_platforms() def media_pause(self): """Send pause command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.pause(self._active_media_plexapi_type) - self.plex_server.update_platforms() def media_stop(self): """Send stop command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.stop(self._active_media_plexapi_type) - self.plex_server.update_platforms() def media_next_track(self): """Send next track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipNext(self._active_media_plexapi_type) - self.plex_server.update_platforms() def media_previous_track(self): """Send previous track command.""" if self.device and "playback" in self._device_protocol_capabilities: self.device.skipPrevious(self._active_media_plexapi_type) - self.plex_server.update_platforms() def play_media(self, media_type, media_id, **kwargs): """Play a piece of media.""" @@ -602,8 +596,6 @@ class PlexMediaPlayer(MediaPlayerDevice): except requests.exceptions.ConnectTimeout: _LOGGER.error("Timed out playing on %s", self.name) - self.plex_server.update_platforms() - def _get_music_media(self, library_name, src): """Find music media and return a Plex media object.""" artist_name = src["artist_name"] diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index 80e7c92640a..a7b66c3a3ba 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -12,7 +12,7 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from homeassistant.helpers.dispatcher import dispatcher_send +from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send from homeassistant.helpers.event import async_call_later from .const import ( @@ -51,10 +51,10 @@ def debounce(func): """Handle call_later callback.""" nonlocal unsub unsub = None - await self.hass.async_add_executor_job(func, self) + await func(self) @wraps(func) - def wrapper(self): + async def wrapper(self): """Schedule async callback.""" nonlocal unsub if unsub: @@ -186,8 +186,12 @@ class PlexServer: session, ) + def _fetch_platform_data(self): + """Fetch all data from the Plex server in a single method.""" + return (self._plex_server.clients(), self._plex_server.sessions()) + @debounce - def update_platforms(self): + async def async_update_platforms(self): """Update the platform entities.""" _LOGGER.debug("Updating devices") @@ -209,8 +213,9 @@ class PlexServer: monitored_users.add(new_user) try: - devices = self._plex_server.clients() - sessions = self._plex_server.sessions() + devices, sessions = await self.hass.async_add_executor_job( + self._fetch_platform_data + ) except ( plexapi.exceptions.BadRequest, requests.exceptions.RequestException, @@ -271,13 +276,13 @@ class PlexServer: self._known_idle.add(client_id) if new_entity_configs: - dispatcher_send( + async_dispatcher_send( self.hass, PLEX_NEW_MP_SIGNAL.format(self.machine_identifier), new_entity_configs, ) - dispatcher_send( + async_dispatcher_send( self.hass, PLEX_UPDATE_SENSOR_SIGNAL.format(self.machine_identifier), sessions, From 8a68b1a3a9b5e7775e71401b8c4925936b6334c6 Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Mon, 6 Apr 2020 19:25:09 +0200 Subject: [PATCH 413/431] Fix MQTT debug info for subscriptions with wildcard. (#33744) --- homeassistant/components/mqtt/__init__.py | 17 ++++++++++++----- homeassistant/components/mqtt/debug_info.py | 2 +- homeassistant/components/mqtt/models.py | 1 + 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/mqtt/__init__.py b/homeassistant/components/mqtt/__init__.py index 734f67906ce..c51f94992f5 100644 --- a/homeassistant/components/mqtt/__init__.py +++ b/homeassistant/components/mqtt/__init__.py @@ -389,7 +389,7 @@ def wrap_msg_callback(msg_callback: MessageCallbackType) -> MessageCallbackType: @wraps(msg_callback) async def async_wrapper(msg: Any) -> None: - """Catch and log exception.""" + """Call with deprecated signature.""" await msg_callback(msg.topic, msg.payload, msg.qos) wrapper_func = async_wrapper @@ -397,7 +397,7 @@ def wrap_msg_callback(msg_callback: MessageCallbackType) -> MessageCallbackType: @wraps(msg_callback) def wrapper(msg: Any) -> None: - """Catch and log exception.""" + """Call with deprecated signature.""" msg_callback(msg.topic, msg.payload, msg.qos) wrapper_func = wrapper @@ -809,7 +809,10 @@ class MQTT: if will_message is not None: self._mqttc.will_set( # pylint: disable=no-value-for-parameter - *attr.astuple(will_message) + *attr.astuple( + will_message, + filter=lambda attr, value: attr.name != "subscribed_topic", + ) ) async def async_publish( @@ -941,7 +944,10 @@ class MQTT: if self.birth_message: self.hass.add_job( self.async_publish( # pylint: disable=no-value-for-parameter - *attr.astuple(self.birth_message) + *attr.astuple( + self.birth_message, + filter=lambda attr, value: attr.name != "subscribed_topic", + ) ) ) @@ -977,7 +983,8 @@ class MQTT: continue self.hass.async_run_job( - subscription.callback, Message(msg.topic, payload, msg.qos, msg.retain) + subscription.callback, + Message(msg.topic, payload, msg.qos, msg.retain, subscription.topic), ) def _mqtt_on_disconnect(self, _mqttc, _userdata, result_code: int) -> None: diff --git a/homeassistant/components/mqtt/debug_info.py b/homeassistant/components/mqtt/debug_info.py index ec4ff1676bb..b51ee619a12 100644 --- a/homeassistant/components/mqtt/debug_info.py +++ b/homeassistant/components/mqtt/debug_info.py @@ -21,7 +21,7 @@ def log_messages(hass: HomeAssistantType, entity_id: str) -> MessageCallbackType def _log_message(msg): """Log message.""" debug_info = hass.data[DATA_MQTT_DEBUG_INFO] - messages = debug_info["entities"][entity_id]["topics"][msg.topic] + messages = debug_info["entities"][entity_id]["topics"][msg.subscribed_topic] messages.append(msg.payload) def _decorator(msg_callback: MessageCallbackType): diff --git a/homeassistant/components/mqtt/models.py b/homeassistant/components/mqtt/models.py index cfdecd3383d..3a4add57298 100644 --- a/homeassistant/components/mqtt/models.py +++ b/homeassistant/components/mqtt/models.py @@ -14,6 +14,7 @@ class Message: payload = attr.ib(type=PublishPayloadType) qos = attr.ib(type=int) retain = attr.ib(type=bool) + subscribed_topic = attr.ib(type=str, default=None) MessageCallbackType = Callable[[Message], None] From 69b98def5c83943896759501f69429494ffa8c7f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Apr 2020 12:24:08 -0500 Subject: [PATCH 414/431] =?UTF-8?q?Abort=20rachio=20config=20flow=20if=20t?= =?UTF-8?q?he=20api=20key=20is=20already=20configured=E2=80=A6=20(#33747)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We now abort before hitting the api which can be slow and block startup if importing from yaml. --- homeassistant/components/rachio/config_flow.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/rachio/config_flow.py b/homeassistant/components/rachio/config_flow.py index 9eff7c99334..64e78a24f4a 100644 --- a/homeassistant/components/rachio/config_flow.py +++ b/homeassistant/components/rachio/config_flow.py @@ -62,9 +62,11 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input is not None: + await self.async_set_unique_id(user_input[CONF_API_KEY]) + self._abort_if_unique_id_configured() try: info = await validate_input(self.hass, user_input) - + return self.async_create_entry(title=info["title"], data=user_input) except CannotConnect: errors["base"] = "cannot_connect" except InvalidAuth: @@ -73,11 +75,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" - if "base" not in errors: - await self.async_set_unique_id(user_input[CONF_API_KEY]) - 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 ) From de2eab30fa1b1b1f10eabd0eccef001668fec6df Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Apr 2020 10:34:33 -0700 Subject: [PATCH 415/431] Bumped version to 0.108.0b4 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 981d5e41916..b282f0a3d62 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b3" +PATCH_VERSION = "0b4" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From e1e768fa6518ce8e9b8d63cce2435202cf18b5f1 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 6 Apr 2020 22:23:22 +0200 Subject: [PATCH 416/431] Bump frontend (#33751) --- 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 b0da48ab713..3c6e8478c09 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==20200403.0" - ], + "requirements": ["home-assistant-frontend==20200406.0"], "dependencies": [ "api", "auth", @@ -16,8 +14,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 fd8e841d568..efc36dc1561 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200403.0 +home-assistant-frontend==20200406.0 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 93f8621355f..5cd0275ad83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200403.0 +home-assistant-frontend==20200406.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c4d80a32788..958bacf633e 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200403.0 +home-assistant-frontend==20200406.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From e3d90f53cc877070c51140d3270fef6e21096509 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Mon, 6 Apr 2020 18:18:13 -0500 Subject: [PATCH 417/431] Defer Plex sensor retry instead of aborting (#33753) --- homeassistant/components/plex/sensor.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index 65d1ba0371b..ab6985c0c43 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -62,6 +62,7 @@ class PlexSensor(Entity): _LOGGER.debug("Refreshing sensor [%s]", self.unique_id) self.sessions = sessions + update_failed = False @callback def update_plex(_): @@ -79,8 +80,8 @@ class PlexSensor(Entity): _LOGGER.debug( "Session temporarily incomplete, will try again: %s", sess ) - async_call_later(self.hass, 5, update_plex) - return + update_failed = True + continue user = sess.usernames[0] device = sess.players[0].title now_playing_user = f"{user} - {device}" @@ -119,6 +120,9 @@ class PlexSensor(Entity): self.async_write_ha_state() + if update_failed: + async_call_later(self.hass, 5, update_plex) + @property def name(self): """Return the name of the sensor.""" From 33849a15a83506171f422cc8e2180b6685d8d10e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 6 Apr 2020 16:30:10 -0500 Subject: [PATCH 418/431] Bump HAP-python to 2.8.1 (#33756) --- homeassistant/components/homekit/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index eb8d16d0c0a..b0c49a58a6a 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -2,7 +2,6 @@ "domain": "homekit", "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", - "requirements": ["HAP-python==2.8.0"], - "dependencies": [], + "requirements": ["HAP-python==2.8.1"], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 5cd0275ad83..080b033ebe3 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -35,7 +35,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==2.8.0 +HAP-python==2.8.1 # homeassistant.components.mastodon Mastodon.py==1.5.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 958bacf633e..651ad377b3d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,7 +4,7 @@ -r requirements_test.txt # homeassistant.components.homekit -HAP-python==2.8.0 +HAP-python==2.8.1 # homeassistant.components.mobile_app # homeassistant.components.owntracks From e8da7f333bb8b34fac5c1ed15c8d0538b1c39422 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Apr 2020 16:14:27 -0600 Subject: [PATCH 419/431] Bump aioambient to 1.1.1 (#33761) --- homeassistant/components/ambient_station/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/ambient_station/manifest.json b/homeassistant/components/ambient_station/manifest.json index 8c4bc1b3cc0..e73190bb580 100644 --- a/homeassistant/components/ambient_station/manifest.json +++ b/homeassistant/components/ambient_station/manifest.json @@ -3,7 +3,6 @@ "name": "Ambient Weather Station", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/ambient_station", - "requirements": ["aioambient==1.1.0"], - "dependencies": [], + "requirements": ["aioambient==1.1.1"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 080b033ebe3..1bea2a07f8d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -140,7 +140,7 @@ aio_geojson_nsw_rfs_incidents==0.3 aio_georss_gdacs==0.3 # homeassistant.components.ambient_station -aioambient==1.1.0 +aioambient==1.1.1 # homeassistant.components.asuswrt aioasuswrt==1.2.3 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 651ad377b3d..cc3d0de9a8b 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -47,7 +47,7 @@ aio_geojson_nsw_rfs_incidents==0.3 aio_georss_gdacs==0.3 # homeassistant.components.ambient_station -aioambient==1.1.0 +aioambient==1.1.1 # homeassistant.components.asuswrt aioasuswrt==1.2.3 From 087ddcb6820c5a0d14b695b50b8272f54e778ae7 Mon Sep 17 00:00:00 2001 From: Aaron Bach Date: Mon, 6 Apr 2020 16:28:42 -0600 Subject: [PATCH 420/431] Bump simplisafe-python to 9.0.6 (#33762) --- homeassistant/components/simplisafe/manifest.json | 3 +-- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 917722a61b8..cd0cda68125 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,6 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==9.0.5"], - "dependencies": [], + "requirements": ["simplisafe-python==9.0.6"], "codeowners": ["@bachya"] } diff --git a/requirements_all.txt b/requirements_all.txt index 1bea2a07f8d..883be49d7c7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1870,7 +1870,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.0.5 +simplisafe-python==9.0.6 # homeassistant.components.sisyphus sisyphus-control==2.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index cc3d0de9a8b..553f707df9a 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -689,7 +689,7 @@ sentry-sdk==0.13.5 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.0.5 +simplisafe-python==9.0.6 # homeassistant.components.sleepiq sleepyq==0.7 From a33e5728de3f204a52ee5e7c00c0ce5b7bd5dec1 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 6 Apr 2020 16:34:47 -0700 Subject: [PATCH 421/431] Bumped version to 0.108.0b5 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index b282f0a3d62..0a6fb9ec971 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b4" +PATCH_VERSION = "0b5" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From abdee3fcb7ac7866274b9874e38b07d588140862 Mon Sep 17 00:00:00 2001 From: Chris Talkington Date: Tue, 7 Apr 2020 11:32:43 -0500 Subject: [PATCH 422/431] Catch IPPParseError during config flow (#33769) * Update config_flow.py * Update strings.json * Update config_flow.py * squash. --- homeassistant/components/ipp/config_flow.py | 19 +++++++++-- homeassistant/components/ipp/manifest.json | 3 +- homeassistant/components/ipp/strings.json | 3 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/ipp/test_config_flow.py | 38 +++++++++++++++++++++ 6 files changed, 59 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/ipp/config_flow.py b/homeassistant/components/ipp/config_flow.py index e95267e7803..fe0808414ad 100644 --- a/homeassistant/components/ipp/config_flow.py +++ b/homeassistant/components/ipp/config_flow.py @@ -2,7 +2,13 @@ import logging from typing import Any, Dict, Optional -from pyipp import IPP, IPPConnectionError, IPPConnectionUpgradeRequired +from pyipp import ( + IPP, + IPPConnectionError, + IPPConnectionUpgradeRequired, + IPPParseError, + IPPResponseError, +) import voluptuous as vol from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, ConfigFlow @@ -63,8 +69,12 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, user_input) except IPPConnectionUpgradeRequired: return self._show_setup_form({"base": "connection_upgrade"}) - except IPPConnectionError: + except (IPPConnectionError, IPPResponseError): return self._show_setup_form({"base": "connection_error"}) + except IPPParseError: + _LOGGER.exception("IPP Parse Error") + return self.async_abort(reason="parse_error") + user_input[CONF_UUID] = info[CONF_UUID] await self.async_set_unique_id(user_input[CONF_UUID]) @@ -100,8 +110,11 @@ class IPPFlowHandler(ConfigFlow, domain=DOMAIN): info = await validate_input(self.hass, self.discovery_info) except IPPConnectionUpgradeRequired: return self.async_abort(reason="connection_upgrade") - except IPPConnectionError: + except (IPPConnectionError, IPPResponseError): return self.async_abort(reason="connection_error") + except IPPParseError: + _LOGGER.exception("IPP Parse Error") + return self.async_abort(reason="parse_error") self.discovery_info[CONF_UUID] = info[CONF_UUID] diff --git a/homeassistant/components/ipp/manifest.json b/homeassistant/components/ipp/manifest.json index 0cb788eeee7..9e491a54896 100644 --- a/homeassistant/components/ipp/manifest.json +++ b/homeassistant/components/ipp/manifest.json @@ -2,8 +2,7 @@ "domain": "ipp", "name": "Internet Printing Protocol (IPP)", "documentation": "https://www.home-assistant.io/integrations/ipp", - "requirements": ["pyipp==0.8.3"], - "dependencies": [], + "requirements": ["pyipp==0.9.0"], "codeowners": ["@ctalkington"], "config_flow": true, "quality_scale": "platinum", diff --git a/homeassistant/components/ipp/strings.json b/homeassistant/components/ipp/strings.json index afd82d1f454..a80a7f2e0ba 100644 --- a/homeassistant/components/ipp/strings.json +++ b/homeassistant/components/ipp/strings.json @@ -26,7 +26,8 @@ "abort": { "already_configured": "This printer is already configured.", "connection_error": "Failed to connect to printer.", - "connection_upgrade": "Failed to connect to printer due to connection upgrade being required." + "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", + "parse_error": "Failed to parse response from printer." } } } diff --git a/requirements_all.txt b/requirements_all.txt index 883be49d7c7..95012a82cd7 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1336,7 +1336,7 @@ pyintesishome==1.7.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.3 +pyipp==0.9.0 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 553f707df9a..df5fa4ac34d 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -519,7 +519,7 @@ pyicloud==0.9.6.1 pyipma==2.0.5 # homeassistant.components.ipp -pyipp==0.8.3 +pyipp==0.9.0 # homeassistant.components.iqvia pyiqvia==0.2.1 diff --git a/tests/components/ipp/test_config_flow.py b/tests/components/ipp/test_config_flow.py index 0682929b7b8..7e16a9fc6e0 100644 --- a/tests/components/ipp/test_config_flow.py +++ b/tests/components/ipp/test_config_flow.py @@ -134,6 +134,44 @@ async def test_zeroconf_connection_upgrade_required( assert result["reason"] == "connection_upgrade" +async def test_user_parse_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort user flow on IPP parse error.""" + aioclient_mock.post( + "http://192.168.1.31:631/ipp/print", + content="BAD", + headers={"Content-Type": "application/ipp"}, + ) + + user_input = MOCK_USER_INPUT.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=user_input, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "parse_error" + + +async def test_zeroconf_parse_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on IPP parse error.""" + aioclient_mock.post( + "http://192.168.1.31:631/ipp/print", + content="BAD", + headers={"Content-Type": "application/ipp"}, + ) + + discovery_info = MOCK_ZEROCONF_IPP_SERVICE_INFO.copy() + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_ZEROCONF}, data=discovery_info, + ) + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "parse_error" + + async def test_user_device_exists_abort( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: From 1bd1b8339d23142e79a3e87cc1944b0af4a9c7fe Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2020 10:33:43 -0500 Subject: [PATCH 423/431] Update nexia for thermostats without zoning (#33770) * Bump nexia to 0.8.0 --- homeassistant/components/nexia/manifest.json | 9 ++------- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- 3 files changed, 4 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/nexia/manifest.json b/homeassistant/components/nexia/manifest.json index 06130f605ef..e69ea352c8e 100644 --- a/homeassistant/components/nexia/manifest.json +++ b/homeassistant/components/nexia/manifest.json @@ -1,13 +1,8 @@ { "domain": "nexia", "name": "Nexia", - "requirements": [ - "nexia==0.7.3" - ], - "dependencies": [], - "codeowners": [ - "@ryannazaretian", "@bdraco" - ], + "requirements": ["nexia==0.8.0"], + "codeowners": ["@ryannazaretian", "@bdraco"], "documentation": "https://www.home-assistant.io/integrations/nexia", "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 95012a82cd7..6d0e0e590eb 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -922,7 +922,7 @@ netdisco==2.6.0 neurio==0.3.1 # homeassistant.components.nexia -nexia==0.7.3 +nexia==0.8.0 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index df5fa4ac34d..8a4af0beaf0 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -357,7 +357,7 @@ nessclient==0.9.15 netdisco==2.6.0 # homeassistant.components.nexia -nexia==0.7.3 +nexia==0.8.0 # homeassistant.components.nsw_fuel_station nsw-fuel-api-client==1.0.10 From b9336272d474226bbdeb86c5d9a5eed40d803ff8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Tue, 7 Apr 2020 12:16:31 -0500 Subject: [PATCH 424/431] Fix nuheat reverting to auto mode after setting temp hold (#33772) * Fix nuheat reverting to auto mode after setting temp hold * clamp temp --- homeassistant/components/nuheat/climate.py | 31 +++++++++++++++++++--- homeassistant/components/nuheat/const.py | 9 ++++++- 2 files changed, 35 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/nuheat/climate.py b/homeassistant/components/nuheat/climate.py index f8d6bf1d8df..c1d591c03eb 100644 --- a/homeassistant/components/nuheat/climate.py +++ b/homeassistant/components/nuheat/climate.py @@ -1,6 +1,7 @@ """Support for NuHeat thermostats.""" -from datetime import timedelta +from datetime import datetime, timedelta import logging +import time from nuheat.config import SCHEDULE_HOLD, SCHEDULE_RUN, SCHEDULE_TEMPORARY_HOLD from nuheat.util import ( @@ -24,7 +25,16 @@ from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers import event as event_helper from homeassistant.util import Throttle -from .const import DOMAIN, MANUFACTURER, NUHEAT_API_STATE_SHIFT_DELAY +from .const import ( + DOMAIN, + MANUFACTURER, + NUHEAT_API_STATE_SHIFT_DELAY, + NUHEAT_DATETIME_FORMAT, + NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME, + NUHEAT_KEY_SCHEDULE_MODE, + NUHEAT_KEY_SET_POINT_TEMP, + TEMP_HOLD_TIME_SEC, +) _LOGGER = logging.getLogger(__name__) @@ -218,9 +228,22 @@ class NuHeatThermostat(ClimateDevice): target_schedule_mode, ) - self._thermostat.set_target_temperature( - target_temperature, target_schedule_mode + target_temperature = max( + min(self._thermostat.max_temperature, target_temperature), + self._thermostat.min_temperature, ) + + request = { + NUHEAT_KEY_SET_POINT_TEMP: target_temperature, + NUHEAT_KEY_SCHEDULE_MODE: target_schedule_mode, + } + + if target_schedule_mode == SCHEDULE_TEMPORARY_HOLD: + request[NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME] = datetime.fromtimestamp( + time.time() + TEMP_HOLD_TIME_SEC + ).strftime(NUHEAT_DATETIME_FORMAT) + + self._thermostat.set_data(request) self._schedule_mode = target_schedule_mode self._target_temperature = target_temperature self._schedule_update() diff --git a/homeassistant/components/nuheat/const.py b/homeassistant/components/nuheat/const.py index 1bb6c3825e7..bd44dcb1711 100644 --- a/homeassistant/components/nuheat/const.py +++ b/homeassistant/components/nuheat/const.py @@ -8,4 +8,11 @@ CONF_SERIAL_NUMBER = "serial_number" MANUFACTURER = "NuHeat" -NUHEAT_API_STATE_SHIFT_DELAY = 4 +NUHEAT_API_STATE_SHIFT_DELAY = 1 + +TEMP_HOLD_TIME_SEC = 43200 + +NUHEAT_KEY_SET_POINT_TEMP = "SetPointTemp" +NUHEAT_KEY_SCHEDULE_MODE = "ScheduleMode" +NUHEAT_KEY_HOLD_SET_POINT_DATE_TIME = "HoldSetPointDateTime" +NUHEAT_DATETIME_FORMAT = "%a, %d %b %Y %H:%M:%S GMT" From d92d74a14f1d6a61b48051224d1407443cb26547 Mon Sep 17 00:00:00 2001 From: jjlawren Date: Tue, 7 Apr 2020 12:17:16 -0500 Subject: [PATCH 425/431] Fix minor async issues in Plex (#33785) * Fix minor async context issues * Annotate callback --- homeassistant/components/plex/sensor.py | 7 +++++-- homeassistant/components/plex/server.py | 12 +++++++----- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/homeassistant/components/plex/sensor.py b/homeassistant/components/plex/sensor.py index ab6985c0c43..6fcfd39d192 100644 --- a/homeassistant/components/plex/sensor.py +++ b/homeassistant/components/plex/sensor.py @@ -2,7 +2,10 @@ import logging from homeassistant.core import callback -from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) from homeassistant.helpers.entity import Entity from homeassistant.helpers.event import async_call_later @@ -66,7 +69,7 @@ class PlexSensor(Entity): @callback def update_plex(_): - dispatcher_send( + async_dispatcher_send( self.hass, PLEX_UPDATE_PLATFORMS_SIGNAL.format(self._server.machine_identifier), ) diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index a7b66c3a3ba..4134ad4e32b 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -12,7 +12,8 @@ import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL -from homeassistant.helpers.dispatcher import async_dispatcher_send, dispatcher_send +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later from .const import ( @@ -175,11 +176,12 @@ class PlexServer: if config_entry_update_needed: raise ShouldUpdateConfigEntry - def refresh_entity(self, machine_identifier, device, session): + @callback + def async_refresh_entity(self, machine_identifier, device, session): """Forward refresh dispatch to media_player.""" unique_id = f"{self.machine_identifier}:{machine_identifier}" _LOGGER.debug("Refreshing %s", unique_id) - dispatcher_send( + async_dispatcher_send( self.hass, PLEX_UPDATE_MEDIA_PLAYER_SIGNAL.format(unique_id), device, @@ -262,7 +264,7 @@ class PlexServer: if client_id in new_clients: new_entity_configs.append(client_data) else: - self.refresh_entity( + self.async_refresh_entity( client_id, client_data["device"], client_data.get("session") ) @@ -272,7 +274,7 @@ class PlexServer: self._known_clients - self._known_idle - ignored_clients ).difference(available_clients) for client_id in idle_clients: - self.refresh_entity(client_id, None, None) + self.async_refresh_entity(client_id, None, None) self._known_idle.add(client_id) if new_entity_configs: From 4901fa24ec2aa3f6f6bc3cd7ce8c9602a9a0df38 Mon Sep 17 00:00:00 2001 From: Ziv <16467659+ziv1234@users.noreply.github.com> Date: Mon, 6 Apr 2020 14:36:49 +0300 Subject: [PATCH 426/431] Fix unhandled exceptions for config, default_config, harmony (#33731) * replaced MagicMock with CoroutineMock to avoid exception * added conversion to str so mock returns unique-id that doesn't throw json exception * added non-empty config since hass throws exception when config is empty --- homeassistant/components/harmony/util.py | 2 +- tests/components/config/test_group.py | 6 ++++-- tests/components/default_config/test_init.py | 2 +- tests/ignore_uncaught_exceptions.py | 2 -- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/homeassistant/components/harmony/util.py b/homeassistant/components/harmony/util.py index 69ed44cb7da..412aa2c6940 100644 --- a/homeassistant/components/harmony/util.py +++ b/homeassistant/components/harmony/util.py @@ -11,7 +11,7 @@ def find_unique_id_for_remote(harmony: HarmonyAPI): """Find the unique id for both websocket and xmpp clients.""" websocket_unique_id = harmony.hub_config.info.get("activeRemoteId") if websocket_unique_id is not None: - return websocket_unique_id + return str(websocket_unique_id) # fallback to the xmpp unique id if websocket is not available return harmony.config["global"]["timeStampHash"].split(";")[-1] diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index 49d168e2796..d00e0317e9e 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,6 +1,8 @@ """Test Group config panel.""" import json -from unittest.mock import MagicMock, patch +from unittest.mock import patch + +from asynctest import CoroutineMock from homeassistant.bootstrap import async_setup_component from homeassistant.components import config @@ -50,7 +52,7 @@ async def test_update_device_config(hass, hass_client): """Mock writing data.""" written.append(data) - mock_call = MagicMock() + mock_call = CoroutineMock() with patch("homeassistant.components.config._read", mock_read), patch( "homeassistant.components.config._write", mock_write diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 6b9004595bb..638130a0ab6 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -34,4 +34,4 @@ def recorder_url_mock(): async def test_setup(hass): """Test setup.""" - assert await async_setup_component(hass, "default_config", {}) + assert await async_setup_component(hass, "default_config", {"foo": "bar"}) diff --git a/tests/ignore_uncaught_exceptions.py b/tests/ignore_uncaught_exceptions.py index df623a2fc20..47242be8a5a 100644 --- a/tests/ignore_uncaught_exceptions.py +++ b/tests/ignore_uncaught_exceptions.py @@ -4,8 +4,6 @@ IGNORE_UNCAUGHT_EXCEPTIONS = [ ("tests.components.cast.test_media_player", "test_entry_setup_single_config"), ("tests.components.cast.test_media_player", "test_entry_setup_list_config"), ("tests.components.cast.test_media_player", "test_entry_setup_platform_not_ready"), - ("tests.components.config.test_group", "test_update_device_config"), - ("tests.components.default_config.test_init", "test_setup"), ("tests.components.demo.test_init", "test_setting_up_demo"), ("tests.components.discovery.test_init", "test_discover_config_flow"), ("tests.components.dsmr.test_sensor", "test_default_setup"), From 265666b75a4b4d3b8663a2aafbd80b9852d65ff2 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 7 Apr 2020 10:29:24 -0700 Subject: [PATCH 427/431] Bumped version to 0.108.0b6 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 0a6fb9ec971..9b82885ce72 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b5" +PATCH_VERSION = "0b6" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 035b28045c04b206cfe65e11e7ba27d2ce972448 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 8 Apr 2020 13:02:45 +0200 Subject: [PATCH 428/431] Update translations --- .../.translations/synology_dsm.ca.json | 26 +++++++++++++ .../.translations/synology_dsm.da.json | 18 +++++++++ .../.translations/synology_dsm.en.json | 38 +++++++++++++++++++ .../.translations/synology_dsm.es.json | 26 +++++++++++++ .../.translations/synology_dsm.ko.json | 26 +++++++++++++ .../.translations/synology_dsm.lb.json | 26 +++++++++++++ .../.translations/synology_dsm.nl.json | 37 ++++++++++++++++++ .../.translations/synology_dsm.ru.json | 26 +++++++++++++ .../components/airly/.translations/bg.json | 1 - .../components/airly/.translations/ca.json | 1 - .../components/airly/.translations/da.json | 1 - .../components/airly/.translations/de.json | 1 - .../components/airly/.translations/en.json | 1 - .../airly/.translations/es-419.json | 1 - .../components/airly/.translations/es.json | 1 - .../components/airly/.translations/fr.json | 1 - .../components/airly/.translations/hu.json | 1 - .../components/airly/.translations/it.json | 1 - .../components/airly/.translations/ko.json | 1 - .../components/airly/.translations/lb.json | 1 - .../components/airly/.translations/nl.json | 1 - .../components/airly/.translations/no.json | 1 - .../components/airly/.translations/pl.json | 1 - .../components/airly/.translations/ru.json | 1 - .../components/airly/.translations/sl.json | 1 - .../components/airly/.translations/sv.json | 1 - .../airly/.translations/zh-Hant.json | 1 - .../airvisual/.translations/ca.json | 3 +- .../airvisual/.translations/de.json | 3 +- .../airvisual/.translations/en.json | 3 +- .../airvisual/.translations/es.json | 3 +- .../airvisual/.translations/fr.json | 3 +- .../airvisual/.translations/it.json | 3 +- .../airvisual/.translations/ko.json | 5 +-- .../airvisual/.translations/lb.json | 6 ++- .../airvisual/.translations/no.json | 3 +- .../airvisual/.translations/pl.json | 3 +- .../airvisual/.translations/ru.json | 5 +-- .../airvisual/.translations/sl.json | 3 +- .../airvisual/.translations/zh-Hant.json | 3 +- .../ambient_station/.translations/bg.json | 1 - .../ambient_station/.translations/ca.json | 1 - .../ambient_station/.translations/da.json | 1 - .../ambient_station/.translations/de.json | 1 - .../ambient_station/.translations/en.json | 1 - .../ambient_station/.translations/es-419.json | 1 - .../ambient_station/.translations/es.json | 1 - .../ambient_station/.translations/fr.json | 1 - .../ambient_station/.translations/hu.json | 1 - .../ambient_station/.translations/it.json | 1 - .../ambient_station/.translations/ko.json | 1 - .../ambient_station/.translations/lb.json | 1 - .../ambient_station/.translations/nl.json | 1 - .../ambient_station/.translations/no.json | 1 - .../ambient_station/.translations/pl.json | 1 - .../ambient_station/.translations/pt-BR.json | 1 - .../ambient_station/.translations/pt.json | 1 - .../ambient_station/.translations/ru.json | 1 - .../ambient_station/.translations/sl.json | 1 - .../ambient_station/.translations/sv.json | 1 - .../.translations/zh-Hans.json | 1 - .../.translations/zh-Hant.json | 1 - .../components/axis/.translations/ca.json | 3 +- .../components/axis/.translations/da.json | 3 +- .../components/axis/.translations/de.json | 3 +- .../components/axis/.translations/en.json | 3 +- .../components/axis/.translations/es.json | 3 +- .../components/axis/.translations/fr.json | 3 +- .../components/axis/.translations/hu.json | 3 -- .../components/axis/.translations/it.json | 3 +- .../components/axis/.translations/ko.json | 3 +- .../components/axis/.translations/lb.json | 3 +- .../components/axis/.translations/nl.json | 3 +- .../components/axis/.translations/no.json | 3 +- .../components/axis/.translations/pl.json | 3 +- .../components/axis/.translations/pt-BR.json | 3 +- .../components/axis/.translations/ru.json | 3 +- .../components/axis/.translations/sl.json | 3 +- .../components/axis/.translations/sv.json | 3 +- .../axis/.translations/zh-Hant.json | 3 +- .../binary_sensor/.translations/bg.json | 2 - .../binary_sensor/.translations/ca.json | 2 - .../binary_sensor/.translations/da.json | 2 - .../binary_sensor/.translations/de.json | 2 - .../binary_sensor/.translations/en.json | 2 - .../binary_sensor/.translations/es-419.json | 2 - .../binary_sensor/.translations/es.json | 2 - .../binary_sensor/.translations/fr.json | 2 - .../binary_sensor/.translations/hu.json | 2 - .../binary_sensor/.translations/it.json | 2 - .../binary_sensor/.translations/ko.json | 2 - .../binary_sensor/.translations/lb.json | 2 - .../binary_sensor/.translations/nl.json | 2 - .../binary_sensor/.translations/no.json | 2 - .../binary_sensor/.translations/pl.json | 2 - .../binary_sensor/.translations/pt.json | 1 - .../binary_sensor/.translations/ru.json | 2 - .../binary_sensor/.translations/sl.json | 2 - .../binary_sensor/.translations/sv.json | 2 - .../binary_sensor/.translations/zh-Hans.json | 1 - .../binary_sensor/.translations/zh-Hant.json | 2 - .../brother/.translations/zh-Hant.json | 2 +- .../cert_expiry/.translations/bg.json | 9 +---- .../cert_expiry/.translations/ca.json | 7 +--- .../cert_expiry/.translations/da.json | 9 +---- .../cert_expiry/.translations/de.json | 7 +--- .../cert_expiry/.translations/en.json | 7 +--- .../cert_expiry/.translations/es-419.json | 9 +---- .../cert_expiry/.translations/es.json | 7 +--- .../cert_expiry/.translations/fr.json | 7 +--- .../cert_expiry/.translations/it.json | 7 +--- .../cert_expiry/.translations/ko.json | 7 +--- .../cert_expiry/.translations/lb.json | 10 ++--- .../cert_expiry/.translations/nl.json | 9 +---- .../cert_expiry/.translations/no.json | 7 +--- .../cert_expiry/.translations/pl.json | 7 +--- .../cert_expiry/.translations/pt-BR.json | 5 --- .../cert_expiry/.translations/ru.json | 7 +--- .../cert_expiry/.translations/sl.json | 7 +--- .../cert_expiry/.translations/sv.json | 9 +---- .../cert_expiry/.translations/zh-Hant.json | 7 +--- .../coronavirus/.translations/hu.json | 16 ++++++++ .../components/cover/.translations/hu.json | 8 ++++ .../components/cover/.translations/lb.json | 7 +++- .../components/deconz/.translations/bg.json | 18 --------- .../components/deconz/.translations/ca.json | 18 --------- .../components/deconz/.translations/cs.json | 7 ---- .../components/deconz/.translations/da.json | 18 --------- .../components/deconz/.translations/de.json | 18 --------- .../components/deconz/.translations/en.json | 18 --------- .../deconz/.translations/es-419.json | 22 ----------- .../components/deconz/.translations/es.json | 18 --------- .../components/deconz/.translations/fr.json | 18 --------- .../components/deconz/.translations/he.json | 7 ---- .../components/deconz/.translations/hr.json | 5 --- .../components/deconz/.translations/hu.json | 17 --------- .../components/deconz/.translations/id.json | 7 ---- .../components/deconz/.translations/it.json | 18 --------- .../components/deconz/.translations/ko.json | 18 --------- .../components/deconz/.translations/lb.json | 18 --------- .../components/deconz/.translations/nl.json | 18 --------- .../components/deconz/.translations/nn.json | 7 ---- .../components/deconz/.translations/no.json | 18 --------- .../components/deconz/.translations/pl.json | 18 --------- .../deconz/.translations/pt-BR.json | 22 ----------- .../components/deconz/.translations/pt.json | 7 ---- .../components/deconz/.translations/ru.json | 18 --------- .../components/deconz/.translations/sl.json | 18 --------- .../components/deconz/.translations/sv.json | 18 --------- .../components/deconz/.translations/vi.json | 7 ---- .../deconz/.translations/zh-Hans.json | 7 ---- .../deconz/.translations/zh-Hant.json | 18 --------- .../components/demo/.translations/lb.json | 16 +++++++- .../components/directv/.translations/ca.json | 3 +- .../components/directv/.translations/de.json | 3 +- .../components/directv/.translations/en.json | 3 +- .../components/directv/.translations/es.json | 3 +- .../components/directv/.translations/fr.json | 3 +- .../components/directv/.translations/it.json | 3 +- .../components/directv/.translations/ko.json | 3 +- .../components/directv/.translations/lb.json | 7 +++- .../components/directv/.translations/no.json | 3 +- .../components/directv/.translations/pl.json | 7 ++-- .../components/directv/.translations/ru.json | 3 +- .../components/directv/.translations/sl.json | 3 +- .../directv/.translations/zh-Hant.json | 3 +- .../components/doorbird/.translations/ca.json | 5 ++- .../components/doorbird/.translations/de.json | 1 + .../components/doorbird/.translations/en.json | 6 +-- .../components/doorbird/.translations/es.json | 2 + .../components/doorbird/.translations/ko.json | 5 ++- .../components/doorbird/.translations/lb.json | 1 + .../components/doorbird/.translations/ru.json | 5 ++- .../doorbird/.translations/zh-Hant.json | 1 + .../elgato/.translations/zh-Hant.json | 2 +- .../esphome/.translations/zh-Hant.json | 2 +- .../flunearyou/.translations/ca.json | 21 ++++++++++ .../flunearyou/.translations/de.json | 20 ++++++++++ .../flunearyou/.translations/en.json | 21 ++++++++++ .../flunearyou/.translations/es.json | 21 ++++++++++ .../flunearyou/.translations/ko.json | 21 ++++++++++ .../flunearyou/.translations/lb.json | 21 ++++++++++ .../flunearyou/.translations/ru.json | 21 ++++++++++ .../flunearyou/.translations/zh-Hant.json | 21 ++++++++++ .../components/freebox/.translations/lb.json | 1 + .../geonetnz_quakes/.translations/bg.json | 3 -- .../geonetnz_quakes/.translations/ca.json | 3 -- .../geonetnz_quakes/.translations/da.json | 3 -- .../geonetnz_quakes/.translations/de.json | 3 -- .../geonetnz_quakes/.translations/en.json | 3 -- .../geonetnz_quakes/.translations/es.json | 3 -- .../geonetnz_quakes/.translations/fr.json | 3 -- .../geonetnz_quakes/.translations/it.json | 3 -- .../geonetnz_quakes/.translations/ko.json | 3 -- .../geonetnz_quakes/.translations/lb.json | 3 -- .../geonetnz_quakes/.translations/nl.json | 3 -- .../geonetnz_quakes/.translations/no.json | 3 -- .../geonetnz_quakes/.translations/pl.json | 3 -- .../geonetnz_quakes/.translations/pt-BR.json | 3 -- .../geonetnz_quakes/.translations/ru.json | 3 -- .../geonetnz_quakes/.translations/sl.json | 3 -- .../geonetnz_quakes/.translations/sv.json | 3 -- .../.translations/zh-Hant.json | 3 -- .../components/harmony/.translations/ca.json | 1 - .../components/harmony/.translations/de.json | 1 - .../components/harmony/.translations/en.json | 1 - .../components/harmony/.translations/es.json | 1 - .../components/harmony/.translations/fr.json | 1 - .../components/harmony/.translations/it.json | 1 - .../components/harmony/.translations/ko.json | 1 - .../components/harmony/.translations/lb.json | 1 - .../components/harmony/.translations/no.json | 1 - .../components/harmony/.translations/pl.json | 1 - .../components/harmony/.translations/ru.json | 1 - .../harmony/.translations/zh-Hant.json | 1 - .../components/heos/.translations/pl.json | 2 +- .../homekit_controller/.translations/hu.json | 2 +- .../huawei_lte/.translations/lb.json | 2 +- .../components/hue/.translations/ca.json | 17 +++++++++ .../components/hue/.translations/da.json | 17 +++++++++ .../components/hue/.translations/de.json | 17 +++++++++ .../components/hue/.translations/es.json | 17 +++++++++ .../components/hue/.translations/ko.json | 17 +++++++++ .../components/hue/.translations/lb.json | 17 +++++++++ .../components/hue/.translations/ru.json | 17 +++++++++ .../components/hue/.translations/zh-Hant.json | 17 +++++++++ .../components/ipp/.translations/ca.json | 24 ++++++++++-- .../components/ipp/.translations/en.json | 3 +- .../components/ipp/.translations/ko.json | 32 ++++++++++++++++ .../konnected/.translations/ca.json | 1 + .../konnected/.translations/es.json | 5 ++- .../konnected/.translations/ko.json | 10 ++++- .../konnected/.translations/lb.json | 2 +- .../konnected/.translations/ru.json | 9 ++++- .../components/light/.translations/hu.json | 2 + .../components/light/.translations/lb.json | 2 + .../components/melcloud/.translations/pl.json | 2 +- .../minecraft_server/.translations/ca.json | 3 +- .../minecraft_server/.translations/da.json | 3 +- .../minecraft_server/.translations/de.json | 3 +- .../minecraft_server/.translations/en.json | 3 +- .../minecraft_server/.translations/es.json | 3 +- .../minecraft_server/.translations/fr.json | 3 +- .../minecraft_server/.translations/hu.json | 5 +-- .../minecraft_server/.translations/it.json | 3 +- .../minecraft_server/.translations/ko.json | 3 +- .../minecraft_server/.translations/lb.json | 3 +- .../minecraft_server/.translations/lv.json | 3 +- .../minecraft_server/.translations/nl.json | 3 +- .../minecraft_server/.translations/no.json | 3 +- .../minecraft_server/.translations/pl.json | 3 +- .../minecraft_server/.translations/ru.json | 3 +- .../minecraft_server/.translations/sl.json | 3 +- .../minecraft_server/.translations/sv.json | 3 +- .../minecraft_server/.translations/tr.json | 3 +- .../.translations/zh-Hant.json | 3 +- .../components/mqtt/.translations/hu.json | 14 ++++++- .../components/notion/.translations/bg.json | 1 - .../components/notion/.translations/ca.json | 1 - .../components/notion/.translations/da.json | 1 - .../components/notion/.translations/de.json | 1 - .../components/notion/.translations/en.json | 1 - .../notion/.translations/es-419.json | 1 - .../components/notion/.translations/es.json | 1 - .../components/notion/.translations/fr.json | 1 - .../components/notion/.translations/hr.json | 1 - .../components/notion/.translations/hu.json | 1 - .../components/notion/.translations/it.json | 1 - .../components/notion/.translations/ko.json | 1 - .../components/notion/.translations/lb.json | 1 - .../components/notion/.translations/nl.json | 1 - .../components/notion/.translations/no.json | 1 - .../components/notion/.translations/pl.json | 1 - .../notion/.translations/pt-BR.json | 1 - .../components/notion/.translations/ru.json | 1 - .../components/notion/.translations/sl.json | 1 - .../components/notion/.translations/sv.json | 1 - .../notion/.translations/zh-Hans.json | 1 - .../notion/.translations/zh-Hant.json | 1 - .../components/nut/.translations/ca.json | 10 +++-- .../components/nut/.translations/ko.json | 37 ++++++++++++++++++ .../components/nut/.translations/lb.json | 1 + .../components/nut/.translations/pl.json | 37 ++++++++++++++++++ .../components/nut/.translations/ru.json | 37 ++++++++++++++++++ .../opentherm_gw/.translations/bg.json | 4 +- .../opentherm_gw/.translations/ca.json | 4 +- .../opentherm_gw/.translations/da.json | 4 +- .../opentherm_gw/.translations/de.json | 4 +- .../opentherm_gw/.translations/en.json | 4 +- .../opentherm_gw/.translations/es.json | 4 +- .../opentherm_gw/.translations/fr.json | 4 +- .../opentherm_gw/.translations/hu.json | 4 +- .../opentherm_gw/.translations/it.json | 4 +- .../opentherm_gw/.translations/ko.json | 4 +- .../opentherm_gw/.translations/lb.json | 4 +- .../opentherm_gw/.translations/nl.json | 4 +- .../opentherm_gw/.translations/no.json | 4 +- .../opentherm_gw/.translations/pl.json | 4 +- .../opentherm_gw/.translations/ru.json | 4 +- .../opentherm_gw/.translations/sl.json | 4 +- .../opentherm_gw/.translations/sv.json | 4 +- .../opentherm_gw/.translations/zh-Hant.json | 4 +- .../components/plex/.translations/bg.json | 21 ---------- .../components/plex/.translations/ca.json | 21 ---------- .../components/plex/.translations/cs.json | 3 -- .../components/plex/.translations/da.json | 21 ---------- .../components/plex/.translations/de.json | 21 ---------- .../components/plex/.translations/en.json | 21 ---------- .../components/plex/.translations/es-419.json | 21 ---------- .../components/plex/.translations/es.json | 21 ---------- .../components/plex/.translations/fr.json | 21 ---------- .../components/plex/.translations/hu.json | 15 -------- .../components/plex/.translations/it.json | 21 ---------- .../components/plex/.translations/ko.json | 21 ---------- .../components/plex/.translations/lb.json | 21 ---------- .../components/plex/.translations/lv.json | 8 ---- .../components/plex/.translations/nl.json | 21 ---------- .../components/plex/.translations/no.json | 21 ---------- .../components/plex/.translations/pl.json | 21 ---------- .../components/plex/.translations/pt-BR.json | 1 - .../components/plex/.translations/ru.json | 21 ---------- .../components/plex/.translations/sl.json | 21 ---------- .../components/plex/.translations/sv.json | 21 ---------- .../plex/.translations/zh-Hant.json | 21 ---------- .../components/rachio/.translations/pl.json | 2 +- .../components/roku/.translations/ca.json | 3 +- .../components/roku/.translations/de.json | 3 +- .../components/roku/.translations/en.json | 3 +- .../components/roku/.translations/es.json | 3 +- .../components/roku/.translations/fr.json | 3 +- .../components/roku/.translations/it.json | 3 +- .../components/roku/.translations/ko.json | 3 +- .../components/roku/.translations/lb.json | 3 +- .../components/roku/.translations/no.json | 3 +- .../components/roku/.translations/pl.json | 3 +- .../components/roku/.translations/ru.json | 3 +- .../components/roku/.translations/sl.json | 3 +- .../roku/.translations/zh-Hant.json | 3 +- .../samsungtv/.translations/ca.json | 1 - .../samsungtv/.translations/da.json | 1 - .../samsungtv/.translations/de.json | 1 - .../samsungtv/.translations/en.json | 1 - .../samsungtv/.translations/es.json | 1 - .../samsungtv/.translations/fr.json | 1 - .../samsungtv/.translations/hu.json | 1 - .../samsungtv/.translations/it.json | 1 - .../samsungtv/.translations/ko.json | 1 - .../samsungtv/.translations/lb.json | 1 - .../samsungtv/.translations/nl.json | 1 - .../samsungtv/.translations/no.json | 1 - .../samsungtv/.translations/pl.json | 1 - .../samsungtv/.translations/ru.json | 1 - .../samsungtv/.translations/sl.json | 1 - .../samsungtv/.translations/sv.json | 1 - .../samsungtv/.translations/tr.json | 1 - .../samsungtv/.translations/zh-Hant.json | 1 - .../components/sensor/.translations/lb.json | 36 +++++++++--------- .../simplisafe/.translations/bg.json | 1 - .../simplisafe/.translations/ca.json | 1 - .../simplisafe/.translations/cs.json | 1 - .../simplisafe/.translations/da.json | 1 - .../simplisafe/.translations/de.json | 1 - .../simplisafe/.translations/en.json | 1 - .../simplisafe/.translations/es-419.json | 1 - .../simplisafe/.translations/es.json | 1 - .../simplisafe/.translations/fr.json | 1 - .../simplisafe/.translations/it.json | 1 - .../simplisafe/.translations/ko.json | 1 - .../simplisafe/.translations/lb.json | 1 - .../simplisafe/.translations/nl.json | 1 - .../simplisafe/.translations/no.json | 1 - .../simplisafe/.translations/pl.json | 1 - .../simplisafe/.translations/pt-BR.json | 1 - .../simplisafe/.translations/pt.json | 1 - .../simplisafe/.translations/ro.json | 1 - .../simplisafe/.translations/ru.json | 1 - .../simplisafe/.translations/sl.json | 1 - .../simplisafe/.translations/sv.json | 1 - .../simplisafe/.translations/uk.json | 1 - .../simplisafe/.translations/zh-Hans.json | 1 - .../simplisafe/.translations/zh-Hant.json | 1 - .../components/switch/.translations/bg.json | 4 +- .../components/switch/.translations/ca.json | 4 +- .../components/switch/.translations/da.json | 4 +- .../components/switch/.translations/de.json | 4 +- .../components/switch/.translations/en.json | 4 +- .../switch/.translations/es-419.json | 4 +- .../components/switch/.translations/es.json | 4 +- .../components/switch/.translations/fr.json | 4 +- .../components/switch/.translations/hu.json | 4 +- .../components/switch/.translations/it.json | 4 +- .../components/switch/.translations/ko.json | 4 +- .../components/switch/.translations/lb.json | 4 +- .../components/switch/.translations/lv.json | 4 -- .../components/switch/.translations/nl.json | 4 +- .../components/switch/.translations/no.json | 4 +- .../components/switch/.translations/pl.json | 4 +- .../components/switch/.translations/ru.json | 4 +- .../components/switch/.translations/sl.json | 4 +- .../components/switch/.translations/sv.json | 4 +- .../switch/.translations/zh-Hant.json | 4 +- .../transmission/.translations/bg.json | 10 +---- .../transmission/.translations/ca.json | 10 +---- .../transmission/.translations/da.json | 10 +---- .../transmission/.translations/de.json | 10 +---- .../transmission/.translations/en.json | 10 +---- .../transmission/.translations/es.json | 10 +---- .../transmission/.translations/fr.json | 10 +---- .../transmission/.translations/hu.json | 9 ----- .../transmission/.translations/it.json | 10 +---- .../transmission/.translations/ko.json | 10 +---- .../transmission/.translations/lb.json | 10 +---- .../transmission/.translations/nl.json | 10 +---- .../transmission/.translations/no.json | 10 +---- .../transmission/.translations/pl.json | 10 +---- .../transmission/.translations/pt-BR.json | 10 +---- .../transmission/.translations/ru.json | 10 +---- .../transmission/.translations/sl.json | 10 +---- .../transmission/.translations/sv.json | 10 +---- .../transmission/.translations/zh-Hant.json | 10 +---- .../components/unifi/.translations/ca.json | 3 +- .../components/unifi/.translations/de.json | 3 +- .../components/unifi/.translations/en.json | 3 +- .../components/unifi/.translations/es.json | 3 +- .../components/unifi/.translations/ko.json | 3 +- .../components/unifi/.translations/lb.json | 18 +++++++-- .../components/unifi/.translations/ru.json | 3 +- .../unifi/.translations/zh-Hant.json | 3 +- .../components/vera/.translations/ca.json | 32 ++++++++++++++++ .../components/vera/.translations/de.json | 31 +++++++++++++++ .../components/vera/.translations/en.json | 32 ++++++++++++++++ .../components/vera/.translations/es.json | 32 ++++++++++++++++ .../components/vera/.translations/ko.json | 32 ++++++++++++++++ .../components/vera/.translations/lb.json | 32 ++++++++++++++++ .../components/vera/.translations/ru.json | 32 ++++++++++++++++ .../vera/.translations/zh-Hant.json | 32 ++++++++++++++++ .../components/vilfo/.translations/pl.json | 2 +- .../components/vizio/.translations/ca.json | 28 +------------- .../components/vizio/.translations/da.json | 12 +----- .../components/vizio/.translations/de.json | 28 +------------- .../components/vizio/.translations/en.json | 28 +------------- .../components/vizio/.translations/es.json | 28 +------------- .../components/vizio/.translations/fr.json | 26 +------------ .../components/vizio/.translations/hu.json | 12 +----- .../components/vizio/.translations/it.json | 28 +------------- .../components/vizio/.translations/ko.json | 28 +------------- .../components/vizio/.translations/lb.json | 31 +++------------ .../components/vizio/.translations/nl.json | 12 +----- .../components/vizio/.translations/no.json | 28 +------------- .../components/vizio/.translations/pl.json | 28 +------------- .../components/vizio/.translations/ru.json | 28 +------------- .../components/vizio/.translations/sl.json | 28 +------------- .../components/vizio/.translations/sv.json | 12 +----- .../vizio/.translations/zh-Hant.json | 28 +------------- .../components/withings/.translations/bg.json | 10 ----- .../components/withings/.translations/ca.json | 10 +---- .../components/withings/.translations/da.json | 10 +---- .../components/withings/.translations/de.json | 10 +---- .../components/withings/.translations/en.json | 10 +---- .../withings/.translations/es-419.json | 11 ------ .../components/withings/.translations/es.json | 10 +---- .../components/withings/.translations/fr.json | 10 +---- .../components/withings/.translations/hu.json | 10 +---- .../components/withings/.translations/it.json | 10 +---- .../components/withings/.translations/ko.json | 10 +---- .../components/withings/.translations/lb.json | 10 +---- .../components/withings/.translations/lv.json | 8 ---- .../components/withings/.translations/nl.json | 10 +---- .../components/withings/.translations/no.json | 10 +---- .../components/withings/.translations/pl.json | 10 +---- .../components/withings/.translations/ru.json | 10 +---- .../components/withings/.translations/sl.json | 10 +---- .../components/withings/.translations/sv.json | 10 +---- .../withings/.translations/zh-Hant.json | 10 +---- .../wled/.translations/zh-Hant.json | 2 +- .../components/wwlln/.translations/bg.json | 3 -- .../components/wwlln/.translations/ca.json | 6 +-- .../components/wwlln/.translations/cy.json | 3 -- .../components/wwlln/.translations/da.json | 3 -- .../components/wwlln/.translations/de.json | 6 +-- .../components/wwlln/.translations/en.json | 6 +-- .../wwlln/.translations/es-419.json | 3 -- .../components/wwlln/.translations/es.json | 6 +-- .../components/wwlln/.translations/fr.json | 6 +-- .../components/wwlln/.translations/hr.json | 3 -- .../components/wwlln/.translations/it.json | 6 +-- .../components/wwlln/.translations/ko.json | 6 +-- .../components/wwlln/.translations/lb.json | 3 -- .../components/wwlln/.translations/nl.json | 3 -- .../components/wwlln/.translations/no.json | 6 +-- .../components/wwlln/.translations/pl.json | 6 +-- .../components/wwlln/.translations/pt-BR.json | 3 -- .../components/wwlln/.translations/ru.json | 3 -- .../components/wwlln/.translations/sl.json | 6 +-- .../components/wwlln/.translations/sv.json | 3 -- .../wwlln/.translations/zh-Hans.json | 3 -- .../wwlln/.translations/zh-Hant.json | 6 +-- 497 files changed, 1344 insertions(+), 2310 deletions(-) create mode 100644 homeassistant/components/.translations/synology_dsm.ca.json create mode 100644 homeassistant/components/.translations/synology_dsm.da.json create mode 100644 homeassistant/components/.translations/synology_dsm.en.json create mode 100644 homeassistant/components/.translations/synology_dsm.es.json create mode 100644 homeassistant/components/.translations/synology_dsm.ko.json create mode 100644 homeassistant/components/.translations/synology_dsm.lb.json create mode 100644 homeassistant/components/.translations/synology_dsm.nl.json create mode 100644 homeassistant/components/.translations/synology_dsm.ru.json create mode 100644 homeassistant/components/coronavirus/.translations/hu.json create mode 100644 homeassistant/components/flunearyou/.translations/ca.json create mode 100644 homeassistant/components/flunearyou/.translations/de.json create mode 100644 homeassistant/components/flunearyou/.translations/en.json create mode 100644 homeassistant/components/flunearyou/.translations/es.json create mode 100644 homeassistant/components/flunearyou/.translations/ko.json create mode 100644 homeassistant/components/flunearyou/.translations/lb.json create mode 100644 homeassistant/components/flunearyou/.translations/ru.json create mode 100644 homeassistant/components/flunearyou/.translations/zh-Hant.json create mode 100644 homeassistant/components/ipp/.translations/ko.json create mode 100644 homeassistant/components/nut/.translations/ko.json create mode 100644 homeassistant/components/nut/.translations/pl.json create mode 100644 homeassistant/components/nut/.translations/ru.json create mode 100644 homeassistant/components/vera/.translations/ca.json create mode 100644 homeassistant/components/vera/.translations/de.json create mode 100644 homeassistant/components/vera/.translations/en.json create mode 100644 homeassistant/components/vera/.translations/es.json create mode 100644 homeassistant/components/vera/.translations/ko.json create mode 100644 homeassistant/components/vera/.translations/lb.json create mode 100644 homeassistant/components/vera/.translations/ru.json create mode 100644 homeassistant/components/vera/.translations/zh-Hant.json diff --git a/homeassistant/components/.translations/synology_dsm.ca.json b/homeassistant/components/.translations/synology_dsm.ca.json new file mode 100644 index 00000000000..39b99ac9306 --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.ca.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat" + }, + "error": { + "login": "Error d\u2019inici de sessi\u00f3: comprova el nom d'usuari i la contrasenya", + "unknown": "Error desconegut: torna-ho a provar m\u00e9s tard o revisa la configuraci\u00f3" + }, + "step": { + "user": { + "data": { + "api_version": "Versi\u00f3 DSM", + "host": "Amfitri\u00f3", + "name": "Nom", + "password": "Contrasenya", + "port": "Port", + "ssl": "Utilitza SSL/TLS per connectar-te al servidor NAS", + "username": "Nom d'usuari" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.da.json b/homeassistant/components/.translations/synology_dsm.da.json new file mode 100644 index 00000000000..f95e08df3c1 --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.da.json @@ -0,0 +1,18 @@ +{ + "config": { + "step": { + "link": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + }, + "user": { + "data": { + "password": "Adgangskode", + "username": "Brugernavn" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.en.json b/homeassistant/components/.translations/synology_dsm.en.json new file mode 100644 index 00000000000..3bac6d16288 --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.en.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "Host already configured" + }, + "error": { + "login": "Login error: please check your username & password", + "unknown": "Unknown error: please retry later or an other configuration" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "link": { + "data": { + "api_version": "DSM version", + "password": "Password", + "port": "Port (Optional)", + "ssl": "Use SSL/TLS to connect to your NAS", + "username": "Username" + }, + "description": "Do you want to setup {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "api_version": "DSM version", + "host": "Host", + "name": "Name", + "password": "Password", + "port": "Port (Optional)", + "ssl": "Use SSL/TLS to connect to your NAS", + "username": "Username" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.es.json b/homeassistant/components/.translations/synology_dsm.es.json new file mode 100644 index 00000000000..fafedb50a0e --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.es.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: comprueba tu direcci\u00f3n de correo electr\u00f3nico y contrase\u00f1a", + "unknown": "Error desconocido: por favor vuelve a intentarlo m\u00e1s tarde o usa otra configuraci\u00f3n" + }, + "step": { + "user": { + "data": { + "api_version": "Versi\u00f3n del DSM", + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "ssl": "Usar SSL/TLS para conectar con tu NAS", + "username": "Usuario" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.ko.json b/homeassistant/components/.translations/synology_dsm.ko.json new file mode 100644 index 00000000000..60fcd9866c1 --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.ko.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + }, + "error": { + "login": "\ub85c\uadf8\uc778 \uc624\ub958: \uc0ac\uc6a9\uc790 \uc774\ub984 \ubc0f \ube44\ubc00\ubc88\ud638\ub97c \ud655\uc778\ud574\uc8fc\uc138\uc694", + "unknown": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uc785\ub2c8\ub2e4. \ub098\uc911\uc5d0 \ub2e4\uc2dc \uc2dc\ub3c4\ud558\uac70\ub098 \ub2e4\ub978 \uad6c\uc131\uc744 \uc2dc\ub3c4\ud574\ubcf4\uc138\uc694" + }, + "step": { + "user": { + "data": { + "api_version": "DSM \ubc84\uc804", + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "ssl": "SSL/TLS \ub97c \uc0ac\uc6a9\ud558\uc5ec NAS \uc5d0 \uc5f0\uacb0", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.lb.json b/homeassistant/components/.translations/synology_dsm.lb.json new file mode 100644 index 00000000000..92026cbe2d8 --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.lb.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Apparat ass scho konfigur\u00e9iert" + }, + "error": { + "login": "Feeler beim Login: iwwerpr\u00e9if de Benotzernumm & Passwuert", + "unknown": "Onbekannte Feeler: prob\u00e9ier sp\u00e9ider nach emol oder mat enger aner Konfiguratioun" + }, + "step": { + "user": { + "data": { + "api_version": "DSM Versioun", + "host": "Apparat", + "name": "Numm", + "password": "Passwuert", + "port": "Port", + "ssl": "Benotzt SSL/TLS fir sech mam NAS ze verbannen", + "username": "Benotzernumm" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.nl.json b/homeassistant/components/.translations/synology_dsm.nl.json new file mode 100644 index 00000000000..1927227b65f --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.nl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Host is al geconfigureerd." + }, + "error": { + "unknown": "Onbekende fout: probeer het later opnieuw of een andere configuratie" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "link": { + "data": { + "api_version": "DSM-versie", + "password": "Wachtwoord", + "port": "Poort (optioneel)", + "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS", + "username": "Gebruikersnaam" + }, + "description": "Wil je {name} ({host}) instellen?", + "title": "Synology DSM" + }, + "user": { + "data": { + "api_version": "DSM-versie", + "host": "Host", + "name": "Naam", + "password": "Wachtwoord", + "port": "Poort (optioneel)", + "ssl": "Gebruik SSL/TLS om verbinding te maken met uw NAS", + "username": "Gebruikersnaam" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/.translations/synology_dsm.ru.json b/homeassistant/components/.translations/synology_dsm.ru.json new file mode 100644 index 00000000000..c76fa9ee972 --- /dev/null +++ b/homeassistant/components/.translations/synology_dsm.ru.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + }, + "error": { + "login": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0432\u0445\u043e\u0434\u0430: \u043f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043b\u043e\u0433\u0438\u043d \u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", + "unknown": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430: \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u0441 \u0434\u0440\u0443\u0433\u043e\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0435\u0439 \u0438\u043b\u0438 \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443 \u043f\u043e\u0437\u0436\u0435." + }, + "step": { + "user": { + "data": { + "api_version": "\u0412\u0435\u0440\u0441\u0438\u044f DSM", + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL / TLS \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "title": "Synology DSM" + } + }, + "title": "Synology DSM" + } +} \ No newline at end of file diff --git a/homeassistant/components/airly/.translations/bg.json b/homeassistant/components/airly/.translations/bg.json index c91190d9852..e09d9c0d62f 100644 --- a/homeassistant/components/airly/.translations/bg.json +++ b/homeassistant/components/airly/.translations/bg.json @@ -2,7 +2,6 @@ "config": { "error": { "auth": "API \u043a\u043b\u044e\u0447\u044a\u0442 \u043d\u0435 \u0435 \u043f\u0440\u0430\u0432\u0438\u043b\u0435\u043d.", - "name_exists": "\u0418\u043c\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0441\u044a\u0449\u0435\u0441\u0442\u0432\u0443\u0432\u0430.", "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/ca.json b/homeassistant/components/airly/.translations/ca.json index 4c5a7a6bd59..00ef4c7180e 100644 --- a/homeassistant/components/airly/.translations/ca.json +++ b/homeassistant/components/airly/.translations/ca.json @@ -5,7 +5,6 @@ }, "error": { "auth": "La clau API no \u00e9s correcta.", - "name_exists": "El nom ja existeix.", "wrong_location": "No hi ha estacions de mesura Airly en aquesta zona." }, "step": { diff --git a/homeassistant/components/airly/.translations/da.json b/homeassistant/components/airly/.translations/da.json index 52bf903d5a8..b33e9b18da8 100644 --- a/homeassistant/components/airly/.translations/da.json +++ b/homeassistant/components/airly/.translations/da.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API-n\u00f8glen er ikke korrekt.", - "name_exists": "Navnet findes allerede.", "wrong_location": "Ingen Airly-m\u00e5lestationer i dette omr\u00e5de." }, "step": { diff --git a/homeassistant/components/airly/.translations/de.json b/homeassistant/components/airly/.translations/de.json index ef2b2d64a4e..727b67e3245 100644 --- a/homeassistant/components/airly/.translations/de.json +++ b/homeassistant/components/airly/.translations/de.json @@ -5,7 +5,6 @@ }, "error": { "auth": "Der API-Schl\u00fcssel ist nicht korrekt.", - "name_exists": "Name existiert bereits", "wrong_location": "Keine Airly Luftmessstation an diesem Ort" }, "step": { diff --git a/homeassistant/components/airly/.translations/en.json b/homeassistant/components/airly/.translations/en.json index cae6854d231..ef485ec610f 100644 --- a/homeassistant/components/airly/.translations/en.json +++ b/homeassistant/components/airly/.translations/en.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API key is not correct.", - "name_exists": "Name already exists.", "wrong_location": "No Airly measuring stations in this area." }, "step": { diff --git a/homeassistant/components/airly/.translations/es-419.json b/homeassistant/components/airly/.translations/es-419.json index 74924493863..41f7e29b408 100644 --- a/homeassistant/components/airly/.translations/es-419.json +++ b/homeassistant/components/airly/.translations/es-419.json @@ -2,7 +2,6 @@ "config": { "error": { "auth": "La clave API no es correcta.", - "name_exists": "El nombre ya existe.", "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta \u00e1rea." }, "step": { diff --git a/homeassistant/components/airly/.translations/es.json b/homeassistant/components/airly/.translations/es.json index 6fd18eb747c..b364a45c344 100644 --- a/homeassistant/components/airly/.translations/es.json +++ b/homeassistant/components/airly/.translations/es.json @@ -5,7 +5,6 @@ }, "error": { "auth": "La clave de la API no es correcta.", - "name_exists": "El nombre ya existe.", "wrong_location": "No hay estaciones de medici\u00f3n Airly en esta zona." }, "step": { diff --git a/homeassistant/components/airly/.translations/fr.json b/homeassistant/components/airly/.translations/fr.json index f2fdbbd9754..b11493e337f 100644 --- a/homeassistant/components/airly/.translations/fr.json +++ b/homeassistant/components/airly/.translations/fr.json @@ -5,7 +5,6 @@ }, "error": { "auth": "La cl\u00e9 API n'est pas correcte.", - "name_exists": "Le nom existe d\u00e9j\u00e0.", "wrong_location": "Aucune station de mesure Airly dans cette zone." }, "step": { diff --git a/homeassistant/components/airly/.translations/hu.json b/homeassistant/components/airly/.translations/hu.json index 30898c61abb..ae3990c31ce 100644 --- a/homeassistant/components/airly/.translations/hu.json +++ b/homeassistant/components/airly/.translations/hu.json @@ -5,7 +5,6 @@ }, "error": { "auth": "Az API kulcs nem megfelel\u0151.", - "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", "wrong_location": "Ezen a ter\u00fcleten nincs Airly m\u00e9r\u0151\u00e1llom\u00e1s." }, "step": { diff --git a/homeassistant/components/airly/.translations/it.json b/homeassistant/components/airly/.translations/it.json index c52e77881c0..0453d397bc4 100644 --- a/homeassistant/components/airly/.translations/it.json +++ b/homeassistant/components/airly/.translations/it.json @@ -5,7 +5,6 @@ }, "error": { "auth": "La chiave API non \u00e8 corretta.", - "name_exists": "Il nome \u00e8 gi\u00e0 esistente", "wrong_location": "Nessuna stazione di misurazione Airly in quest'area." }, "step": { diff --git a/homeassistant/components/airly/.translations/ko.json b/homeassistant/components/airly/.translations/ko.json index b64a16635a6..75b9bcfc1c4 100644 --- a/homeassistant/components/airly/.translations/ko.json +++ b/homeassistant/components/airly/.translations/ko.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API \ud0a4\uac00 \uc62c\ubc14\ub974\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "name_exists": "\uc774\ub984\uc774 \uc774\ubbf8 \uc874\uc7ac\ud569\ub2c8\ub2e4.", "wrong_location": "\uc774 \uc9c0\uc5ed\uc5d0\ub294 Airly \uce21\uc815 \uc2a4\ud14c\uc774\uc158\uc774 \uc5c6\uc2b5\ub2c8\ub2e4." }, "step": { diff --git a/homeassistant/components/airly/.translations/lb.json b/homeassistant/components/airly/.translations/lb.json index 8c2f5c615f3..75c77d9481e 100644 --- a/homeassistant/components/airly/.translations/lb.json +++ b/homeassistant/components/airly/.translations/lb.json @@ -5,7 +5,6 @@ }, "error": { "auth": "Api Schl\u00ebssel ass net korrekt.", - "name_exists": "Numm g\u00ebtt et schonn", "wrong_location": "Keng Airly Moos Statioun an d\u00ebsem Ber\u00e4ich" }, "step": { diff --git a/homeassistant/components/airly/.translations/nl.json b/homeassistant/components/airly/.translations/nl.json index a9c6865ad91..2e9c97c8232 100644 --- a/homeassistant/components/airly/.translations/nl.json +++ b/homeassistant/components/airly/.translations/nl.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API-sleutel is niet correct.", - "name_exists": "Naam bestaat al.", "wrong_location": "Geen Airly meetstations in dit gebied." }, "step": { diff --git a/homeassistant/components/airly/.translations/no.json b/homeassistant/components/airly/.translations/no.json index 79dfcd7307e..492e1471351 100644 --- a/homeassistant/components/airly/.translations/no.json +++ b/homeassistant/components/airly/.translations/no.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API-n\u00f8kkelen er ikke korrekt.", - "name_exists": "Navnet finnes allerede.", "wrong_location": "Ingen Airly m\u00e5lestasjoner i dette omr\u00e5det." }, "step": { diff --git a/homeassistant/components/airly/.translations/pl.json b/homeassistant/components/airly/.translations/pl.json index 5274a4383b6..85918d7c711 100644 --- a/homeassistant/components/airly/.translations/pl.json +++ b/homeassistant/components/airly/.translations/pl.json @@ -5,7 +5,6 @@ }, "error": { "auth": "Klucz API jest nieprawid\u0142owy.", - "name_exists": "Nazwa ju\u017c istnieje.", "wrong_location": "Brak stacji pomiarowych Airly w tym rejonie." }, "step": { diff --git a/homeassistant/components/airly/.translations/ru.json b/homeassistant/components/airly/.translations/ru.json index 5094d3f4d1e..7846d8173c4 100644 --- a/homeassistant/components/airly/.translations/ru.json +++ b/homeassistant/components/airly/.translations/ru.json @@ -5,7 +5,6 @@ }, "error": { "auth": "\u041d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API.", - "name_exists": "\u042d\u0442\u043e \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435 \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f.", "wrong_location": "\u0412 \u044d\u0442\u043e\u0439 \u043e\u0431\u043b\u0430\u0441\u0442\u0438 \u043d\u0435\u0442 \u0438\u0437\u043c\u0435\u0440\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u0441\u0442\u0430\u043d\u0446\u0438\u0439 Airly." }, "step": { diff --git a/homeassistant/components/airly/.translations/sl.json b/homeassistant/components/airly/.translations/sl.json index f8ca4e5b6d5..d7797997910 100644 --- a/homeassistant/components/airly/.translations/sl.json +++ b/homeassistant/components/airly/.translations/sl.json @@ -5,7 +5,6 @@ }, "error": { "auth": "Klju\u010d API ni pravilen.", - "name_exists": "Ime \u017ee obstaja", "wrong_location": "Na tem obmo\u010dju ni merilnih postaj Airly." }, "step": { diff --git a/homeassistant/components/airly/.translations/sv.json b/homeassistant/components/airly/.translations/sv.json index 5b81b4625a2..7c7d10f47dc 100644 --- a/homeassistant/components/airly/.translations/sv.json +++ b/homeassistant/components/airly/.translations/sv.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API-nyckeln \u00e4r inte korrekt.", - "name_exists": "Namnet finns redan.", "wrong_location": "Inga Airly m\u00e4tstationer i detta omr\u00e5de." }, "step": { diff --git a/homeassistant/components/airly/.translations/zh-Hant.json b/homeassistant/components/airly/.translations/zh-Hant.json index 5bc0a52f394..66934d7a986 100644 --- a/homeassistant/components/airly/.translations/zh-Hant.json +++ b/homeassistant/components/airly/.translations/zh-Hant.json @@ -5,7 +5,6 @@ }, "error": { "auth": "API \u5bc6\u9470\u4e0d\u6b63\u78ba\u3002", - "name_exists": "\u8a72\u540d\u7a31\u5df2\u5b58\u5728", "wrong_location": "\u8a72\u5340\u57df\u6c92\u6709 Arily \u76e3\u6e2c\u7ad9\u3002" }, "step": { diff --git a/homeassistant/components/airvisual/.translations/ca.json b/homeassistant/components/airvisual/.translations/ca.json index 66cd796d752..070eeee8b51 100644 --- a/homeassistant/components/airvisual/.translations/ca.json +++ b/homeassistant/components/airvisual/.translations/ca.json @@ -11,8 +11,7 @@ "data": { "api_key": "Clau API", "latitude": "Latitud", - "longitude": "Longitud", - "show_on_map": "Mostra al mapa l'\u00e0rea geogr\u00e0fica monitoritzada" + "longitude": "Longitud" }, "description": "Monitoritzaci\u00f3 de la qualitat de l'aire per ubicaci\u00f3 geogr\u00e0fica.", "title": "Configura AirVisual" diff --git a/homeassistant/components/airvisual/.translations/de.json b/homeassistant/components/airvisual/.translations/de.json index fc603318ab4..02f25900428 100644 --- a/homeassistant/components/airvisual/.translations/de.json +++ b/homeassistant/components/airvisual/.translations/de.json @@ -11,8 +11,7 @@ "data": { "api_key": "API-Schl\u00fcssel", "latitude": "Breitengrad", - "longitude": "L\u00e4ngengrad", - "show_on_map": "Zeigen Sie die \u00fcberwachte Geografie auf der Karte an" + "longitude": "L\u00e4ngengrad" }, "description": "\u00dcberwachen Sie die Luftqualit\u00e4t an einem geografischen Ort.", "title": "Konfigurieren Sie AirVisual" diff --git a/homeassistant/components/airvisual/.translations/en.json b/homeassistant/components/airvisual/.translations/en.json index 982ed8e13e7..30d501f1af6 100644 --- a/homeassistant/components/airvisual/.translations/en.json +++ b/homeassistant/components/airvisual/.translations/en.json @@ -11,8 +11,7 @@ "data": { "api_key": "API Key", "latitude": "Latitude", - "longitude": "Longitude", - "show_on_map": "Show monitored geography on the map" + "longitude": "Longitude" }, "description": "Monitor air quality in a geographical location.", "title": "Configure AirVisual" diff --git a/homeassistant/components/airvisual/.translations/es.json b/homeassistant/components/airvisual/.translations/es.json index a1054c79098..752593ce29d 100644 --- a/homeassistant/components/airvisual/.translations/es.json +++ b/homeassistant/components/airvisual/.translations/es.json @@ -11,8 +11,7 @@ "data": { "api_key": "Clave API", "latitude": "Latitud", - "longitude": "Longitud", - "show_on_map": "Mostrar geograf\u00eda monitorizada en el mapa" + "longitude": "Longitud" }, "description": "Monitorizar la calidad del aire en una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar AirVisual" diff --git a/homeassistant/components/airvisual/.translations/fr.json b/homeassistant/components/airvisual/.translations/fr.json index 9f32bbf5d94..6ee4377db95 100644 --- a/homeassistant/components/airvisual/.translations/fr.json +++ b/homeassistant/components/airvisual/.translations/fr.json @@ -11,8 +11,7 @@ "data": { "api_key": "Cl\u00e9 API", "latitude": "Latitude", - "longitude": "Longitude", - "show_on_map": "Afficher la g\u00e9ographie surveill\u00e9e sur la carte" + "longitude": "Longitude" }, "description": "Surveiller la qualit\u00e9 de l\u2019air dans un emplacement g\u00e9ographique.", "title": "Configurer AirVisual" diff --git a/homeassistant/components/airvisual/.translations/it.json b/homeassistant/components/airvisual/.translations/it.json index 7d309fdb22a..762c99ec4d7 100644 --- a/homeassistant/components/airvisual/.translations/it.json +++ b/homeassistant/components/airvisual/.translations/it.json @@ -11,8 +11,7 @@ "data": { "api_key": "Chiave API", "latitude": "Latitudine", - "longitude": "Logitudine", - "show_on_map": "Mostra l'area geografica monitorata sulla mappa" + "longitude": "Logitudine" }, "description": "Monitorare la qualit\u00e0 dell'aria in una posizione geografica.", "title": "Configura AirVisual" diff --git a/homeassistant/components/airvisual/.translations/ko.json b/homeassistant/components/airvisual/.translations/ko.json index 8f1155aa5f9..4e1511b2d2d 100644 --- a/homeassistant/components/airvisual/.translations/ko.json +++ b/homeassistant/components/airvisual/.translations/ko.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 API \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." + "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "error": { "invalid_api_key": "\uc798\ubabb\ub41c API \ud0a4" @@ -11,8 +11,7 @@ "data": { "api_key": "API \ud0a4", "latitude": "\uc704\ub3c4", - "longitude": "\uacbd\ub3c4", - "show_on_map": "\uc9c0\ub3c4\uc5d0 \ubaa8\ub2c8\ud130\ub9c1\ub41c \uc9c0\ub9ac \uc815\ubcf4 \ud45c\uc2dc" + "longitude": "\uacbd\ub3c4" }, "description": "\uc9c0\ub9ac\uc801 \uc704\uce58\uc5d0\uc11c \ub300\uae30\uc9c8\uc744 \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", "title": "AirVisual \uad6c\uc131" diff --git a/homeassistant/components/airvisual/.translations/lb.json b/homeassistant/components/airvisual/.translations/lb.json index eb267e793bb..a7f20253ef1 100644 --- a/homeassistant/components/airvisual/.translations/lb.json +++ b/homeassistant/components/airvisual/.translations/lb.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "D\u00ebsen App Schl\u00ebssel g\u00ebtt scho benotzt" + "already_configured": "D\u00ebs Koordinate si schon registr\u00e9iert." }, "error": { "invalid_api_key": "Ong\u00ebltegen API Schl\u00ebssel" @@ -13,6 +13,7 @@ "latitude": "Breedegrad", "longitude": "L\u00e4ngegrad" }, + "description": "Loft Qualit\u00e9it an enger geografescher Lag iwwerwaachen.", "title": "AirVisual konfigur\u00e9ieren" } }, @@ -21,6 +22,9 @@ "options": { "step": { "init": { + "data": { + "show_on_map": "Iwwerwaachte Geografie op der Kaart uweisen" + }, "description": "Verschidden Optioune fir d'AirVisual Integratioun d\u00e9fin\u00e9ieren.", "title": "Airvisual ariichten" } diff --git a/homeassistant/components/airvisual/.translations/no.json b/homeassistant/components/airvisual/.translations/no.json index 82533db387f..2a2a1fcd07c 100644 --- a/homeassistant/components/airvisual/.translations/no.json +++ b/homeassistant/components/airvisual/.translations/no.json @@ -11,8 +11,7 @@ "data": { "api_key": "API-n\u00f8kkel", "latitude": "Breddegrad", - "longitude": "Lengdegrad", - "show_on_map": "Vis overv\u00e5ket geografi p\u00e5 kartet" + "longitude": "Lengdegrad" }, "description": "Overv\u00e5k luftkvaliteten p\u00e5 et geografisk sted.", "title": "Konfigurer AirVisual" diff --git a/homeassistant/components/airvisual/.translations/pl.json b/homeassistant/components/airvisual/.translations/pl.json index ebcbc12e405..99c74c3e5cd 100644 --- a/homeassistant/components/airvisual/.translations/pl.json +++ b/homeassistant/components/airvisual/.translations/pl.json @@ -11,8 +11,7 @@ "data": { "api_key": "Klucz API", "latitude": "Szeroko\u015b\u0107 geograficzna", - "longitude": "D\u0142ugo\u015b\u0107 geograficzna", - "show_on_map": "Wy\u015bwietlaj encje na mapie" + "longitude": "D\u0142ugo\u015b\u0107 geograficzna" }, "description": "Monitoruj jako\u015b\u0107 powietrza w okre\u015blonej lokalizacji geograficznej.", "title": "Konfiguracja AirVisual" diff --git a/homeassistant/components/airvisual/.translations/ru.json b/homeassistant/components/airvisual/.translations/ru.json index 5c9634390c6..e8682a0188a 100644 --- a/homeassistant/components/airvisual/.translations/ru.json +++ b/homeassistant/components/airvisual/.translations/ru.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "already_configured": "\u042d\u0442\u043e\u0442 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." + "already_configured": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b." }, "error": { "invalid_api_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API." @@ -11,8 +11,7 @@ "data": { "api_key": "\u041a\u043b\u044e\u0447 API", "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", - "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430", - "show_on_map": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u043e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u0443\u044e \u043e\u0431\u043b\u0430\u0441\u0442\u044c \u043d\u0430 \u043a\u0430\u0440\u0442\u0435" + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" }, "description": "\u041a\u043e\u043d\u0442\u0440\u043e\u043b\u0438\u0440\u0443\u0439\u0442\u0435 \u043a\u0430\u0447\u0435\u0441\u0442\u0432\u043e \u0432\u043e\u0437\u0434\u0443\u0445\u0430 \u0432 \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u043c \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0438.", "title": "AirVisual" diff --git a/homeassistant/components/airvisual/.translations/sl.json b/homeassistant/components/airvisual/.translations/sl.json index 97ed91592d5..6511c7b6da8 100644 --- a/homeassistant/components/airvisual/.translations/sl.json +++ b/homeassistant/components/airvisual/.translations/sl.json @@ -11,8 +11,7 @@ "data": { "api_key": "API Klju\u010d", "latitude": "Zemljepisna \u0161irina", - "longitude": "Zemljepisna dol\u017eina", - "show_on_map": "Prika\u017ei nadzorovano obmo\u010dje na zemljevidu" + "longitude": "Zemljepisna dol\u017eina" }, "description": "Spremljajte kakovost zraka na zemljepisni lokaciji.", "title": "Nastavite AirVisual" diff --git a/homeassistant/components/airvisual/.translations/zh-Hant.json b/homeassistant/components/airvisual/.translations/zh-Hant.json index 5c347e3b251..e40926d4a08 100644 --- a/homeassistant/components/airvisual/.translations/zh-Hant.json +++ b/homeassistant/components/airvisual/.translations/zh-Hant.json @@ -11,8 +11,7 @@ "data": { "api_key": "API \u5bc6\u9470", "latitude": "\u7def\u5ea6", - "longitude": "\u7d93\u5ea6", - "show_on_map": "\u65bc\u5730\u5716\u4e0a\u986f\u793a\u76e3\u63a7\u4f4d\u7f6e\u3002" + "longitude": "\u7d93\u5ea6" }, "description": "\u4f9d\u5730\u7406\u4f4d\u7f6e\u76e3\u63a7\u7a7a\u6c23\u54c1\u8cea\u3002", "title": "\u8a2d\u5b9a AirVisual" diff --git a/homeassistant/components/ambient_station/.translations/bg.json b/homeassistant/components/ambient_station/.translations/bg.json index 2099038f004..df9fe8866ac 100644 --- a/homeassistant/components/ambient_station/.translations/bg.json +++ b/homeassistant/components/ambient_station/.translations/bg.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Application \u0438/\u0438\u043b\u0438 API \u043a\u043b\u044e\u0447\u044a\u0442 \u0432\u0435\u0447\u0435 \u0441\u0430 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u0438", "invalid_key": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u0435\u043d API \u043a\u043b\u044e\u0447 \u0438/\u0438\u043b\u0438 Application \u043a\u043b\u044e\u0447", "no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430" }, diff --git a/homeassistant/components/ambient_station/.translations/ca.json b/homeassistant/components/ambient_station/.translations/ca.json index 280a90354b0..0991c74b0a5 100644 --- a/homeassistant/components/ambient_station/.translations/ca.json +++ b/homeassistant/components/ambient_station/.translations/ca.json @@ -4,7 +4,6 @@ "already_configured": "Aquesta clau d'aplicaci\u00f3 ja est\u00e0 en \u00fas." }, "error": { - "identifier_exists": "Clau d'aplicaci\u00f3 i/o clau API ja registrada", "invalid_key": "Clau API i/o clau d'aplicaci\u00f3 inv\u00e0lida/es", "no_devices": "No s'ha trobat cap dispositiu al compte" }, diff --git a/homeassistant/components/ambient_station/.translations/da.json b/homeassistant/components/ambient_station/.translations/da.json index 6428508687d..5028a84eb31 100644 --- a/homeassistant/components/ambient_station/.translations/da.json +++ b/homeassistant/components/ambient_station/.translations/da.json @@ -4,7 +4,6 @@ "already_configured": "Denne appn\u00f8gle er allerede i brug." }, "error": { - "identifier_exists": "Applikationsn\u00f8gle og/eller API n\u00f8gle er allerede registreret", "invalid_key": "Ugyldig API n\u00f8gle og/eller applikationsn\u00f8gle", "no_devices": "Ingen enheder fundet i konto" }, diff --git a/homeassistant/components/ambient_station/.translations/de.json b/homeassistant/components/ambient_station/.translations/de.json index 451a2e70e68..9213007e935 100644 --- a/homeassistant/components/ambient_station/.translations/de.json +++ b/homeassistant/components/ambient_station/.translations/de.json @@ -4,7 +4,6 @@ "already_configured": "Dieser App-Schl\u00fcssel wird bereits verwendet." }, "error": { - "identifier_exists": "Anwendungsschl\u00fcssel und / oder API-Schl\u00fcssel bereits registriert", "invalid_key": "Ung\u00fcltiger API Key und / oder Anwendungsschl\u00fcssel", "no_devices": "Keine Ger\u00e4te im Konto gefunden" }, diff --git a/homeassistant/components/ambient_station/.translations/en.json b/homeassistant/components/ambient_station/.translations/en.json index 8b8e71d5316..c3e2a40ab13 100644 --- a/homeassistant/components/ambient_station/.translations/en.json +++ b/homeassistant/components/ambient_station/.translations/en.json @@ -4,7 +4,6 @@ "already_configured": "This app key is already in use." }, "error": { - "identifier_exists": "Application Key and/or API Key already registered", "invalid_key": "Invalid API Key and/or Application Key", "no_devices": "No devices found in account" }, diff --git a/homeassistant/components/ambient_station/.translations/es-419.json b/homeassistant/components/ambient_station/.translations/es-419.json index 268a6ba001e..4cca42afbf4 100644 --- a/homeassistant/components/ambient_station/.translations/es-419.json +++ b/homeassistant/components/ambient_station/.translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Clave de aplicaci\u00f3n y/o clave de API ya registrada", "invalid_key": "Clave de API y/o clave de aplicaci\u00f3n no v\u00e1lida", "no_devices": "No se han encontrado dispositivos en la cuenta." }, diff --git a/homeassistant/components/ambient_station/.translations/es.json b/homeassistant/components/ambient_station/.translations/es.json index d575db2ba71..ae8b829d56e 100644 --- a/homeassistant/components/ambient_station/.translations/es.json +++ b/homeassistant/components/ambient_station/.translations/es.json @@ -4,7 +4,6 @@ "already_configured": "Esta clave API ya est\u00e1 en uso." }, "error": { - "identifier_exists": "La clave API y/o la clave de aplicaci\u00f3n ya est\u00e1 registrada", "invalid_key": "Clave API y/o clave de aplicaci\u00f3n no v\u00e1lida", "no_devices": "No se han encontrado dispositivos en la cuenta" }, diff --git a/homeassistant/components/ambient_station/.translations/fr.json b/homeassistant/components/ambient_station/.translations/fr.json index 00f4e3d02fc..34490332c12 100644 --- a/homeassistant/components/ambient_station/.translations/fr.json +++ b/homeassistant/components/ambient_station/.translations/fr.json @@ -4,7 +4,6 @@ "already_configured": "Cette cl\u00e9 d'application est d\u00e9j\u00e0 utilis\u00e9e." }, "error": { - "identifier_exists": "Cl\u00e9 d'application et / ou cl\u00e9 API d\u00e9j\u00e0 enregistr\u00e9e", "invalid_key": "Cl\u00e9 d'API et / ou cl\u00e9 d'application non valide", "no_devices": "Aucun appareil trouv\u00e9 dans le compte" }, diff --git a/homeassistant/components/ambient_station/.translations/hu.json b/homeassistant/components/ambient_station/.translations/hu.json index 222b512c39f..6febc6ec20d 100644 --- a/homeassistant/components/ambient_station/.translations/hu.json +++ b/homeassistant/components/ambient_station/.translations/hu.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Alkalmaz\u00e1s kulcsot \u00e9s/vagy az API kulcsot m\u00e1r regisztr\u00e1lt\u00e1k", "invalid_key": "\u00c9rv\u00e9nytelen API kulcs \u00e9s / vagy alkalmaz\u00e1skulcs", "no_devices": "Nincs a fi\u00f3kodban tal\u00e1lhat\u00f3 eszk\u00f6z" }, diff --git a/homeassistant/components/ambient_station/.translations/it.json b/homeassistant/components/ambient_station/.translations/it.json index 6bfaaac8f01..e5c27bd3939 100644 --- a/homeassistant/components/ambient_station/.translations/it.json +++ b/homeassistant/components/ambient_station/.translations/it.json @@ -4,7 +4,6 @@ "already_configured": "Questa chiave dell'app \u00e8 gi\u00e0 in uso." }, "error": { - "identifier_exists": "API Key e/o Application Key gi\u00e0 registrata", "invalid_key": "API Key e/o Application Key non valida", "no_devices": "Nessun dispositivo trovato nell'account" }, diff --git a/homeassistant/components/ambient_station/.translations/ko.json b/homeassistant/components/ambient_station/.translations/ko.json index 3379411678b..2aa38688957 100644 --- a/homeassistant/components/ambient_station/.translations/ko.json +++ b/homeassistant/components/ambient_station/.translations/ko.json @@ -4,7 +4,6 @@ "already_configured": "\uc774 \uc571 \ud0a4\ub294 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." }, "error": { - "identifier_exists": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_key": "\uc560\ud50c\ub9ac\ucf00\uc774\uc158 \ud0a4 \ud639\uc740 API \ud0a4\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices": "\uacc4\uc815\uc5d0 \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/ambient_station/.translations/lb.json b/homeassistant/components/ambient_station/.translations/lb.json index 891051bae00..1c6f9224c57 100644 --- a/homeassistant/components/ambient_station/.translations/lb.json +++ b/homeassistant/components/ambient_station/.translations/lb.json @@ -4,7 +4,6 @@ "already_configured": "D\u00ebsen App Schl\u00ebssel g\u00ebtt scho benotzt" }, "error": { - "identifier_exists": "Applikatioun's Schl\u00ebssel an/oder API Schl\u00ebssel ass scho registr\u00e9iert", "invalid_key": "Ong\u00ebltegen API Schl\u00ebssel an/oder Applikatioun's Schl\u00ebssel", "no_devices": "Keng Apparater am Kont fonnt" }, diff --git a/homeassistant/components/ambient_station/.translations/nl.json b/homeassistant/components/ambient_station/.translations/nl.json index a070128eefe..bc8f90057e3 100644 --- a/homeassistant/components/ambient_station/.translations/nl.json +++ b/homeassistant/components/ambient_station/.translations/nl.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Applicatiesleutel en/of API-sleutel al geregistreerd", "invalid_key": "Ongeldige API-sleutel en/of applicatiesleutel", "no_devices": "Geen apparaten gevonden in account" }, diff --git a/homeassistant/components/ambient_station/.translations/no.json b/homeassistant/components/ambient_station/.translations/no.json index 2b915aafce1..b69081286ed 100644 --- a/homeassistant/components/ambient_station/.translations/no.json +++ b/homeassistant/components/ambient_station/.translations/no.json @@ -4,7 +4,6 @@ "already_configured": "Denne app n\u00f8kkelen er allerede i bruk." }, "error": { - "identifier_exists": "Programn\u00f8kkel og/eller API-n\u00f8kkel er allerede registrert", "invalid_key": "Ugyldig API-n\u00f8kkel og/eller programn\u00f8kkel", "no_devices": "Ingen enheter funnet i kontoen" }, diff --git a/homeassistant/components/ambient_station/.translations/pl.json b/homeassistant/components/ambient_station/.translations/pl.json index 5da886f05cd..45d98e64dbb 100644 --- a/homeassistant/components/ambient_station/.translations/pl.json +++ b/homeassistant/components/ambient_station/.translations/pl.json @@ -4,7 +4,6 @@ "already_configured": "Ten klucz aplikacji jest ju\u017c w u\u017cyciu." }, "error": { - "identifier_exists": "Klucz aplikacji i/lub klucz API ju\u017c jest zarejestrowany.", "invalid_key": "Nieprawid\u0142owy klucz API i/lub klucz aplikacji", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, diff --git a/homeassistant/components/ambient_station/.translations/pt-BR.json b/homeassistant/components/ambient_station/.translations/pt-BR.json index 61f5cea5e26..533d46ca8b7 100644 --- a/homeassistant/components/ambient_station/.translations/pt-BR.json +++ b/homeassistant/components/ambient_station/.translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Chave de aplicativo e / ou chave de API j\u00e1 registrada", "invalid_key": "Chave de API e / ou chave de aplicativo inv\u00e1lidas", "no_devices": "Nenhum dispositivo encontrado na conta" }, diff --git a/homeassistant/components/ambient_station/.translations/pt.json b/homeassistant/components/ambient_station/.translations/pt.json index 92746b29f3d..61d8bf3ae1c 100644 --- a/homeassistant/components/ambient_station/.translations/pt.json +++ b/homeassistant/components/ambient_station/.translations/pt.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Chave de aplica\u00e7\u00e3o e/ou chave de API j\u00e1 registradas.", "invalid_key": "Chave de API e/ou chave de aplica\u00e7\u00e3o inv\u00e1lidas", "no_devices": "Nenhum dispositivo encontrado na conta" }, diff --git a/homeassistant/components/ambient_station/.translations/ru.json b/homeassistant/components/ambient_station/.translations/ru.json index 07f3907eea1..e1f01d1567f 100644 --- a/homeassistant/components/ambient_station/.translations/ru.json +++ b/homeassistant/components/ambient_station/.translations/ru.json @@ -4,7 +4,6 @@ "already_configured": "\u042d\u0442\u043e\u0442 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "error": { - "identifier_exists": "\u041a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 API \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d.", "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 API \u0438/\u0438\u043b\u0438 \u043a\u043b\u044e\u0447 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", "no_devices": "\u0412 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u044b." }, diff --git a/homeassistant/components/ambient_station/.translations/sl.json b/homeassistant/components/ambient_station/.translations/sl.json index d5cf039b9f4..4f9389e7e49 100644 --- a/homeassistant/components/ambient_station/.translations/sl.json +++ b/homeassistant/components/ambient_station/.translations/sl.json @@ -4,7 +4,6 @@ "already_configured": "Ta klju\u010d za aplikacijo je \u017ee v uporabi." }, "error": { - "identifier_exists": "Aplikacijski klju\u010d in / ali klju\u010d API je \u017ee registriran", "invalid_key": "Neveljaven klju\u010d API in / ali klju\u010d aplikacije", "no_devices": "V ra\u010dunu ni najdene nobene naprave" }, diff --git a/homeassistant/components/ambient_station/.translations/sv.json b/homeassistant/components/ambient_station/.translations/sv.json index c429d439503..2f68fe4332d 100644 --- a/homeassistant/components/ambient_station/.translations/sv.json +++ b/homeassistant/components/ambient_station/.translations/sv.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Applikationsnyckel och/eller API-nyckel \u00e4r redan registrerade", "invalid_key": "Ogiltigt API-nyckel och/eller applikationsnyckel", "no_devices": "Inga enheter hittades i kontot" }, diff --git a/homeassistant/components/ambient_station/.translations/zh-Hans.json b/homeassistant/components/ambient_station/.translations/zh-Hans.json index 866c06316f1..dc6f2d51ee9 100644 --- a/homeassistant/components/ambient_station/.translations/zh-Hans.json +++ b/homeassistant/components/ambient_station/.translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Application Key \u548c/\u6216 API Key \u5df2\u6ce8\u518c", "invalid_key": "\u65e0\u6548\u7684 API \u5bc6\u94a5\u548c/\u6216 Application Key", "no_devices": "\u6ca1\u6709\u5728\u5e10\u6237\u4e2d\u627e\u5230\u8bbe\u5907" }, diff --git a/homeassistant/components/ambient_station/.translations/zh-Hant.json b/homeassistant/components/ambient_station/.translations/zh-Hant.json index 6de1579f6ff..fdc7b87aa6b 100644 --- a/homeassistant/components/ambient_station/.translations/zh-Hant.json +++ b/homeassistant/components/ambient_station/.translations/zh-Hant.json @@ -4,7 +4,6 @@ "already_configured": "\u6b64\u61c9\u7528\u7a0b\u5f0f\u5bc6\u9470\u5df2\u88ab\u4f7f\u7528\u3002" }, "error": { - "identifier_exists": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u5df2\u8a3b\u518a", "invalid_key": "API \u5bc6\u9470\u53ca/\u6216\u61c9\u7528\u5bc6\u9470\u7121\u6548", "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, diff --git a/homeassistant/components/axis/.translations/ca.json b/homeassistant/components/axis/.translations/ca.json index 58f5c0e4ad2..b391af0e609 100644 --- a/homeassistant/components/axis/.translations/ca.json +++ b/homeassistant/components/axis/.translations/ca.json @@ -4,8 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat", "bad_config_file": "Dades incorrectes del fitxer de configuraci\u00f3", "link_local_address": "L'enlla\u00e7 d'adreces locals no est\u00e0 disponible", - "not_axis_device": "El dispositiu descobert no \u00e9s un dispositiu Axis", - "updated_configuration": "S'ha actualitzat la configuraci\u00f3 del dispositiu amb l'adre\u00e7a nova" + "not_axis_device": "El dispositiu descobert no \u00e9s un dispositiu Axis" }, "error": { "already_configured": "El dispositiu ja est\u00e0 configurat", diff --git a/homeassistant/components/axis/.translations/da.json b/homeassistant/components/axis/.translations/da.json index 355dbad83d5..21f33d120f7 100644 --- a/homeassistant/components/axis/.translations/da.json +++ b/homeassistant/components/axis/.translations/da.json @@ -4,8 +4,7 @@ "already_configured": "Enheden er allerede konfigureret", "bad_config_file": "Forkerte data fra konfigurationsfilen", "link_local_address": "Link lokale adresser underst\u00f8ttes ikke", - "not_axis_device": "Fundet enhed ikke en Axis enhed", - "updated_configuration": "Opdaterede enhedskonfiguration med ny v\u00e6rtsadresse" + "not_axis_device": "Fundet enhed ikke en Axis enhed" }, "error": { "already_configured": "Enheden er allerede konfigureret", diff --git a/homeassistant/components/axis/.translations/de.json b/homeassistant/components/axis/.translations/de.json index a92c948a2a7..f238b00e847 100644 --- a/homeassistant/components/axis/.translations/de.json +++ b/homeassistant/components/axis/.translations/de.json @@ -4,8 +4,7 @@ "already_configured": "Ger\u00e4t ist bereits konfiguriert", "bad_config_file": "Fehlerhafte Daten aus der Konfigurationsdatei", "link_local_address": "Link-local Adressen werden nicht unterst\u00fctzt", - "not_axis_device": "Erkanntes Ger\u00e4t ist kein Axis-Ger\u00e4t", - "updated_configuration": "Ger\u00e4tekonfiguration mit neuer Hostadresse aktualisiert" + "not_axis_device": "Erkanntes Ger\u00e4t ist kein Axis-Ger\u00e4t" }, "error": { "already_configured": "Ger\u00e4t ist bereits konfiguriert", diff --git a/homeassistant/components/axis/.translations/en.json b/homeassistant/components/axis/.translations/en.json index 1f00800422c..b56cb0c5b74 100644 --- a/homeassistant/components/axis/.translations/en.json +++ b/homeassistant/components/axis/.translations/en.json @@ -4,8 +4,7 @@ "already_configured": "Device is already configured", "bad_config_file": "Bad data from configuration file", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Discovered device not an Axis device", - "updated_configuration": "Updated device configuration with new host address" + "not_axis_device": "Discovered device not an Axis device" }, "error": { "already_configured": "Device is already configured", diff --git a/homeassistant/components/axis/.translations/es.json b/homeassistant/components/axis/.translations/es.json index 885e8f68913..3f7db674fdf 100644 --- a/homeassistant/components/axis/.translations/es.json +++ b/homeassistant/components/axis/.translations/es.json @@ -4,8 +4,7 @@ "already_configured": "El dispositivo ya est\u00e1 configurado", "bad_config_file": "Datos err\u00f3neos en el archivo de configuraci\u00f3n", "link_local_address": "Las direcciones de enlace locales no son compatibles", - "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis", - "updated_configuration": "Configuraci\u00f3n del dispositivo actualizada con la nueva direcci\u00f3n de host" + "not_axis_device": "El dispositivo descubierto no es un dispositivo de Axis" }, "error": { "already_configured": "El dispositivo ya est\u00e1 configurado", diff --git a/homeassistant/components/axis/.translations/fr.json b/homeassistant/components/axis/.translations/fr.json index 07cfbd46504..608e12d020a 100644 --- a/homeassistant/components/axis/.translations/fr.json +++ b/homeassistant/components/axis/.translations/fr.json @@ -4,8 +4,7 @@ "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", "bad_config_file": "Mauvaises donn\u00e9es du fichier de configuration", "link_local_address": "Les adresses locales ne sont pas prises en charge", - "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis", - "updated_configuration": "Mise \u00e0 jour de la configuration du dispositif avec la nouvelle adresse de l'h\u00f4te" + "not_axis_device": "L'appareil d\u00e9couvert n'est pas un appareil Axis" }, "error": { "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9", diff --git a/homeassistant/components/axis/.translations/hu.json b/homeassistant/components/axis/.translations/hu.json index 4f05087cad8..b6347e21744 100644 --- a/homeassistant/components/axis/.translations/hu.json +++ b/homeassistant/components/axis/.translations/hu.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "updated_configuration": "Friss\u00edtett eszk\u00f6zkonfigur\u00e1ci\u00f3 \u00faj \u00e1llom\u00e1sc\u00edmmel" - }, "error": { "already_configured": "Az eszk\u00f6zt m\u00e1r konfigur\u00e1ltuk", "device_unavailable": "Az eszk\u00f6z nem \u00e9rhet\u0151 el", diff --git a/homeassistant/components/axis/.translations/it.json b/homeassistant/components/axis/.translations/it.json index 9e2eecf5747..3f303140c68 100644 --- a/homeassistant/components/axis/.translations/it.json +++ b/homeassistant/components/axis/.translations/it.json @@ -4,8 +4,7 @@ "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", "bad_config_file": "Dati errati dal file di configurazione", "link_local_address": "Gli indirizzi locali di collegamento non sono supportati", - "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis", - "updated_configuration": "Configurazione del dispositivo aggiornata con nuovo indirizzo host" + "not_axis_device": "Il dispositivo rilevato non \u00e8 un dispositivo Axis" }, "error": { "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", diff --git a/homeassistant/components/axis/.translations/ko.json b/homeassistant/components/axis/.translations/ko.json index 3f1aa97f266..648bd3cfd7d 100644 --- a/homeassistant/components/axis/.translations/ko.json +++ b/homeassistant/components/axis/.translations/ko.json @@ -4,8 +4,7 @@ "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "bad_config_file": "\uad6c\uc131 \ud30c\uc77c\uc5d0 \uc798\ubabb\ub41c \ub370\uc774\ud130\uac00 \uc788\uc2b5\ub2c8\ub2e4", "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", - "not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4", - "updated_configuration": "\uc0c8\ub85c\uc6b4 \ud638\uc2a4\ud2b8 \uc8fc\uc18c\ub85c \uc5c5\ub370\uc774\ud2b8\ub41c \uae30\uae30 \uad6c\uc131" + "not_axis_device": "\ubc1c\uacac\ub41c \uae30\uae30\ub294 Axis \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", diff --git a/homeassistant/components/axis/.translations/lb.json b/homeassistant/components/axis/.translations/lb.json index 589932cd68e..24ee0e24125 100644 --- a/homeassistant/components/axis/.translations/lb.json +++ b/homeassistant/components/axis/.translations/lb.json @@ -4,8 +4,7 @@ "already_configured": "Apparat ass scho konfigur\u00e9iert", "bad_config_file": "Feelerhaft Donn\u00e9e\u00eb aus der Konfiguratioun's Datei", "link_local_address": "Lokal Link Adressen ginn net \u00ebnnerst\u00ebtzt", - "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat", - "updated_configuration": "Konfiguratioun vum Apparat gouf mat der neier Adress aktualis\u00e9iert" + "not_axis_device": "Entdeckten Apparat ass keen Axis Apparat" }, "error": { "already_configured": "Apparat ass scho konfigur\u00e9iert", diff --git a/homeassistant/components/axis/.translations/nl.json b/homeassistant/components/axis/.translations/nl.json index b512690e2a3..10fc8c02d66 100644 --- a/homeassistant/components/axis/.translations/nl.json +++ b/homeassistant/components/axis/.translations/nl.json @@ -4,8 +4,7 @@ "already_configured": "Apparaat is al geconfigureerd", "bad_config_file": "Slechte gegevens van het configuratiebestand", "link_local_address": "Link-lokale adressen worden niet ondersteund", - "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat", - "updated_configuration": "Bijgewerkte apparaatconfiguratie met nieuw hostadres" + "not_axis_device": "Ontdekte apparaat, is geen Axis-apparaat" }, "error": { "already_configured": "Apparaat is al geconfigureerd", diff --git a/homeassistant/components/axis/.translations/no.json b/homeassistant/components/axis/.translations/no.json index 010472d2cce..1ad7a446cfa 100644 --- a/homeassistant/components/axis/.translations/no.json +++ b/homeassistant/components/axis/.translations/no.json @@ -4,8 +4,7 @@ "already_configured": "Enheten er allerede konfigurert", "bad_config_file": "D\u00e5rlige data fra konfigurasjonsfilen", "link_local_address": "Linking av lokale adresser st\u00f8ttes ikke", - "not_axis_device": "Oppdaget enhet ikke en Axis enhet", - "updated_configuration": "Oppdatert enhetskonfigurasjonen med ny vertsadresse" + "not_axis_device": "Oppdaget enhet ikke en Axis enhet" }, "error": { "already_configured": "Enheten er allerede konfigurert", diff --git a/homeassistant/components/axis/.translations/pl.json b/homeassistant/components/axis/.translations/pl.json index 9f7bb55147d..dd1a63039e2 100644 --- a/homeassistant/components/axis/.translations/pl.json +++ b/homeassistant/components/axis/.translations/pl.json @@ -4,8 +4,7 @@ "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", "bad_config_file": "B\u0142\u0119dne dane z pliku konfiguracyjnego", "link_local_address": "Po\u0142\u0105czenie lokalnego adresu nie jest obs\u0142ugiwane", - "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis", - "updated_configuration": "Zaktualizowano konfiguracj\u0119 urz\u0105dzenia o nowy adres hosta" + "not_axis_device": "Wykryte urz\u0105dzenie nie jest urz\u0105dzeniem Axis" }, "error": { "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane.", diff --git a/homeassistant/components/axis/.translations/pt-BR.json b/homeassistant/components/axis/.translations/pt-BR.json index ceb6325af60..453c8fa3643 100644 --- a/homeassistant/components/axis/.translations/pt-BR.json +++ b/homeassistant/components/axis/.translations/pt-BR.json @@ -4,8 +4,7 @@ "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", "bad_config_file": "Dados incorretos do arquivo de configura\u00e7\u00e3o", "link_local_address": "Link de endere\u00e7os locais n\u00e3o s\u00e3o suportados", - "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis", - "updated_configuration": "Configura\u00e7\u00e3o do dispositivo atualizada com novo endere\u00e7o de host" + "not_axis_device": "Dispositivo descoberto n\u00e3o \u00e9 um dispositivo Axis" }, "error": { "already_configured": "O dispositivo j\u00e1 est\u00e1 configurado", diff --git a/homeassistant/components/axis/.translations/ru.json b/homeassistant/components/axis/.translations/ru.json index b0da189d20f..d9e3a40d304 100644 --- a/homeassistant/components/axis/.translations/ru.json +++ b/homeassistant/components/axis/.translations/ru.json @@ -4,8 +4,7 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "bad_config_file": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0438\u0437 \u0444\u0430\u0439\u043b\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438.", "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", - "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis.", - "updated_configuration": "\u0410\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d." + "not_axis_device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 \u044f\u0432\u043b\u044f\u0435\u0442\u0441\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e\u043c Axis." }, "error": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", diff --git a/homeassistant/components/axis/.translations/sl.json b/homeassistant/components/axis/.translations/sl.json index 44a701ed117..9d66831b91a 100644 --- a/homeassistant/components/axis/.translations/sl.json +++ b/homeassistant/components/axis/.translations/sl.json @@ -4,8 +4,7 @@ "already_configured": "Naprava je \u017ee konfigurirana", "bad_config_file": "Slabi podatki iz konfiguracijske datoteke", "link_local_address": "Lokalni naslovi povezave niso podprti", - "not_axis_device": "Odkrita naprava ni naprava Axis", - "updated_configuration": "Posodobljena konfiguracija naprave z novim naslovom gostitelja" + "not_axis_device": "Odkrita naprava ni naprava Axis" }, "error": { "already_configured": "Naprava je \u017ee konfigurirana", diff --git a/homeassistant/components/axis/.translations/sv.json b/homeassistant/components/axis/.translations/sv.json index 59761c7202f..76ceaf7cbd7 100644 --- a/homeassistant/components/axis/.translations/sv.json +++ b/homeassistant/components/axis/.translations/sv.json @@ -4,8 +4,7 @@ "already_configured": "Enheten \u00e4r redan konfigurerad", "bad_config_file": "Felaktig data fr\u00e5n konfigurationsfilen", "link_local_address": "Link local addresses are not supported", - "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet", - "updated_configuration": "Uppdaterad enhetskonfiguration med ny v\u00e4rdadress" + "not_axis_device": "Uppt\u00e4ckte enhet som inte \u00e4r en Axis enhet" }, "error": { "already_configured": "Enheten \u00e4r redan konfigurerad", diff --git a/homeassistant/components/axis/.translations/zh-Hant.json b/homeassistant/components/axis/.translations/zh-Hant.json index ac552afe583..41ecfdb80b7 100644 --- a/homeassistant/components/axis/.translations/zh-Hant.json +++ b/homeassistant/components/axis/.translations/zh-Hant.json @@ -4,8 +4,7 @@ "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "bad_config_file": "\u8a2d\u5b9a\u6a94\u6848\u8cc7\u6599\u7121\u6548\u932f\u8aa4", "link_local_address": "\u4e0d\u652f\u63f4\u9023\u7d50\u672c\u5730\u7aef\u4f4d\u5740", - "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099", - "updated_configuration": "\u4f7f\u7528\u65b0\u4e3b\u6a5f\u7aef\u4f4d\u5740\u66f4\u65b0\u88dd\u7f6e\u8a2d\u5b9a" + "not_axis_device": "\u6240\u767c\u73fe\u7684\u8a2d\u5099\u4e26\u975e Axis \u8a2d\u5099" }, "error": { "already_configured": "\u8a2d\u5099\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", diff --git a/homeassistant/components/binary_sensor/.translations/bg.json b/homeassistant/components/binary_sensor/.translations/bg.json index 373866ecd8c..3006b8cadbc 100644 --- a/homeassistant/components/binary_sensor/.translations/bg.json +++ b/homeassistant/components/binary_sensor/.translations/bg.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} \u0438\u0437\u0442\u043e\u0449\u0435\u043d\u0430 \u0431\u0430\u0442\u0435\u0440\u0438\u044f", - "closed": "{entity_name} \u0437\u0430\u0442\u0432\u043e\u0440\u0435\u043d", "cold": "{entity_name} \u0441\u0435 \u0438\u0437\u0441\u0442\u0443\u0434\u0438", "connected": "{entity_name} \u0441\u0432\u044a\u0440\u0437\u0430\u043d", "gas": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", @@ -54,7 +53,6 @@ "light": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u0430", "locked": "{entity_name} \u0437\u0430\u043a\u043b\u044e\u0447\u0435\u043d", "moist": "{entity_name} \u0441\u0442\u0430\u043d\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", - "moist\u00a7": "{entity_name} \u0441\u0442\u0430\u0432\u0430 \u0432\u043b\u0430\u0436\u0435\u043d", "motion": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430\u043d\u0435 \u043d\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "moving": "{entity_name} \u0437\u0430\u043f\u043e\u0447\u043d\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "no_gas": "{entity_name} \u0441\u043f\u0440\u044f \u0434\u0430 \u043e\u0442\u043a\u0440\u0438\u0432\u0430 \u0433\u0430\u0437", diff --git a/homeassistant/components/binary_sensor/.translations/ca.json b/homeassistant/components/binary_sensor/.translations/ca.json index 8bbd19a0d45..3a3485a3be7 100644 --- a/homeassistant/components/binary_sensor/.translations/ca.json +++ b/homeassistant/components/binary_sensor/.translations/ca.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "Bateria de {entity_name} baixa", - "closed": "{entity_name} est\u00e0 tancat", "cold": "{entity_name} es torna fred", "connected": "{entity_name} est\u00e0 connectat", "gas": "{entity_name} ha comen\u00e7at a detectar gas", @@ -54,7 +53,6 @@ "light": "{entity_name} ha comen\u00e7at a detectar llum", "locked": "{entity_name} est\u00e0 bloquejat", "moist": "{entity_name} es torna humit", - "moist\u00a7": "{entity_name} es torna humit", "motion": "{entity_name} ha comen\u00e7at a detectar moviment", "moving": "{entity_name} ha comen\u00e7at a moure's", "no_gas": "{entity_name} ha deixat de detectar gas", diff --git a/homeassistant/components/binary_sensor/.translations/da.json b/homeassistant/components/binary_sensor/.translations/da.json index 19229c16cb3..ffa68b094be 100644 --- a/homeassistant/components/binary_sensor/.translations/da.json +++ b/homeassistant/components/binary_sensor/.translations/da.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} lavt batteriniveau", - "closed": "{entity_name} lukket", "cold": "{entity_name} blev kold", "connected": "{entity_name} tilsluttet", "gas": "{entity_name} begyndte at registrere gas", @@ -54,7 +53,6 @@ "light": "{entity_name} begyndte at registrere lys", "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} blev fugtig", - "moist\u00a7": "{entity_name} blev fugtig", "motion": "{entity_name} begyndte at registrere bev\u00e6gelse", "moving": "{entity_name} begyndte at bev\u00e6ge sig", "no_gas": "{entity_name} stoppede med at registrere gas", diff --git a/homeassistant/components/binary_sensor/.translations/de.json b/homeassistant/components/binary_sensor/.translations/de.json index e246198864b..55a079ca42a 100644 --- a/homeassistant/components/binary_sensor/.translations/de.json +++ b/homeassistant/components/binary_sensor/.translations/de.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} Batterie schwach", - "closed": "{entity_name} geschlossen", "cold": "{entity_name} wurde kalt", "connected": "{entity_name} verbunden", "gas": "{entity_name} hat Gas detektiert", @@ -54,7 +53,6 @@ "light": "{entity_name} hat Licht detektiert", "locked": "{entity_name} gesperrt", "moist": "{entity_name} wurde feucht", - "moist\u00a7": "{entity_name} wurde feucht", "motion": "{entity_name} hat Bewegungen detektiert", "moving": "{entity_name} hat angefangen sich zu bewegen", "no_gas": "{entity_name} hat kein Gas mehr erkannt", diff --git a/homeassistant/components/binary_sensor/.translations/en.json b/homeassistant/components/binary_sensor/.translations/en.json index 93b61893980..213d947236c 100644 --- a/homeassistant/components/binary_sensor/.translations/en.json +++ b/homeassistant/components/binary_sensor/.translations/en.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} battery low", - "closed": "{entity_name} closed", "cold": "{entity_name} became cold", "connected": "{entity_name} connected", "gas": "{entity_name} started detecting gas", @@ -54,7 +53,6 @@ "light": "{entity_name} started detecting light", "locked": "{entity_name} locked", "moist": "{entity_name} became moist", - "moist\u00a7": "{entity_name} became moist", "motion": "{entity_name} started detecting motion", "moving": "{entity_name} started moving", "no_gas": "{entity_name} stopped detecting gas", diff --git a/homeassistant/components/binary_sensor/.translations/es-419.json b/homeassistant/components/binary_sensor/.translations/es-419.json index e727e18775a..18b5e060818 100644 --- a/homeassistant/components/binary_sensor/.translations/es-419.json +++ b/homeassistant/components/binary_sensor/.translations/es-419.json @@ -44,7 +44,6 @@ }, "trigger_type": { "bat_low": "{entity_name} bater\u00eda baja", - "closed": "{entity_name} cerrado", "cold": "{entity_name} se enfri\u00f3", "connected": "{entity_name} conectado", "gas": "{entity_name} comenz\u00f3 a detectar gas", @@ -52,7 +51,6 @@ "light": "{entity_name} comenz\u00f3 a detectar luz", "locked": "{entity_name} bloqueado", "moist": "{entity_name} se humedeci\u00f3", - "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} comenz\u00f3 a moverse", "no_gas": "{entity_name} dej\u00f3 de detectar gas", diff --git a/homeassistant/components/binary_sensor/.translations/es.json b/homeassistant/components/binary_sensor/.translations/es.json index 9720fb974f6..02fbc465252 100644 --- a/homeassistant/components/binary_sensor/.translations/es.json +++ b/homeassistant/components/binary_sensor/.translations/es.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} bater\u00eda baja", - "closed": "{entity_name} cerrado", "cold": "{entity_name} se enfri\u00f3", "connected": "{entity_name} conectado", "gas": "{entity_name} empez\u00f3 a detectar gas", @@ -54,7 +53,6 @@ "light": "{entity_name} empez\u00f3 a detectar la luz", "locked": "{entity_name} bloqueado", "moist": "{entity_name} se humedece", - "moist\u00a7": "{entity_name} se humedeci\u00f3", "motion": "{entity_name} comenz\u00f3 a detectar movimiento", "moving": "{entity_name} empez\u00f3 a moverse", "no_gas": "{entity_name} dej\u00f3 de detectar gas", diff --git a/homeassistant/components/binary_sensor/.translations/fr.json b/homeassistant/components/binary_sensor/.translations/fr.json index 65abfbcd0bd..f5b2e2bfd97 100644 --- a/homeassistant/components/binary_sensor/.translations/fr.json +++ b/homeassistant/components/binary_sensor/.translations/fr.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} batterie faible", - "closed": "{entity_name} ferm\u00e9", "cold": "{entity_name} est devenu froid", "connected": "{entity_name} connect\u00e9", "gas": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du gaz", @@ -54,7 +53,6 @@ "light": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter la lumi\u00e8re", "locked": "{entity_name} verrouill\u00e9", "moist": "{entity_name} est devenu humide", - "moist\u00a7": "{entity_name} est devenu humide", "motion": "{entity_name} a commenc\u00e9 \u00e0 d\u00e9tecter du mouvement", "moving": "{entity_name} a commenc\u00e9 \u00e0 se d\u00e9placer", "no_gas": "{entity_name} a arr\u00eat\u00e9 de d\u00e9tecter le gaz", diff --git a/homeassistant/components/binary_sensor/.translations/hu.json b/homeassistant/components/binary_sensor/.translations/hu.json index 7ec9b5268e2..d53e869e075 100644 --- a/homeassistant/components/binary_sensor/.translations/hu.json +++ b/homeassistant/components/binary_sensor/.translations/hu.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} akkufesz\u00fclts\u00e9g alacsony", - "closed": "{entity_name} be lett csukva", "cold": "{entity_name} hideg lett", "connected": "{entity_name} csatlakozik", "gas": "{entity_name} g\u00e1zt \u00e9rz\u00e9kel", @@ -54,7 +53,6 @@ "light": "{entity_name} f\u00e9nyt \u00e9rz\u00e9kel", "locked": "{entity_name} be lett z\u00e1rva", "moist": "{entity_name} nedves lett", - "moist\u00a7": "{entity_name} nedves lett", "motion": "{entity_name} mozg\u00e1st \u00e9rz\u00e9kel", "moving": "{entity_name} mozog", "no_gas": "{entity_name} m\u00e1r nem \u00e9rz\u00e9kel g\u00e1zt", diff --git a/homeassistant/components/binary_sensor/.translations/it.json b/homeassistant/components/binary_sensor/.translations/it.json index 74d295f3055..db897b68da0 100644 --- a/homeassistant/components/binary_sensor/.translations/it.json +++ b/homeassistant/components/binary_sensor/.translations/it.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} batteria scarica", - "closed": "{entity_name} \u00e8 chiuso", "cold": "{entity_name} \u00e8 diventato freddo", "connected": "{entity_name} connesso", "gas": "{entity_name} ha iniziato a rilevare il gas", @@ -54,7 +53,6 @@ "light": "{entity_name} ha iniziato a rilevare la luce", "locked": "{entity_name} bloccato", "moist": "{entity_name} diventato umido", - "moist\u00a7": "{entity_name} \u00e8 diventato umido", "motion": "{entity_name} ha iniziato a rilevare il movimento", "moving": "{entity_name} ha iniziato a muoversi", "no_gas": "{entity_name} ha smesso la rilevazione di gas", diff --git a/homeassistant/components/binary_sensor/.translations/ko.json b/homeassistant/components/binary_sensor/.translations/ko.json index 7fa745a9a9a..733d3a8de8f 100644 --- a/homeassistant/components/binary_sensor/.translations/ko.json +++ b/homeassistant/components/binary_sensor/.translations/ko.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} \ubc30\ud130\ub9ac \uc794\ub7c9\uc774 \ubd80\uc871\ud574\uc9c8 \ub54c", - "closed": "{entity_name} \uc774(\uac00) \ub2eb\ud790 \ub54c", "cold": "{entity_name} \uc774(\uac00) \ucc28\uac00\uc6cc\uc9c8 \ub54c", "connected": "{entity_name} \uc774(\uac00) \uc5f0\uacb0\ub420 \ub54c", "gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud560 \ub54c", @@ -54,7 +53,6 @@ "light": "{entity_name} \uc774(\uac00) \ube5b\uc744 \uac10\uc9c0\ud560 \ub54c", "locked": "{entity_name} \uc774(\uac00) \uc7a0\uae38 \ub54c", "moist": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", - "moist\u00a7": "{entity_name} \uc774(\uac00) \uc2b5\ud574\uc9c8 \ub54c", "motion": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc784\uc744 \uac10\uc9c0\ud560 \ub54c", "moving": "{entity_name} \uc774(\uac00) \uc6c0\uc9c1\uc77c \ub54c", "no_gas": "{entity_name} \uc774(\uac00) \uac00\uc2a4\ub97c \uac10\uc9c0\ud558\uc9c0 \uc54a\uac8c \ub420 \ub54c", diff --git a/homeassistant/components/binary_sensor/.translations/lb.json b/homeassistant/components/binary_sensor/.translations/lb.json index c65ae94396b..7120b1bb289 100644 --- a/homeassistant/components/binary_sensor/.translations/lb.json +++ b/homeassistant/components/binary_sensor/.translations/lb.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} Batterie niddereg", - "closed": "{entity_name} gouf zougemaach", "cold": "{entity_name} gouf kal", "connected": "{entity_name} ass verbonnen", "gas": "{entity_name} huet ugefaangen Gas z'entdecken", @@ -54,7 +53,6 @@ "light": "{entity_name} huet ugefange Luucht z'entdecken", "locked": "{entity_name} gespaart", "moist": "{entity_name} gouf fiicht", - "moist\u00a7": "{entity_name} gouf fiicht", "motion": "{entity_name} huet ugefaange Beweegung z'entdecken", "moving": "{entity_name} huet ugefaangen sech ze beweegen", "no_gas": "{entity_name} huet opgehale Gas z'entdecken", diff --git a/homeassistant/components/binary_sensor/.translations/nl.json b/homeassistant/components/binary_sensor/.translations/nl.json index 508a06b38a2..04d40ecf9b8 100644 --- a/homeassistant/components/binary_sensor/.translations/nl.json +++ b/homeassistant/components/binary_sensor/.translations/nl.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} batterij bijna leeg", - "closed": "{entity_name} gesloten", "cold": "{entity_name} werd koud", "connected": "{entity_name} verbonden", "gas": "{entity_name} begon gas te detecteren", @@ -54,7 +53,6 @@ "light": "{entity_name} begon licht te detecteren", "locked": "{entity_name} vergrendeld", "moist": "{entity_name} werd vochtig", - "moist\u00a7": "{entity_name} werd vochtig", "motion": "{entity_name} begon beweging te detecteren", "moving": "{entity_name} begon te bewegen", "no_gas": "{entity_name} is gestopt met het detecteren van gas", diff --git a/homeassistant/components/binary_sensor/.translations/no.json b/homeassistant/components/binary_sensor/.translations/no.json index 4194102948b..b82dd8b0533 100644 --- a/homeassistant/components/binary_sensor/.translations/no.json +++ b/homeassistant/components/binary_sensor/.translations/no.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} lavt batteri", - "closed": "{entity_name} stengt", "cold": "{entity_name} ble kald", "connected": "{entity_name} tilkoblet", "gas": "{entity_name} begynte \u00e5 registrere gass", @@ -54,7 +53,6 @@ "light": "{entity_name} begynte \u00e5 registrere lys", "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} ble fuktig", - "moist\u00a7": "{entity_name} ble fuktig", "motion": "{entity_name} begynte \u00e5 registrere bevegelse", "moving": "{entity_name} begynte \u00e5 bevege seg", "no_gas": "{entity_name} sluttet \u00e5 registrere gass", diff --git a/homeassistant/components/binary_sensor/.translations/pl.json b/homeassistant/components/binary_sensor/.translations/pl.json index bc474e3d514..ef174e72336 100644 --- a/homeassistant/components/binary_sensor/.translations/pl.json +++ b/homeassistant/components/binary_sensor/.translations/pl.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "nast\u0105pi roz\u0142adowanie baterii {entity_name}", - "closed": "nast\u0105pi zamkni\u0119cie {entity_name}", "cold": "sensor {entity_name} wykryje zimno", "connected": "nast\u0105pi pod\u0142\u0105czenie {entity_name}", "gas": "sensor {entity_name} wykryje gaz", @@ -54,7 +53,6 @@ "light": "sensor {entity_name} wykryje \u015bwiat\u0142o", "locked": "nast\u0105pi zamkni\u0119cie {entity_name}", "moist": "nast\u0105pi wykrycie wilgoci {entity_name}", - "moist\u00a7": "sensor {entity_name} wykryje wilgo\u0107", "motion": "sensor {entity_name} wykryje ruch", "moving": "sensor {entity_name} zacznie porusza\u0107 si\u0119", "no_gas": "sensor {entity_name} przestanie wykrywa\u0107 gaz", diff --git a/homeassistant/components/binary_sensor/.translations/pt.json b/homeassistant/components/binary_sensor/.translations/pt.json index aa16576d2c1..caea4c6c97a 100644 --- a/homeassistant/components/binary_sensor/.translations/pt.json +++ b/homeassistant/components/binary_sensor/.translations/pt.json @@ -17,7 +17,6 @@ "is_vibration": "{entity_name} est\u00e1 a detectar vibra\u00e7\u00f5es" }, "trigger_type": { - "closed": "{entity_name} est\u00e1 fechado", "moist": "ficou h\u00famido {entity_name}", "not_opened": "fechado {entity_name}", "not_plugged_in": "{entity_name} desligado", diff --git a/homeassistant/components/binary_sensor/.translations/ru.json b/homeassistant/components/binary_sensor/.translations/ru.json index 4c9cfb99a1c..fe1323c6744 100644 --- a/homeassistant/components/binary_sensor/.translations/ru.json +++ b/homeassistant/components/binary_sensor/.translations/ru.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0443\u0435\u0442 \u043d\u0438\u0437\u043a\u0438\u0439 \u0437\u0430\u0440\u044f\u0434", - "closed": "{entity_name} \u0437\u0430\u043a\u0440\u044b\u0432\u0430\u0435\u0442\u0441\u044f", "cold": "{entity_name} \u043e\u0445\u043b\u0430\u0436\u0434\u0430\u0435\u0442\u0441\u044f", "connected": "{entity_name} \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", "gas": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", @@ -54,7 +53,6 @@ "light": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0441\u0432\u0435\u0442", "locked": "{entity_name} \u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0435\u0442\u0441\u044f", "moist": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", - "moist\u00a7": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0432\u043b\u0430\u0433\u0443", "motion": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0434\u0432\u0438\u0436\u0435\u043d\u0438\u0435", "moving": "{entity_name} \u043d\u0430\u0447\u0438\u043d\u0430\u0435\u0442 \u043f\u0435\u0440\u0435\u043c\u0435\u0449\u0435\u043d\u0438\u0435", "no_gas": "{entity_name} \u043f\u0440\u0435\u043a\u0440\u0430\u0449\u0430\u0435\u0442 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0438\u0432\u0430\u0442\u044c \u0433\u0430\u0437", diff --git a/homeassistant/components/binary_sensor/.translations/sl.json b/homeassistant/components/binary_sensor/.translations/sl.json index 2004caeb342..234146e2e6f 100644 --- a/homeassistant/components/binary_sensor/.translations/sl.json +++ b/homeassistant/components/binary_sensor/.translations/sl.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} ima prazno baterijo", - "closed": "{entity_name} zaprto", "cold": "{entity_name} je postal hladen", "connected": "{entity_name} povezan", "gas": "{entity_name} za\u010del zaznavati plin", @@ -54,7 +53,6 @@ "light": "{entity_name} za\u010del zaznavati svetlobo", "locked": "{entity_name} zaklenjen", "moist": "{entity_name} postal vla\u017een", - "moist\u00a7": "{entity_name} postal vla\u017een", "motion": "{entity_name} za\u010del zaznavati gibanje", "moving": "{entity_name} se je za\u010del premikati", "no_gas": "{entity_name} prenehal zaznavati plin", diff --git a/homeassistant/components/binary_sensor/.translations/sv.json b/homeassistant/components/binary_sensor/.translations/sv.json index 5df2ce17c92..ec5d57daa79 100644 --- a/homeassistant/components/binary_sensor/.translations/sv.json +++ b/homeassistant/components/binary_sensor/.translations/sv.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name} batteri l\u00e5gt", - "closed": "{entity_name} st\u00e4ngd", "cold": "{entity_name} blev kall", "connected": "{entity_name} ansluten", "gas": "{entity_name} b\u00f6rjade detektera gas", @@ -54,7 +53,6 @@ "light": "{entity_name} b\u00f6rjade uppt\u00e4cka ljus", "locked": "{entity_name} l\u00e5st", "moist": "{entity_name} blev fuktig", - "moist\u00a7": "{entity_name} blev fuktig", "motion": "{entity_name} b\u00f6rjade detektera r\u00f6relse", "moving": "{entity_name} b\u00f6rjade r\u00f6ra sig", "no_gas": "{entity_name} slutade uppt\u00e4cka gas", diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hans.json b/homeassistant/components/binary_sensor/.translations/zh-Hans.json index aeb24e5056a..9ad8e67e6b8 100644 --- a/homeassistant/components/binary_sensor/.translations/zh-Hans.json +++ b/homeassistant/components/binary_sensor/.translations/zh-Hans.json @@ -44,7 +44,6 @@ }, "trigger_type": { "bat_low": "{entity_name} \u7535\u6c60\u7535\u91cf\u4f4e", - "closed": "{entity_name} \u5df2\u5173\u95ed", "cold": "{entity_name} \u53d8\u51b7", "connected": "{entity_name} \u5df2\u8fde\u63a5", "gas": "{entity_name} \u5f00\u59cb\u68c0\u6d4b\u5230\u71c3\u6c14\u6cc4\u6f0f", diff --git a/homeassistant/components/binary_sensor/.translations/zh-Hant.json b/homeassistant/components/binary_sensor/.translations/zh-Hant.json index 712c0fbd7c1..7b48833dd7b 100644 --- a/homeassistant/components/binary_sensor/.translations/zh-Hant.json +++ b/homeassistant/components/binary_sensor/.translations/zh-Hant.json @@ -46,7 +46,6 @@ }, "trigger_type": { "bat_low": "{entity_name}\u96fb\u91cf\u4f4e", - "closed": "{entity_name}\u5df2\u95dc\u9589", "cold": "{entity_name}\u5df2\u8b8a\u51b7", "connected": "{entity_name}\u5df2\u9023\u7dda", "gas": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u6c23\u9ad4", @@ -54,7 +53,6 @@ "light": "{entity_name}\u5df2\u958b\u59cb\u5075\u6e2c\u5149\u7dda", "locked": "{entity_name}\u5df2\u4e0a\u9396", "moist": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", - "moist\u00a7": "{entity_name}\u5df2\u8b8a\u6f6e\u6fd5", "motion": "{entity_name}\u5df2\u5075\u6e2c\u5230\u52d5\u4f5c", "moving": "{entity_name}\u958b\u59cb\u79fb\u52d5", "no_gas": "{entity_name}\u5df2\u505c\u6b62\u5075\u6e2c\u6c23\u9ad4", diff --git a/homeassistant/components/brother/.translations/zh-Hant.json b/homeassistant/components/brother/.translations/zh-Hant.json index 0ef813dffea..987a15f8a2f 100644 --- a/homeassistant/components/brother/.translations/zh-Hant.json +++ b/homeassistant/components/brother/.translations/zh-Hant.json @@ -24,7 +24,7 @@ "type": "\u5370\u8868\u6a5f\u985e\u578b" }, "description": "\u662f\u5426\u8981\u5c07\u5e8f\u865f {serial_number} \u4e4bBrother \u5370\u8868\u6a5f {model} \u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u767c\u73fe Brother \u5370\u8868\u6a5f" + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Brother \u5370\u8868\u6a5f" } }, "title": "Brother \u5370\u8868\u6a5f" diff --git a/homeassistant/components/cert_expiry/.translations/bg.json b/homeassistant/components/cert_expiry/.translations/bg.json index a4a36cb04dc..cf89911071b 100644 --- a/homeassistant/components/cert_expiry/.translations/bg.json +++ b/homeassistant/components/cert_expiry/.translations/bg.json @@ -1,15 +1,8 @@ { "config": { - "abort": { - "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430" - }, "error": { - "certificate_error": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u0432\u0430\u043b\u0438\u0434\u0438\u0440\u0430\u043d", - "certificate_fetch_failed": "\u041d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0441\u0435 \u043c\u0438\u0437\u0432\u043b\u0435\u0447\u0435 \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043e\u0442 \u0442\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442", "connection_timeout": "\u041d\u0435\u0432\u044a\u0437\u043c\u043e\u0436\u043d\u043e\u0441\u0442 \u0437\u0430 \u0441\u0432\u043e\u0435\u0432\u0440\u0435\u043c\u0435\u043d\u043d\u043e \u0441\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u0441 \u0442\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441", - "host_port_exists": "\u0422\u0430\u0437\u0438 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u043e\u0442 \u0430\u0434\u0440\u0435\u0441 \u0438 \u043f\u043e\u0440\u0442 \u0435 \u0432\u0435\u0447\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0430", - "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d", - "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u044a\u0442 \u043d\u0435 \u0441\u044a\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0430 \u043d\u0430 \u0438\u043c\u0435\u0442\u043e \u043d\u0430 \u0445\u043e\u0441\u0442\u0430" + "resolve_failed": "\u0422\u043e\u0437\u0438 \u0430\u0434\u0440\u0435\u0441 \u043d\u0435 \u043c\u043e\u0436\u0435 \u0434\u0430 \u0431\u044a\u0434\u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ca.json b/homeassistant/components/cert_expiry/.translations/ca.json index 4786e258ff8..dce3519f09f 100644 --- a/homeassistant/components/cert_expiry/.translations/ca.json +++ b/homeassistant/components/cert_expiry/.translations/ca.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", - "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", "import_failed": "La importaci\u00f3 des de configuraci\u00f3 ha fallat" }, "error": { - "certificate_error": "El certificat no ha pogut ser validat", - "certificate_fetch_failed": "No s'ha pogut obtenir el certificat des d'aquesta combinaci\u00f3 d'amfitri\u00f3 i port", "connection_refused": "La connexi\u00f3 s'ha rebutjat en connectar-se a l'amfitri\u00f3", "connection_timeout": "S'ha acabat el temps d'espera durant la connexi\u00f3 amb l'amfitri\u00f3.", - "host_port_exists": "Aquesta combinaci\u00f3 d'amfitri\u00f3 i port ja est\u00e0 configurada", - "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3", - "wrong_host": "El certificat no coincideix amb el nom de l'amfitri\u00f3" + "resolve_failed": "No s'ha pogut resoldre l'amfitri\u00f3" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/da.json b/homeassistant/components/cert_expiry/.translations/da.json index 26ee436860a..cf5f42338c3 100644 --- a/homeassistant/components/cert_expiry/.translations/da.json +++ b/homeassistant/components/cert_expiry/.translations/da.json @@ -1,15 +1,8 @@ { "config": { - "abort": { - "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret" - }, "error": { - "certificate_error": "Certifikatet kunne ikke valideres", - "certificate_fetch_failed": "Kan ikke hente certifikat fra denne v\u00e6rt- og portkombination", "connection_timeout": "Timeout ved tilslutning til denne v\u00e6rt", - "host_port_exists": "Denne v\u00e6rt- og portkombination er allerede konfigureret", - "resolve_failed": "V\u00e6rten kunne ikke findes", - "wrong_host": "Certifikatet stemmer ikke overens med v\u00e6rtsnavnet" + "resolve_failed": "V\u00e6rten kunne ikke findes" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/de.json b/homeassistant/components/cert_expiry/.translations/de.json index edf116203c7..119d172690a 100644 --- a/homeassistant/components/cert_expiry/.translations/de.json +++ b/homeassistant/components/cert_expiry/.translations/de.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Diese Kombination aus Host und Port ist bereits konfiguriert.", - "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.", "import_failed": "Import aus Konfiguration fehlgeschlagen" }, "error": { - "certificate_error": "Zertifikat konnte nicht validiert werden", - "certificate_fetch_failed": "Zertifikat kann von dieser Kombination aus Host und Port nicht abgerufen werden", "connection_refused": "Verbindung beim Herstellen einer Verbindung zum Host abgelehnt", "connection_timeout": "Zeit\u00fcberschreitung beim Herstellen einer Verbindung mit diesem Host", - "host_port_exists": "Diese Kombination aus Host und Port ist bereits konfiguriert.", - "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden", - "wrong_host": "Zertifikat stimmt nicht mit Hostname \u00fcberein" + "resolve_failed": "Dieser Host kann nicht aufgel\u00f6st werden" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/en.json b/homeassistant/components/cert_expiry/.translations/en.json index 1c1b9a882e3..5aca41f7f78 100644 --- a/homeassistant/components/cert_expiry/.translations/en.json +++ b/homeassistant/components/cert_expiry/.translations/en.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "This host and port combination is already configured", - "host_port_exists": "This host and port combination is already configured", "import_failed": "Import from config failed" }, "error": { - "certificate_error": "Certificate could not be validated", - "certificate_fetch_failed": "Can not fetch certificate from this host and port combination", "connection_refused": "Connection refused when connecting to host", "connection_timeout": "Timeout when connecting to this host", - "host_port_exists": "This host and port combination is already configured", - "resolve_failed": "This host can not be resolved", - "wrong_host": "Certificate does not match hostname" + "resolve_failed": "This host can not be resolved" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/es-419.json b/homeassistant/components/cert_expiry/.translations/es-419.json index e350faffcb3..4e0b1ffca5d 100644 --- a/homeassistant/components/cert_expiry/.translations/es-419.json +++ b/homeassistant/components/cert_expiry/.translations/es-419.json @@ -1,15 +1,8 @@ { "config": { - "abort": { - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada" - }, "error": { - "certificate_error": "El certificado no pudo ser validado", - "certificate_fetch_failed": "No se puede recuperar el certificado de esta combinaci\u00f3n de host y puerto", "connection_timeout": "Tiempo de espera al conectarse a este host", - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", - "resolve_failed": "Este host no puede resolverse", - "wrong_host": "El certificado no coincide con el nombre de host" + "resolve_failed": "Este host no puede resolverse" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/es.json b/homeassistant/components/cert_expiry/.translations/es.json index 628f2b22e21..7cc44d7038a 100644 --- a/homeassistant/components/cert_expiry/.translations/es.json +++ b/homeassistant/components/cert_expiry/.translations/es.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", "import_failed": "No se pudo importar desde la configuraci\u00f3n" }, "error": { - "certificate_error": "El certificado no pudo ser validado", - "certificate_fetch_failed": "No se puede obtener el certificado de esta combinaci\u00f3n de host y puerto", "connection_refused": "Conexi\u00f3n rechazada al conectarse al host", "connection_timeout": "Tiempo de espera agotado al conectar a este host", - "host_port_exists": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", - "resolve_failed": "Este host no se puede resolver", - "wrong_host": "El certificado no coincide con el nombre de host" + "resolve_failed": "Este host no se puede resolver" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/fr.json b/homeassistant/components/cert_expiry/.translations/fr.json index 245b899fadf..18398a2b048 100644 --- a/homeassistant/components/cert_expiry/.translations/fr.json +++ b/homeassistant/components/cert_expiry/.translations/fr.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Cette combinaison h\u00f4te et port est d\u00e9j\u00e0 configur\u00e9e", - "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", "import_failed": "\u00c9chec de l'importation \u00e0 partir de la configuration" }, "error": { - "certificate_error": "Le certificat n'a pas pu \u00eatre valid\u00e9", - "certificate_fetch_failed": "Impossible de r\u00e9cup\u00e9rer le certificat de cette combinaison h\u00f4te / port", "connection_refused": "Connexion refus\u00e9e lors de la connexion \u00e0 l'h\u00f4te", "connection_timeout": "D\u00e9lai d'attente lors de la connexion \u00e0 cet h\u00f4te", - "host_port_exists": "Cette combinaison h\u00f4te / port est d\u00e9j\u00e0 configur\u00e9e", - "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu", - "wrong_host": "Le certificat ne correspond pas au nom d'h\u00f4te" + "resolve_failed": "Cet h\u00f4te ne peut pas \u00eatre r\u00e9solu" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/it.json b/homeassistant/components/cert_expiry/.translations/it.json index e7a2801423d..e88afa7caef 100644 --- a/homeassistant/components/cert_expiry/.translations/it.json +++ b/homeassistant/components/cert_expiry/.translations/it.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", - "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", "import_failed": "Importazione dalla configurazione non riuscita" }, "error": { - "certificate_error": "Il certificato non pu\u00f2 essere convalidato", - "certificate_fetch_failed": "Non \u00e8 possibile recuperare il certificato da questa combinazione di host e porta", "connection_refused": "Connessione rifiutata durante la connessione all'host", "connection_timeout": "Tempo scaduto collegandosi a questo host", - "host_port_exists": "Questa combinazione di host e porta \u00e8 gi\u00e0 configurata", - "resolve_failed": "Questo host non pu\u00f2 essere risolto", - "wrong_host": "Il certificato non corrisponde al nome host" + "resolve_failed": "Questo host non pu\u00f2 essere risolto" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/ko.json b/homeassistant/components/cert_expiry/.translations/ko.json index 060bf6e26bd..962f9ebe42c 100644 --- a/homeassistant/components/cert_expiry/.translations/ko.json +++ b/homeassistant/components/cert_expiry/.translations/ko.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "import_failed": "\uad6c\uc131\uc5d0\uc11c \uac00\uc838\uc624\uae30 \uc2e4\ud328" }, "error": { - "certificate_error": "\uc778\uc99d\uc11c\ub97c \ud655\uc778\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "certificate_fetch_failed": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uc5d0\uc11c \uc778\uc99d\uc11c\ub97c \uac00\uc838 \uc62c \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "connection_refused": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\uc774 \uac70\ubd80\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "connection_timeout": "\ud638\uc2a4\ud2b8 \uc5f0\uacb0 \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", - "host_port_exists": "\ud638\uc2a4\ud2b8\uc640 \ud3ec\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", - "wrong_host": "\uc778\uc99d\uc11c\uac00 \ud638\uc2a4\ud2b8 \uc774\ub984\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" + "resolve_failed": "\ud638\uc2a4\ud2b8\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/lb.json b/homeassistant/components/cert_expiry/.translations/lb.json index 14d12967a38..55ac013f96a 100644 --- a/homeassistant/components/cert_expiry/.translations/lb.json +++ b/homeassistant/components/cert_expiry/.translations/lb.json @@ -1,15 +1,13 @@ { "config": { "abort": { - "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert" + "already_configured": "D\u00ebs Kombinatioun vun Host an Port sinn scho konfigur\u00e9iert", + "import_failed": "Import vun der Konfiguratioun feelgeschloen" }, "error": { - "certificate_error": "Zertifikat konnt net valid\u00e9iert ginn", - "certificate_fetch_failed": "Kann keen Zertifikat vun d\u00ebsen Host a Port recuper\u00e9ieren", + "connection_refused": "Verbindung refus\u00e9iert beim verbannen mam Host", "connection_timeout": "Z\u00e4it Iwwerschreidung beim verbannen.", - "host_port_exists": "D\u00ebsen Host an Port sinn scho konfigur\u00e9iert", - "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn", - "wrong_host": "Zertifikat entspr\u00e9cht net den Numm vum Apparat" + "resolve_failed": "D\u00ebsen Host kann net opgel\u00e9ist ginn" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/nl.json b/homeassistant/components/cert_expiry/.translations/nl.json index 0544c8c02c1..7c9fbe67565 100644 --- a/homeassistant/components/cert_expiry/.translations/nl.json +++ b/homeassistant/components/cert_expiry/.translations/nl.json @@ -1,15 +1,8 @@ { "config": { - "abort": { - "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd" - }, "error": { - "certificate_error": "Certificaat kon niet worden gevalideerd", - "certificate_fetch_failed": "Kan certificaat niet ophalen van deze combinatie van host en poort", "connection_timeout": "Time-out bij verbinding maken met deze host", - "host_port_exists": "Deze combinatie van host en poort is al geconfigureerd", - "resolve_failed": "Deze host kon niet gevonden worden", - "wrong_host": "Certificaat komt niet overeen met hostnaam" + "resolve_failed": "Deze host kon niet gevonden worden" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/no.json b/homeassistant/components/cert_expiry/.translations/no.json index e5faab74995..a798ead27b6 100644 --- a/homeassistant/components/cert_expiry/.translations/no.json +++ b/homeassistant/components/cert_expiry/.translations/no.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Denne verts- og portkombinasjonen er allerede konfigurert", - "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", "import_failed": "Import fra config mislyktes" }, "error": { - "certificate_error": "Sertifikatet kunne ikke valideres", - "certificate_fetch_failed": "Kan ikke hente sertifikat fra denne verts- og portkombinasjonen", "connection_refused": "Tilkoblingen ble nektet da den koblet til verten", "connection_timeout": "Tidsavbrudd n\u00e5r du kobler til denne verten", - "host_port_exists": "Denne verts- og portkombinasjonen er allerede konfigurert", - "resolve_failed": "Denne verten kan ikke l\u00f8ses", - "wrong_host": "Sertifikatet samsvarer ikke med vertsnavn" + "resolve_failed": "Denne verten kan ikke l\u00f8ses" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/pl.json b/homeassistant/components/cert_expiry/.translations/pl.json index a4806ff13aa..510b75658a2 100644 --- a/homeassistant/components/cert_expiry/.translations/pl.json +++ b/homeassistant/components/cert_expiry/.translations/pl.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Ta kombinacja hosta i portu jest ju\u017c skonfigurowana", - "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.", "import_failed": "Import z konfiguracji nie powi\u00f3d\u0142 si\u0119" }, "error": { - "certificate_error": "Nie mo\u017cna zweryfikowa\u0107 certyfikatu", - "certificate_fetch_failed": "Nie mo\u017cna pobra\u0107 certyfikatu z tej kombinacji hosta i portu", "connection_refused": "Po\u0142\u0105czenie odrzucone podczas \u0142\u0105czenia z hostem", "connection_timeout": "Przekroczono limit czasu po\u0142\u0105czenia z hostem.", - "host_port_exists": "Ten host z tym portem jest ju\u017c skonfigurowany.", - "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107", - "wrong_host": "Certyfikat nie pasuje do nazwy hosta" + "resolve_failed": "Tego hosta nie mo\u017cna rozwi\u0105za\u0107" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/pt-BR.json b/homeassistant/components/cert_expiry/.translations/pt-BR.json index 06534314e00..0c0e272e23b 100644 --- a/homeassistant/components/cert_expiry/.translations/pt-BR.json +++ b/homeassistant/components/cert_expiry/.translations/pt-BR.json @@ -1,12 +1,7 @@ { "config": { - "abort": { - "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada" - }, "error": { - "certificate_fetch_failed": "N\u00e3o \u00e9 poss\u00edvel buscar o certificado dessa combina\u00e7\u00e3o de host e porta", "connection_timeout": "Tempo limite ao conectar-se a este host", - "host_port_exists": "Essa combina\u00e7\u00e3o de host e porta j\u00e1 est\u00e1 configurada", "resolve_failed": "Este host n\u00e3o pode ser resolvido" }, "step": { diff --git a/homeassistant/components/cert_expiry/.translations/ru.json b/homeassistant/components/cert_expiry/.translations/ru.json index 04a41704500..39c78acc4c0 100644 --- a/homeassistant/components/cert_expiry/.translations/ru.json +++ b/homeassistant/components/cert_expiry/.translations/ru.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", "import_failed": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0438\u043c\u043f\u043e\u0440\u0442\u0430 \u0438\u0437 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438." }, "error": { - "certificate_error": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u0440\u043e\u0432\u0435\u0440\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442.", - "certificate_fetch_failed": "\u041d\u0435 \u0443\u0434\u0430\u0435\u0442\u0441\u044f \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u0441 \u044d\u0442\u043e\u0439 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u0438 \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430.", "connection_refused": "\u041f\u0440\u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0438 \u043a \u0445\u043e\u0441\u0442\u0443 \u0431\u044b\u043b\u043e \u043e\u0442\u043a\u0430\u0437\u0430\u043d\u043e \u0432 \u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u0438.", "connection_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u0445\u043e\u0441\u0442\u0443.", - "host_port_exists": "\u042d\u0442\u0430 \u043a\u043e\u043c\u0431\u0438\u043d\u0430\u0446\u0438\u044f \u0445\u043e\u0441\u0442\u0430 \u0438 \u043f\u043e\u0440\u0442\u0430 \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442.", - "wrong_host": "\u0421\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0434\u043e\u043c\u0435\u043d\u043d\u043e\u043c\u0443 \u0438\u043c\u0435\u043d\u0438." + "resolve_failed": "\u041d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u043f\u0440\u0435\u043e\u0431\u0440\u0430\u0437\u043e\u0432\u0430\u0442\u044c \u0445\u043e\u0441\u0442." }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/sl.json b/homeassistant/components/cert_expiry/.translations/sl.json index 284b0b960ba..605eb0b8182 100644 --- a/homeassistant/components/cert_expiry/.translations/sl.json +++ b/homeassistant/components/cert_expiry/.translations/sl.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", - "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", "import_failed": "Uvoz iz konfiguracije ni uspel" }, "error": { - "certificate_error": "Certifikata ni bilo mogo\u010de preveriti", - "certificate_fetch_failed": "Iz te kombinacije gostitelja in vrat ni mogo\u010de pridobiti potrdila", "connection_refused": "Povezava zavrnjena, ko ste se povezali z gostiteljem", "connection_timeout": "\u010casovna omejitev za povezavo s tem gostiteljem je potekla", - "host_port_exists": "Ta kombinacija gostitelja in vrat je \u017ee konfigurirana", - "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti", - "wrong_host": "Potrdilo se ne ujema z imenom gostitelja" + "resolve_failed": "Tega gostitelja ni mogo\u010de razre\u0161iti" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/sv.json b/homeassistant/components/cert_expiry/.translations/sv.json index bdccf51b2cd..2655bb40f08 100644 --- a/homeassistant/components/cert_expiry/.translations/sv.json +++ b/homeassistant/components/cert_expiry/.translations/sv.json @@ -1,15 +1,8 @@ { "config": { - "abort": { - "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad" - }, "error": { - "certificate_error": "Certifikatet kunde inte valideras", - "certificate_fetch_failed": "Kan inte h\u00e4mta certifikat fr\u00e5n denna v\u00e4rd- och portkombination", "connection_timeout": "Timeout vid anslutning till den h\u00e4r v\u00e4rden", - "host_port_exists": "Denna v\u00e4rd- och portkombination \u00e4r redan konfigurerad", - "resolve_failed": "Denna v\u00e4rd kan inte resolveras", - "wrong_host": "Certifikatet matchar inte v\u00e4rdnamnet" + "resolve_failed": "Denna v\u00e4rd kan inte resolveras" }, "step": { "user": { diff --git a/homeassistant/components/cert_expiry/.translations/zh-Hant.json b/homeassistant/components/cert_expiry/.translations/zh-Hant.json index 833e2370dde..f08e3e277e9 100644 --- a/homeassistant/components/cert_expiry/.translations/zh-Hant.json +++ b/homeassistant/components/cert_expiry/.translations/zh-Hant.json @@ -2,17 +2,12 @@ "config": { "abort": { "already_configured": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "import_failed": "\u532f\u5165\u8a2d\u5b9a\u5931\u6557" }, "error": { - "certificate_error": "\u8a8d\u8b49\u7121\u6cd5\u78ba\u8a8d", - "certificate_fetch_failed": "\u7121\u6cd5\u81ea\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u7372\u5f97\u8a8d\u8b49", "connection_refused": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u6642\u906d\u62d2\u7d55", "connection_timeout": "\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef\u903e\u6642", - "host_port_exists": "\u6b64\u4e3b\u6a5f\u7aef\u8207\u901a\u8a0a\u57e0\u7d44\u5408\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", - "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790", - "wrong_host": "\u8a8d\u8b49\u8207\u4e3b\u6a5f\u540d\u7a31\u4e0d\u7b26\u5408" + "resolve_failed": "\u4e3b\u6a5f\u7aef\u7121\u6cd5\u89e3\u6790" }, "step": { "user": { diff --git a/homeassistant/components/coronavirus/.translations/hu.json b/homeassistant/components/coronavirus/.translations/hu.json new file mode 100644 index 00000000000..171aedc801d --- /dev/null +++ b/homeassistant/components/coronavirus/.translations/hu.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "Ez az orsz\u00e1g m\u00e1r konfigur\u00e1lva van." + }, + "step": { + "user": { + "data": { + "country": "Orsz\u00e1g" + }, + "title": "V\u00e1lassz egy orsz\u00e1got a megfigyel\u00e9shez" + } + }, + "title": "Koronav\u00edrus" + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/.translations/hu.json b/homeassistant/components/cover/.translations/hu.json index d460c53109d..5e91736a263 100644 --- a/homeassistant/components/cover/.translations/hu.json +++ b/homeassistant/components/cover/.translations/hu.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "close": "{entity_name} z\u00e1r\u00e1sa", + "close_tilt": "{entity_name} d\u00f6nt\u00e9s z\u00e1r\u00e1sa", + "open": "{entity_name} nyit\u00e1sa", + "open_tilt": "{entity_name} d\u00f6nt\u00e9s nyit\u00e1sa", + "set_position": "{entity_name} poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa", + "set_tilt_position": "{entity_name} d\u00f6nt\u00e9si poz\u00edci\u00f3j\u00e1nak be\u00e1ll\u00edt\u00e1sa" + }, "condition_type": { "is_closed": "{entity_name} z\u00e1rva van", "is_closing": "{entity_name} z\u00e1r\u00f3dik", diff --git a/homeassistant/components/cover/.translations/lb.json b/homeassistant/components/cover/.translations/lb.json index 41c29adf91d..4cbbf348872 100644 --- a/homeassistant/components/cover/.translations/lb.json +++ b/homeassistant/components/cover/.translations/lb.json @@ -1,7 +1,10 @@ { "device_automation": { "action_type": { + "close": "{entity_name} zoumaachen", + "close_tilt": "{entity_name} Kipp zoumaachen", "open": "{entity_name} opmaachen", + "open_tilt": "{entity_name} op Kipp stelle", "set_position": "{entity_name} positioun programm\u00e9ieren", "set_tilt_position": "{entity_name} kipp positioun programm\u00e9ieren" }, @@ -10,8 +13,8 @@ "is_closing": "{entity_name} g\u00ebtt zougemaach", "is_open": "{entity_name} ass op", "is_opening": "{entity_name} g\u00ebtt opgemaach", - "is_position": "{entity_name} positioun ass", - "is_tilt_position": "{entity_name} kipp positioun ass" + "is_position": "Aktuell {entity_name} positioun ass", + "is_tilt_position": "Aktuell {entity_name} kipp positioun ass" }, "trigger_type": { "closed": "{entity_name} gouf zougemaach", diff --git a/homeassistant/components/deconz/.translations/bg.json b/homeassistant/components/deconz/.translations/bg.json index fb75fc81f5f..3bcbb592301 100644 --- a/homeassistant/components/deconz/.translations/bg.json +++ b/homeassistant/components/deconz/.translations/bg.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee \u0448\u043b\u044e\u0437 ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u043d\u0438 \u0441\u0435\u043d\u0437\u043e\u0440\u0438", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0438 \u043e\u0442 deCONZ" - }, "description": "\u0418\u0441\u043a\u0430\u0442\u0435 \u043b\u0438 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Home Assistant \u0434\u0430 \u0441\u0435 \u0441\u0432\u044a\u0440\u0437\u0432\u0430 \u0441 deCONZ \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f, \u043f\u0440\u0435\u0434\u043e\u0441\u0442\u0430\u0432\u0435\u043d \u043e\u0442 \u0434\u043e\u0431\u0430\u0432\u043a\u0430\u0442\u0430 \u0437\u0430 hass.io {addon}?", "title": "deCONZ Zigbee \u0431\u0430\u0437\u043e\u0432\u0430 \u0441\u0442\u0430\u043d\u0446\u0438\u044f \u0447\u0440\u0435\u0437 Hass.io \u0434\u043e\u0431\u0430\u0432\u043a\u0430" }, @@ -31,13 +27,6 @@ "link": { "description": "\u041e\u0442\u043a\u043b\u044e\u0447\u0438 deCONZ \u0448\u043b\u044e\u0437\u0430 \u0437\u0430 \u0434\u0430 \u0441\u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430 \u0441 Home Assistant.\n\n1. \u041e\u0442\u0438\u0434\u0435\u0442\u0435 \u043d\u0430 deCONZ Settings -> Gateway -> Advanced\n2. \u041d\u0430\u0442\u0438\u0441\u043d\u0435\u0442\u0435 \u0431\u0443\u0442\u043e\u043d\u0430 \"Authenticate app\"", "title": "\u0412\u0440\u044a\u0437\u043a\u0430 \u0441 deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u043d\u0438 \u0441\u0435\u043d\u0437\u043e\u0440\u0438", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u0433\u0440\u0443\u043f\u0438 \u043e\u0442 deCONZ" - }, - "title": "\u0414\u043e\u043f\u044a\u043b\u043d\u0438\u0442\u0435\u043b\u043d\u0438 \u043e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 deCONZ" } }, "title": "deCONZ Zigbee \u0448\u043b\u044e\u0437" @@ -90,13 +79,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ \u0441\u0432\u0435\u0442\u043b\u0438\u043d\u043d\u0438 \u0433\u0440\u0443\u043f\u0438" - }, - "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0439\u0442\u0435 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0442\u0430 \u043d\u0430 \u0442\u0438\u043f\u043e\u0432\u0435\u0442\u0435 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 deCONZ CLIP \u0441\u0435\u043d\u0437\u043e\u0440\u0438", diff --git a/homeassistant/components/deconz/.translations/ca.json b/homeassistant/components/deconz/.translations/ca.json index e690d597dce..ee386bece55 100644 --- a/homeassistant/components/deconz/.translations/ca.json +++ b/homeassistant/components/deconz/.translations/ca.json @@ -14,10 +14,6 @@ "flow_title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", - "allow_deconz_groups": "Permet la importaci\u00f3 de grups deCONZ" - }, "description": "Vols configurar Home Assistant perqu\u00e8 es connecti amb la passarel\u00b7la deCONZ proporcionada pel complement de Hass.io: {addon}?", "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee (complement de Hass.io)" }, @@ -31,13 +27,6 @@ "link": { "description": "Desbloqueja la teva passarel\u00b7la d'enlla\u00e7 deCONZ per a registrar-te amb Home Assistant.\n\n1. V\u00e9s a la configuraci\u00f3 del sistema deCONZ -> Passarel\u00b7la -> Avan\u00e7at\n2. Prem el bot\u00f3 \"Autenticar applicaci\u00f3\"", "title": "Vincular amb deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Permet la importaci\u00f3 de sensors virtuals", - "allow_deconz_groups": "Permetre la importaci\u00f3 de grups deCONZ" - }, - "title": "Opcions de configuraci\u00f3 addicionals de deCONZ" } }, "title": "Passarel\u00b7la d'enlla\u00e7 deCONZ Zigbee" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Permet sensors deCONZ CLIP", - "allow_deconz_groups": "Permet grups de llums deCONZ" - }, - "description": "Configura la visibilitat dels tipus dels dispositius deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Permet sensors deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/cs.json b/homeassistant/components/deconz/.translations/cs.json index 954d1c8eb6e..544ab0ff2ed 100644 --- a/homeassistant/components/deconz/.translations/cs.json +++ b/homeassistant/components/deconz/.translations/cs.json @@ -24,13 +24,6 @@ "link": { "description": "Odemkn\u011bte br\u00e1nu deCONZ, pro registraci v Home Assistant. \n\n 1. P\u0159ejd\u011bte do nastaven\u00ed syst\u00e9mu deCONZ \n 2. Stiskn\u011bte tla\u010d\u00edtko \"Unlock Gateway\"", "title": "Propojit s deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Povolit import virtu\u00e1ln\u00edch \u010didel", - "allow_deconz_groups": "Povolit import skupin deCONZ" - }, - "title": "Dal\u0161\u00ed mo\u017enosti konfigurace pro deCONZ" } }, "title": "Br\u00e1na deCONZ Zigbee" diff --git a/homeassistant/components/deconz/.translations/da.json b/homeassistant/components/deconz/.translations/da.json index d1af7e1f4ba..91dd0ea9a54 100644 --- a/homeassistant/components/deconz/.translations/da.json +++ b/homeassistant/components/deconz/.translations/da.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad import af deCONZ-grupper" - }, "description": "Vil du konfigurere Home Assistant til at oprette forbindelse til deCONZ-gateway'en leveret af Hass.io-tilf\u00f8jelsen {addon}?", "title": "deCONZ Zigbee-gateway via Hass.io-tilf\u00f8jelse" }, @@ -31,13 +27,6 @@ "link": { "description": "L\u00e5s din deCONZ-gateway op for at registrere dig med Home Assistant. \n\n 1. G\u00e5 til deCONZ settings -> Gateway -> Advanced\n 2. Tryk p\u00e5 knappen \"Authenticate app\"", "title": "Forbind med deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Tillad import af virtuelle sensorer", - "allow_deconz_groups": "Tillad import af deCONZ-grupper" - }, - "title": "Ekstra konfigurationsindstillinger for deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", - "allow_deconz_groups": "Tillad deCONZ-lysgrupper" - }, - "description": "Konfigurer synligheden af deCONZ-enhedstyper" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Tillad deCONZ CLIP-sensorer", diff --git a/homeassistant/components/deconz/.translations/de.json b/homeassistant/components/deconz/.translations/de.json index c3ad3cd24c8..1b2daecbc4e 100644 --- a/homeassistant/components/deconz/.translations/de.json +++ b/homeassistant/components/deconz/.translations/de.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee Gateway", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Import virtueller Sensoren zulassen", - "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" - }, "description": "M\u00f6chtest du Home Assistant so konfigurieren, dass er eine Verbindung mit dem deCONZ Gateway herstellt, der vom Add-on hass.io {addon} bereitgestellt wird?", "title": "deCONZ Zigbee Gateway \u00fcber das Hass.io Add-on" }, @@ -31,13 +27,6 @@ "link": { "description": "Entsperre dein deCONZ-Gateway, um es bei Home Assistant zu registrieren. \n\n 1. Gehe in die deCONZ-Systemeinstellungen \n 2. Dr\u00fccke die Taste \"Gateway entsperren\"", "title": "Mit deCONZ verbinden" - }, - "options": { - "data": { - "allow_clip_sensor": "Import virtueller Sensoren zulassen", - "allow_deconz_groups": "Import von deCONZ-Gruppen zulassen" - }, - "title": "Weitere Konfigurationsoptionen f\u00fcr deCONZ" } }, "title": "deCONZ Zigbee Gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", - "allow_deconz_groups": "deCONZ-Lichtgruppen zulassen" - }, - "description": "Konfigurieren der Sichtbarkeit von deCONZ-Ger\u00e4tetypen" - }, "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ CLIP-Sensoren zulassen", diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index 756636ad90a..2c9562359f5 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - }, "description": "Do you want to configure Home Assistant to connect to the deCONZ gateway provided by the Hass.io add-on {addon}?", "title": "deCONZ Zigbee gateway via Hass.io add-on" }, @@ -31,13 +27,6 @@ "link": { "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ Settings -> Gateway -> Advanced\n2. Press \"Authenticate app\" button", "title": "Link with deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Allow importing virtual sensors", - "allow_deconz_groups": "Allow importing deCONZ groups" - }, - "title": "Extra configuration options for deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Allow deCONZ CLIP sensors", - "allow_deconz_groups": "Allow deCONZ light groups" - }, - "description": "Configure visibility of deCONZ device types" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Allow deCONZ CLIP sensors", diff --git a/homeassistant/components/deconz/.translations/es-419.json b/homeassistant/components/deconz/.translations/es-419.json index 448b654c86e..ea65ffbab33 100644 --- a/homeassistant/components/deconz/.translations/es-419.json +++ b/homeassistant/components/deconz/.translations/es-419.json @@ -13,10 +13,6 @@ }, "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", - "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - }, "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", "title": "deCONZ Zigbee gateway a trav\u00e9s del complemento Hass.io" }, @@ -30,13 +26,6 @@ "link": { "description": "Desbloquee su puerta de enlace deCONZ para registrarse con Home Assistant. \n\n 1. Vaya a Configuraci\u00f3n deCONZ - > Gateway - > Avanzado \n 2. Presione el bot\u00f3n \"Autenticar aplicaci\u00f3n\"", "title": "Enlazar con deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Permitir la importaci\u00f3n de sensores virtuales", - "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - }, - "title": "Opciones de configuraci\u00f3n adicionales para deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -59,16 +48,5 @@ "remote_button_rotated": "Bot\u00f3n girado \"{subtype}\"", "remote_gyro_activated": "Dispositivo agitado" } - }, - "options": { - "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Permitir sensores deCONZ CLIP", - "allow_deconz_groups": "Permitir grupos de luz deCONZ" - }, - "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/es.json b/homeassistant/components/deconz/.translations/es.json index 047be1c7933..517170fe225 100644 --- a/homeassistant/components/deconz/.translations/es.json +++ b/homeassistant/components/deconz/.translations/es.json @@ -14,10 +14,6 @@ "flow_title": "pasarela deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Permitir importar sensores virtuales", - "allow_deconz_groups": "Permite importar grupos de deCONZ" - }, "description": "\u00bfQuieres configurar Home Assistant para que se conecte al gateway de deCONZ proporcionado por el add-on {addon} de hass.io?", "title": "Add-on deCONZ Zigbee v\u00eda Hass.io" }, @@ -31,13 +27,6 @@ "link": { "description": "Desbloquea tu gateway de deCONZ para registrarte con Home Assistant.\n\n1. Dir\u00edgete a deCONZ Settings -> Gateway -> Advanced\n2. Pulsa el bot\u00f3n \"Authenticate app\"", "title": "Enlazar con deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Permitir importar sensores virtuales", - "allow_deconz_groups": "Permitir la importaci\u00f3n de grupos deCONZ" - }, - "title": "Opciones de configuraci\u00f3n adicionales para deCONZ" } }, "title": "Pasarela Zigbee deCONZ" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Permitir sensores deCONZ CLIP", - "allow_deconz_groups": "Permitir grupos de luz deCONZ" - }, - "description": "Configurar la visibilidad de los tipos de dispositivos deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Permitir sensores deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/fr.json b/homeassistant/components/deconz/.translations/fr.json index 2213dcf2d49..0c2ecf9edb8 100644 --- a/homeassistant/components/deconz/.translations/fr.json +++ b/homeassistant/components/deconz/.translations/fr.json @@ -14,10 +14,6 @@ "flow_title": "Passerelle deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels", - "allow_deconz_groups": "Autoriser l'importation des groupes deCONZ" - }, "description": "Voulez-vous configurer Home Assistant pour qu'il se connecte \u00e0 la passerelle deCONZ fournie par l'add-on hass.io {addon} ?", "title": "Passerelle deCONZ Zigbee via l'add-on Hass.io" }, @@ -31,13 +27,6 @@ "link": { "description": "D\u00e9verrouillez votre passerelle deCONZ pour vous enregistrer avec Home Assistant. \n\n 1. Acc\u00e9dez aux param\u00e8tres avanc\u00e9s du syst\u00e8me deCONZ \n 2. Cliquez sur \"D\u00e9verrouiller la passerelle\"", "title": "Lien vers deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Autoriser l'importation de capteurs virtuels", - "allow_deconz_groups": "Autoriser l'importation des groupes deCONZ" - }, - "title": "Options de configuration suppl\u00e9mentaires pour deCONZ" } }, "title": "Passerelle deCONZ Zigbee" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", - "allow_deconz_groups": "Autoriser les groupes de lumi\u00e8res deCONZ" - }, - "description": "Configurer la visibilit\u00e9 des appareils de type deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Autoriser les capteurs deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/he.json b/homeassistant/components/deconz/.translations/he.json index 89a2d69950e..da7878e94af 100644 --- a/homeassistant/components/deconz/.translations/he.json +++ b/homeassistant/components/deconz/.translations/he.json @@ -19,13 +19,6 @@ "link": { "description": "\u05d1\u05d8\u05dc \u05d0\u05ea \u05e0\u05e2\u05d9\u05dc\u05ea \u05d4\u05de\u05e9\u05e8 deCONZ \u05e9\u05dc\u05da \u05db\u05d3\u05d9 \u05dc\u05d4\u05ea\u05d7\u05d1\u05e8 \u05e2\u05dd Home Assistant.\n\n 1. \u05e2\u05d1\u05d5\u05e8 \u05d0\u05dc \u05d4\u05d2\u05d3\u05e8\u05d5\u05ea \u05de\u05e2\u05e8\u05db\u05ea deCONZ \n .2 \u05dc\u05d7\u05e5 \u05e2\u05dc \"Unlock Gateway\"", "title": "\u05e7\u05e9\u05e8 \u05e2\u05dd deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "\u05d0\u05e4\u05e9\u05e8 \u05dc\u05d9\u05d9\u05d1\u05d0 \u05d7\u05d9\u05d9\u05e9\u05e0\u05d9\u05dd \u05d5\u05d9\u05e8\u05d8\u05d5\u05d0\u05dc\u05d9\u05d9\u05dd", - "allow_deconz_groups": "\u05d0\u05e4\u05e9\u05e8 \u05dc\u05d9\u05d9\u05d1\u05d0 \u05e7\u05d1\u05d5\u05e6\u05d5\u05ea deCONZ" - }, - "title": "\u05d0\u05e4\u05e9\u05e8\u05d5\u05d9\u05d5\u05ea \u05ea\u05e6\u05d5\u05e8\u05d4 \u05e0\u05d5\u05e1\u05e4\u05d5\u05ea \u05e2\u05d1\u05d5\u05e8 deCONZ" } }, "title": "\u05de\u05d2\u05e9\u05e8 deCONZ Zigbee" diff --git a/homeassistant/components/deconz/.translations/hr.json b/homeassistant/components/deconz/.translations/hr.json index 2f2eb6df214..1700ec050bf 100644 --- a/homeassistant/components/deconz/.translations/hr.json +++ b/homeassistant/components/deconz/.translations/hr.json @@ -6,11 +6,6 @@ "host": "Host", "port": "Port" } - }, - "options": { - "data": { - "allow_clip_sensor": "Dopusti uvoz virtualnih senzora" - } } } } diff --git a/homeassistant/components/deconz/.translations/hu.json b/homeassistant/components/deconz/.translations/hu.json index c5bf9718127..31148c80e30 100644 --- a/homeassistant/components/deconz/.translations/hu.json +++ b/homeassistant/components/deconz/.translations/hu.json @@ -14,9 +14,6 @@ "flow_title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Virtu\u00e1lis \u00e9rz\u00e9kel\u0151k import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" - }, "title": "deCONZ Zigbee \u00e1tj\u00e1r\u00f3 a Hass.io kieg\u00e9sz\u00edt\u0151 seg\u00edts\u00e9g\u00e9vel" }, "init": { @@ -29,13 +26,6 @@ "link": { "description": "Oldja fel a deCONZ \u00e1tj\u00e1r\u00f3t a Home Assistant-ban val\u00f3 regisztr\u00e1l\u00e1shoz.\n\n1. Menjen a deCONZ rendszer be\u00e1ll\u00edt\u00e1sokhoz\n2. Nyomja meg az \"\u00c1tj\u00e1r\u00f3 felold\u00e1sa\" gombot", "title": "Kapcsol\u00f3d\u00e1s a deCONZ-hoz" - }, - "options": { - "data": { - "allow_clip_sensor": "Virtu\u00e1lis szenzorok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se", - "allow_deconz_groups": "deCONZ csoportok import\u00e1l\u00e1s\u00e1nak enged\u00e9lyez\u00e9se" - }, - "title": "Extra be\u00e1ll\u00edt\u00e1si lehet\u0151s\u00e9gek a deCONZhoz" } }, "title": "deCONZ Zigbee gateway" @@ -94,13 +84,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", - "allow_deconz_groups": "DeCONZ f\u00e9nycsoportok enged\u00e9lyez\u00e9se" - }, - "description": "A deCONZ eszk\u00f6zt\u00edpusok l\u00e1that\u00f3s\u00e1g\u00e1nak konfigur\u00e1l\u00e1sa" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Enged\u00e9lyezze a deCONZ CLIP \u00e9rz\u00e9kel\u0151ket", diff --git a/homeassistant/components/deconz/.translations/id.json b/homeassistant/components/deconz/.translations/id.json index 7d0b3163a40..72aaa84e70d 100644 --- a/homeassistant/components/deconz/.translations/id.json +++ b/homeassistant/components/deconz/.translations/id.json @@ -19,13 +19,6 @@ "link": { "description": "Buka gerbang deCONZ Anda untuk mendaftar dengan Home Assistant. \n\n 1. Pergi ke pengaturan sistem deCONZ \n 2. Tekan tombol \"Buka Kunci Gateway\"", "title": "Tautan dengan deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Izinkan mengimpor sensor virtual", - "allow_deconz_groups": "Izinkan mengimpor grup deCONZ" - }, - "title": "Opsi konfigurasi tambahan untuk deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/it.json b/homeassistant/components/deconz/.translations/it.json index f6223cec6c1..e12668f082c 100644 --- a/homeassistant/components/deconz/.translations/it.json +++ b/homeassistant/components/deconz/.translations/it.json @@ -14,10 +14,6 @@ "flow_title": "Gateway Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Consenti l'importazione di sensori virtuali", - "allow_deconz_groups": "Consenti l'importazione di gruppi deCONZ" - }, "description": "Vuoi configurare Home Assistant per connettersi al gateway deCONZ fornito dal componente aggiuntivo di Hass.io: {addon}?", "title": "Gateway Pigmee deCONZ tramite il componente aggiuntivo di Hass.io" }, @@ -31,13 +27,6 @@ "link": { "description": "Sblocca il tuo gateway deCONZ per registrarti con Home Assistant.\n\n1. Vai a Impostazioni deCONZ -> Gateway -> Avanzate\n2. Premere il pulsante \"Autentica app\"", "title": "Collega con deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Consenti l'importazione di sensori virtuali", - "allow_deconz_groups": "Consenti l'importazione di gruppi deCONZ" - }, - "title": "Opzioni di configurazione extra per deCONZ" } }, "title": "Gateway Zigbee deCONZ" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Consentire sensori CLIP deCONZ", - "allow_deconz_groups": "Consentire gruppi luce deCONZ" - }, - "description": "Configurare la visibilit\u00e0 dei tipi di dispositivi deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Consentire sensori CLIP deCONZ", diff --git a/homeassistant/components/deconz/.translations/ko.json b/homeassistant/components/deconz/.translations/ko.json index 1b72545bc09..00b9c1f437a 100644 --- a/homeassistant/components/deconz/.translations/ko.json +++ b/homeassistant/components/deconz/.translations/ko.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774 ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", - "allow_deconz_groups": "deCONZ \uadf8\ub8f9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" - }, "description": "Hass.io {addon} \uc560\ub4dc\uc628\uc5d0\uc11c \uc81c\uacf5\ub41c deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\uc5d0 \uc5f0\uacb0\ud558\ub3c4\ub85d Home Assistant \ub97c \uad6c\uc131\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", "title": "Hass.io \uc560\ub4dc\uc628\uc758 deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" }, @@ -31,13 +27,6 @@ "link": { "description": "deCONZ \uac8c\uc774\ud2b8\uc6e8\uc774\ub97c \uc5b8\ub77d\ud558\uc5ec Home Assistant \uc5d0 \uc5f0\uacb0\ud558\uae30.\n\n1. deCONZ \uc2dc\uc2a4\ud15c \uc124\uc815\uc73c\ub85c \uc774\ub3d9\ud558\uc138\uc694\n2. \"Authenticate app\" \ubc84\ud2bc\uc744 \ub20c\ub7ec\uc8fc\uc138\uc694", "title": "deCONZ\uc640 \uc5f0\uacb0" - }, - "options": { - "data": { - "allow_clip_sensor": "\uac00\uc0c1 \uc13c\uc11c \uac00\uc838\uc624\uae30 \ud5c8\uc6a9", - "allow_deconz_groups": "deCONZ \uadf8\ub8f9 \uac00\uc838\uc624\uae30 \ud5c8\uc6a9" - }, - "title": "deCONZ \ucd94\uac00 \uad6c\uc131 \uc635\uc158" } }, "title": "deCONZ Zigbee \uac8c\uc774\ud2b8\uc6e8\uc774" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", - "allow_deconz_groups": "deCONZ \ub77c\uc774\ud2b8 \uadf8\ub8f9 \ud5c8\uc6a9" - }, - "description": "deCONZ \uae30\uae30 \uc720\ud615\uc758 \ud45c\uc2dc \uc5ec\ubd80 \uad6c\uc131" - }, "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ CLIP \uc13c\uc11c \ud5c8\uc6a9", diff --git a/homeassistant/components/deconz/.translations/lb.json b/homeassistant/components/deconz/.translations/lb.json index 42fd840524f..61479cb78e2 100644 --- a/homeassistant/components/deconz/.translations/lb.json +++ b/homeassistant/components/deconz/.translations/lb.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren", - "allow_deconz_groups": "Erlaabt den Import vun deCONZ Gruppen" - }, "description": "W\u00ebllt dir Home Assistant konfigur\u00e9iere fir sech mat der deCONZ gateway ze verbannen d\u00e9i vum hass.io add-on {addon} bereet gestallt g\u00ebtt?", "title": "deCONZ Zigbee gateway via Hass.io add-on" }, @@ -31,13 +27,6 @@ "link": { "description": "Entsperrt \u00e4r deCONZ gateway fir se mat Home Assistant ze registr\u00e9ieren.\n\n1. Gidd op deCONZ System Astellungen\n2. Dr\u00e9ckt \"Unlock\" Gateway Kn\u00e4ppchen", "title": "Link mat deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Erlaabt den Import vun virtuellen Sensoren", - "allow_deconz_groups": "Erlaabt den Import vun deCONZ Gruppen" - }, - "title": "Extra Konfiguratiouns Optiounen fir deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", - "allow_deconz_groups": "deCONZ Luucht Gruppen erlaben" - }, - "description": "Visibilit\u00e9it vun deCONZ Apparater konfigur\u00e9ieren" - }, "deconz_devices": { "data": { "allow_clip_sensor": "deCONZ Clip Sensoren erlaben", diff --git a/homeassistant/components/deconz/.translations/nl.json b/homeassistant/components/deconz/.translations/nl.json index 585c09c5339..611d38ba950 100644 --- a/homeassistant/components/deconz/.translations/nl.json +++ b/homeassistant/components/deconz/.translations/nl.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee gateway ( {host} )", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Sta het importeren van virtuele sensoren toe", - "allow_deconz_groups": "Sta de import van deCONZ-groepen toe" - }, "description": "Wilt u de Home Assistant configureren om verbinding te maken met de deCONZ gateway van de hass.io add-on {addon}?", "title": "deCONZ Zigbee Gateway via Hass.io add-on" }, @@ -31,13 +27,6 @@ "link": { "description": "Ontgrendel je deCONZ gateway om te registreren met Home Assistant.\n\n1. Ga naar deCONZ systeeminstellingen (Instellingen -> Gateway -> Geavanceerd)\n2. Druk op de knop \"Gateway ontgrendelen\"", "title": "Koppel met deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Sta het importeren van virtuele sensoren toe", - "allow_deconz_groups": "Sta de import van deCONZ-groepen toe" - }, - "title": "Extra configuratieopties voor deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan", - "allow_deconz_groups": "DeCONZ-lichtgroepen toestaan" - }, - "description": "De zichtbaarheid van deCONZ-apparaattypen configureren" - }, "deconz_devices": { "data": { "allow_clip_sensor": "DeCONZ CLIP sensoren toestaan", diff --git a/homeassistant/components/deconz/.translations/nn.json b/homeassistant/components/deconz/.translations/nn.json index 46933ced427..986795e11c9 100644 --- a/homeassistant/components/deconz/.translations/nn.json +++ b/homeassistant/components/deconz/.translations/nn.json @@ -19,13 +19,6 @@ "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere den med Home Assistant.\n\n1. G\u00e5 til systeminnstillingane til deCONZ\n2. Trykk p\u00e5 \"L\u00e5s opp gateway\"-knappen", "title": "Link med deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Tillat importering av virtuelle sensorar", - "allow_deconz_groups": "Tillat \u00e5 importera deCONZ-grupper" - }, - "title": "Ekstra konfigurasjonsalternativ for deCONZ" } }, "title": "deCONZ Zigbee gateway" diff --git a/homeassistant/components/deconz/.translations/no.json b/homeassistant/components/deconz/.translations/no.json index e7c5893b4f1..a10ae01e25f 100644 --- a/homeassistant/components/deconz/.translations/no.json +++ b/homeassistant/components/deconz/.translations/no.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Tillat import av virtuelle sensorer", - "allow_deconz_groups": "Tillat import av deCONZ grupper" - }, "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?", "title": "deCONZ Zigbee gateway via Hass.io tillegg" }, @@ -31,13 +27,6 @@ "link": { "description": "L\u00e5s opp deCONZ-gatewayen din for \u00e5 registrere deg med Home Assistant. \n\n 1. G\u00e5 til deCONZ-systeminnstillinger -> Gateway -> Avansert \n 2. Trykk p\u00e5 \"L\u00e5s opp gateway\" knappen", "title": "Koble til deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Tillat import av virtuelle sensorer", - "allow_deconz_groups": "Tillat import av deCONZ grupper" - }, - "title": "Ekstra konfigurasjonsalternativer for deCONZ" } }, "title": "deCONZ Zigbee gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", - "allow_deconz_groups": "Tillat deCONZ lys grupper" - }, - "description": "Konfigurere synlighet av deCONZ enhetstyper" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Tillat deCONZ CLIP-sensorer", diff --git a/homeassistant/components/deconz/.translations/pl.json b/homeassistant/components/deconz/.translations/pl.json index d12e633bf23..ace1f4182a4 100644 --- a/homeassistant/components/deconz/.translations/pl.json +++ b/homeassistant/components/deconz/.translations/pl.json @@ -14,10 +14,6 @@ "flow_title": "Bramka deCONZ Zigbee ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w", - "allow_deconz_groups": "Zezwalaj na importowanie grup deCONZ" - }, "description": "Czy chcesz skonfigurowa\u0107 Home Assistant, aby po\u0142\u0105czy\u0142 si\u0119 z bramk\u0105 deCONZ dostarczon\u0105 przez dodatek Hass.io {addon}?", "title": "Bramka deCONZ Zigbee przez dodatek Hass.io" }, @@ -31,13 +27,6 @@ "link": { "description": "Odblokuj bramk\u0119 deCONZ, aby zarejestrowa\u0107 j\u0105 w Home Assistant. \n\n 1. Przejd\u017a do ustawienia deCONZ > bramka > Zaawansowane\n 2. Naci\u015bnij przycisk \"Uwierzytelnij aplikacj\u0119\"", "title": "Po\u0142\u0105cz z deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Zezwalaj na importowanie wirtualnych sensor\u00f3w", - "allow_deconz_groups": "Zezw\u00f3l na importowanie grup deCONZ" - }, - "title": "Dodatkowe opcje konfiguracji dla deCONZ" } }, "title": "Brama deCONZ Zigbee" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", - "allow_deconz_groups": "Zezwalaj na grupy \u015bwiate\u0142 deCONZ" - }, - "description": "Skonfiguruj widoczno\u015b\u0107 urz\u0105dze\u0144 deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Zezwalaj na czujniki deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/pt-BR.json b/homeassistant/components/deconz/.translations/pt-BR.json index 8d54c470846..6d800bb0269 100644 --- a/homeassistant/components/deconz/.translations/pt-BR.json +++ b/homeassistant/components/deconz/.translations/pt-BR.json @@ -13,10 +13,6 @@ }, "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais", - "allow_deconz_groups": "Permitir a importa\u00e7\u00e3o de grupos deCONZ" - }, "description": "Deseja configurar o Home Assistant para conectar-se ao gateway deCONZ fornecido pelo add-on hass.io {addon} ?", "title": "Gateway deCONZ Zigbee via add-on Hass.io" }, @@ -30,26 +26,8 @@ "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Linkar com deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais", - "allow_deconz_groups": "Permitir a importa\u00e7\u00e3o de grupos deCONZ" - }, - "title": "Op\u00e7\u00f5es extras de configura\u00e7\u00e3o para deCONZ" } }, "title": "Gateway deCONZ Zigbee" - }, - "options": { - "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Permitir sensores deCONZ CLIP", - "allow_deconz_groups": "Permitir grupos de luz deCONZ" - }, - "description": "Configurar visibilidade dos tipos de dispositivos deCONZ" - } - } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/.translations/pt.json b/homeassistant/components/deconz/.translations/pt.json index f24d7692a55..f0ea9e57ca0 100644 --- a/homeassistant/components/deconz/.translations/pt.json +++ b/homeassistant/components/deconz/.translations/pt.json @@ -19,13 +19,6 @@ "link": { "description": "Desbloqueie o seu gateway deCONZ para se registar no Home Assistant. \n\n 1. V\u00e1 para as configura\u00e7\u00f5es do sistema deCONZ \n 2. Pressione o bot\u00e3o \"Desbloquear Gateway\"", "title": "Liga\u00e7\u00e3o com deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Permitir a importa\u00e7\u00e3o de sensores virtuais", - "allow_deconz_groups": "Permitir a importa\u00e7\u00e3o de grupos deCONZ" - }, - "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o extra para deCONZ" } }, "title": "Gateway Zigbee deCONZ" diff --git a/homeassistant/components/deconz/.translations/ru.json b/homeassistant/components/deconz/.translations/ru.json index 054c85f595a..4d89f5ff8e0 100644 --- a/homeassistant/components/deconz/.translations/ru.json +++ b/homeassistant/components/deconz/.translations/ru.json @@ -14,10 +14,6 @@ "flow_title": "\u0428\u043b\u044e\u0437 Zigbee deCONZ ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" - }, "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 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a deCONZ (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io \"{addon}\")?", "title": "Zigbee \u0448\u043b\u044e\u0437 deCONZ (\u0440\u0430\u0441\u0448\u0438\u0440\u0435\u043d\u0438\u0435 \u0434\u043b\u044f Hass.io)" }, @@ -31,13 +27,6 @@ "link": { "description": "\u0420\u0430\u0437\u0431\u043b\u043e\u043a\u0438\u0440\u0443\u0439\u0442\u0435 \u0448\u043b\u044e\u0437 deCONZ \u0434\u043b\u044f \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0430\u0446\u0438\u0438 \u0432 Home Assistant:\n\n1. \u041f\u0435\u0440\u0435\u0439\u0434\u0438\u0442\u0435 \u043a \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430\u043c \u0441\u0438\u0441\u0442\u0435\u043c\u044b deCONZ -> Gateway -> Advanced.\n2. \u041d\u0430\u0436\u043c\u0438\u0442\u0435 \u043a\u043d\u043e\u043f\u043a\u0443 \u00abAuthenticate app\u00bb.", "title": "\u0421\u0432\u044f\u0437\u044c \u0441 deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0432\u0438\u0440\u0442\u0443\u0430\u043b\u044c\u043d\u044b\u0445 \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432", - "allow_deconz_groups": "\u0420\u0430\u0437\u0440\u0435\u0448\u0438\u0442\u044c \u0438\u043c\u043f\u043e\u0440\u0442 \u0433\u0440\u0443\u043f\u043f deCONZ" - }, - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 deCONZ" } }, "title": "deCONZ" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP", - "allow_deconz_groups": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0433\u0440\u0443\u043f\u043f\u044b \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u044f deCONZ" - }, - "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0432\u0438\u0434\u0438\u043c\u043e\u0441\u0442\u0438 \u0442\u0438\u043f\u043e\u0432 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "\u041e\u0442\u043e\u0431\u0440\u0430\u0436\u0430\u0442\u044c \u0441\u0435\u043d\u0441\u043e\u0440\u044b deCONZ CLIP", diff --git a/homeassistant/components/deconz/.translations/sl.json b/homeassistant/components/deconz/.translations/sl.json index d8d98c103c3..15927059d32 100644 --- a/homeassistant/components/deconz/.translations/sl.json +++ b/homeassistant/components/deconz/.translations/sl.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee prehod ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev", - "allow_deconz_groups": "Dovoli uvoz deCONZ skupin" - }, "description": "Ali \u017eelite konfigurirati Home Assistant za povezavo s prehodom deCONZ, ki ga ponuja dodatek Hass.io {addon} ?", "title": "deCONZ Zigbee prehod preko dodatka Hass.io" }, @@ -31,13 +27,6 @@ "link": { "description": "Odklenite va\u0161 deCONZ gateway za registracijo s Home Assistant-om. \n1. Pojdite v deCONZ sistemske nastavitve\n2. Pritisnite tipko \"odkleni prehod\"", "title": "Povezava z deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Dovoli uvoz virtualnih senzorjev", - "allow_deconz_groups": "Dovoli uvoz deCONZ skupin" - }, - "title": "Dodatne mo\u017enosti konfiguracije za deCONZ" } }, "title": "deCONZ Zigbee prehod" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", - "allow_deconz_groups": "Dovolite deCONZ skupine lu\u010di" - }, - "description": "Konfiguracija vidnosti tipov naprav deCONZ" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Dovoli deCONZ CLIP senzorje", diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index 3d74d6cb944..11a8aac485a 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", - "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" - }, "description": "Vill du konfigurera Home Assistant att ansluta till den deCONZ-gateway som tillhandah\u00e5lls av Hass.io-till\u00e4gget {addon}?", "title": "deCONZ Zigbee gateway via Hass.io till\u00e4gg" }, @@ -31,13 +27,6 @@ "link": { "description": "L\u00e5s upp din deCONZ-gateway f\u00f6r att registrera dig med Home Assistant. \n\n 1. G\u00e5 till deCONZ-systeminst\u00e4llningarna \n 2. Tryck p\u00e5 \"L\u00e5s upp gateway\"-knappen", "title": "L\u00e4nka med deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "Till\u00e5t import av virtuella sensorer", - "allow_deconz_groups": "Till\u00e5t import av deCONZ-grupper" - }, - "title": "Extra konfigurationsalternativ f\u00f6r deCONZ" } }, "title": "deCONZ Zigbee Gateway" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", - "allow_deconz_groups": "Till\u00e5t deCONZ ljusgrupper" - }, - "description": "Konfigurera synlighet f\u00f6r deCONZ-enhetstyper" - }, "deconz_devices": { "data": { "allow_clip_sensor": "Till\u00e5t deCONZ CLIP-sensorer", diff --git a/homeassistant/components/deconz/.translations/vi.json b/homeassistant/components/deconz/.translations/vi.json index 00f1d9be57f..75d8969495b 100644 --- a/homeassistant/components/deconz/.translations/vi.json +++ b/homeassistant/components/deconz/.translations/vi.json @@ -13,13 +13,6 @@ "data": { "port": "C\u1ed5ng (gi\u00e1 tr\u1ecb m\u1eb7c \u0111\u1ecbnh: '80')" } - }, - "options": { - "data": { - "allow_clip_sensor": "Cho ph\u00e9p nh\u1eadp c\u1ea3m bi\u1ebfn \u1ea3o", - "allow_deconz_groups": "Cho ph\u00e9p nh\u1eadp c\u00e1c nh\u00f3m deCONZ" - }, - "title": "T\u00f9y ch\u1ecdn c\u1ea5u h\u00ecnh b\u1ed5 sung cho deCONZ" } } } diff --git a/homeassistant/components/deconz/.translations/zh-Hans.json b/homeassistant/components/deconz/.translations/zh-Hans.json index ce51a54ac77..ada31494619 100644 --- a/homeassistant/components/deconz/.translations/zh-Hans.json +++ b/homeassistant/components/deconz/.translations/zh-Hans.json @@ -19,13 +19,6 @@ "link": { "description": "\u89e3\u9501\u60a8\u7684 deCONZ \u7f51\u5173\u4ee5\u6ce8\u518c\u5230 Home Assistant\u3002 \n\n 1. \u524d\u5f80 deCONZ \u7cfb\u7edf\u8bbe\u7f6e\n 2. \u70b9\u51fb\u201c\u89e3\u9501\u7f51\u5173\u201d\u6309\u94ae", "title": "\u8fde\u63a5 deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "\u5141\u8bb8\u5bfc\u5165\u865a\u62df\u4f20\u611f\u5668", - "allow_deconz_groups": "\u5141\u8bb8\u5bfc\u5165 deCONZ \u7fa4\u7ec4" - }, - "title": "deCONZ \u7684\u9644\u52a0\u914d\u7f6e\u9879" } }, "title": "deCONZ" diff --git a/homeassistant/components/deconz/.translations/zh-Hant.json b/homeassistant/components/deconz/.translations/zh-Hant.json index 073ebd784c6..07b7c6e997b 100644 --- a/homeassistant/components/deconz/.translations/zh-Hant.json +++ b/homeassistant/components/deconz/.translations/zh-Hant.json @@ -14,10 +14,6 @@ "flow_title": "deCONZ Zigbee \u9598\u9053\u5668\uff08{host}\uff09", "step": { "hassio_confirm": { - "data": { - "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668", - "allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44" - }, "description": "\u662f\u5426\u8981\u8a2d\u5b9a Home Assistant \u4ee5\u9023\u7dda\u81f3 Hass.io \u9644\u52a0\u6574\u5408 {addon} \u4e4b deCONZ \u9598\u9053\u5668\uff1f", "title": "\u900f\u904e Hass.io \u9644\u52a0\u7d44\u4ef6 deCONZ Zigbee \u9598\u9053\u5668" }, @@ -31,13 +27,6 @@ "link": { "description": "\u89e3\u9664 deCONZ \u9598\u9053\u5668\u9396\u5b9a\uff0c\u4ee5\u65bc Home Assistant \u9032\u884c\u8a3b\u518a\u3002\n\n1. \u9032\u5165 deCONZ \u7cfb\u7d71\u8a2d\u5b9a -> \u9598\u9053\u5668 -> \u9032\u968e\u8a2d\u5b9a\n2. \u6309\u4e0b\u300c\u8a8d\u8b49\u7a0b\u5f0f\uff08Authenticate app\uff09\u300d\u6309\u9215", "title": "\u9023\u7d50\u81f3 deCONZ" - }, - "options": { - "data": { - "allow_clip_sensor": "\u5141\u8a31\u532f\u5165\u865b\u64ec\u611f\u61c9\u5668", - "allow_deconz_groups": "\u5141\u8a31\u532f\u5165 deCONZ \u7fa4\u7d44" - }, - "title": "deCONZ \u9644\u52a0\u8a2d\u5b9a\u9078\u9805" } }, "title": "deCONZ Zigbee \u9598\u9053\u5668" @@ -96,13 +85,6 @@ }, "options": { "step": { - "async_step_deconz_devices": { - "data": { - "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", - "allow_deconz_groups": "\u5141\u8a31 deCONZ \u71c8\u5149\u7fa4\u7d44" - }, - "description": "\u8a2d\u5b9a deCONZ \u53ef\u8996\u8a2d\u5099\u985e\u578b" - }, "deconz_devices": { "data": { "allow_clip_sensor": "\u5141\u8a31 deCONZ CLIP \u611f\u61c9\u5668", diff --git a/homeassistant/components/demo/.translations/lb.json b/homeassistant/components/demo/.translations/lb.json index d968b43af8b..05b4ba93427 100644 --- a/homeassistant/components/demo/.translations/lb.json +++ b/homeassistant/components/demo/.translations/lb.json @@ -4,9 +4,23 @@ }, "options": { "step": { + "init": { + "data": { + "one": "Een", + "other": "Aner" + } + }, + "options_1": { + "data": { + "bool": "Optionelle Boolean", + "int": "Numeresch Agab" + } + }, "options_2": { "data": { - "select": "Eng Optioun auswielen" + "multi": "Multiple Auswiel", + "select": "Eng Optioun auswielen", + "string": "String W\u00e4ert" } } } diff --git a/homeassistant/components/directv/.translations/ca.json b/homeassistant/components/directv/.translations/ca.json index a88e1feb2e9..4bdc104e7de 100644 --- a/homeassistant/components/directv/.translations/ca.json +++ b/homeassistant/components/directv/.translations/ca.json @@ -5,8 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", - "unknown": "Error inesperat" + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/de.json b/homeassistant/components/directv/.translations/de.json index b6074c732f6..98a9e81f661 100644 --- a/homeassistant/components/directv/.translations/de.json +++ b/homeassistant/components/directv/.translations/de.json @@ -5,8 +5,7 @@ "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", - "unknown": "Unerwarteter Fehler" + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/en.json b/homeassistant/components/directv/.translations/en.json index f5105477c45..774ce1f2035 100644 --- a/homeassistant/components/directv/.translations/en.json +++ b/homeassistant/components/directv/.translations/en.json @@ -5,8 +5,7 @@ "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/es.json b/homeassistant/components/directv/.translations/es.json index ef9edd6dd73..f23f83481e5 100644 --- a/homeassistant/components/directv/.translations/es.json +++ b/homeassistant/components/directv/.translations/es.json @@ -5,8 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", - "unknown": "Error inesperado" + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo." }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/fr.json b/homeassistant/components/directv/.translations/fr.json index 6ba9237a3ad..d7262f50eaf 100644 --- a/homeassistant/components/directv/.translations/fr.json +++ b/homeassistant/components/directv/.translations/fr.json @@ -5,8 +5,7 @@ "unknown": "Erreur inattendue" }, "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "unknown": "Erreur inattendue" + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/it.json b/homeassistant/components/directv/.translations/it.json index 4fe98fc6024..777b66d5c91 100644 --- a/homeassistant/components/directv/.translations/it.json +++ b/homeassistant/components/directv/.translations/it.json @@ -5,8 +5,7 @@ "unknown": "Errore imprevisto" }, "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare", - "unknown": "Errore imprevisto" + "cannot_connect": "Impossibile connettersi, si prega di riprovare" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/ko.json b/homeassistant/components/directv/.translations/ko.json index 46ad9b15e49..5099b264085 100644 --- a/homeassistant/components/directv/.translations/ko.json +++ b/homeassistant/components/directv/.translations/ko.json @@ -5,8 +5,7 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/lb.json b/homeassistant/components/directv/.translations/lb.json index 4a0c1267d2b..4e2a09c6bef 100644 --- a/homeassistant/components/directv/.translations/lb.json +++ b/homeassistant/components/directv/.translations/lb.json @@ -5,12 +5,15 @@ "unknown": "Onerwaarte Feeler" }, "error": { - "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", - "unknown": "Onerwaarte Feeler" + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol." }, "flow_title": "DirecTV: {name}", "step": { "ssdp_confirm": { + "data": { + "one": "Een", + "other": "Aner" + }, "description": "Soll {name} konfigur\u00e9iert ginn?", "title": "Mam DirecTV Receiver verbannen" }, diff --git a/homeassistant/components/directv/.translations/no.json b/homeassistant/components/directv/.translations/no.json index b010b1aac01..be2500b38b3 100644 --- a/homeassistant/components/directv/.translations/no.json +++ b/homeassistant/components/directv/.translations/no.json @@ -5,8 +5,7 @@ "unknown": "Uventet feil" }, "error": { - "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", - "unknown": "Uventet feil" + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen" }, "flow_title": "", "step": { diff --git a/homeassistant/components/directv/.translations/pl.json b/homeassistant/components/directv/.translations/pl.json index c02e69601c8..d9de1368ec5 100644 --- a/homeassistant/components/directv/.translations/pl.json +++ b/homeassistant/components/directv/.translations/pl.json @@ -5,8 +5,7 @@ "unknown": "Niespodziewany b\u0142\u0105d." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." }, "flow_title": "DirecTV: {name}", "step": { @@ -18,13 +17,13 @@ "other": "inne" }, "description": "Czy chcesz skonfigurowa\u0107 {name}?", - "title": "Po\u0142\u0105cz si\u0119 z odbiornikiem DirecTV" + "title": "Po\u0142\u0105czenie z odbiornikiem DirecTV" }, "user": { "data": { "host": "Nazwa hosta lub adres IP" }, - "title": "Po\u0142\u0105cz si\u0119 z odbiornikiem DirecTV" + "title": "Po\u0142\u0105czenie z odbiornikiem DirecTV" } }, "title": "DirecTV" diff --git a/homeassistant/components/directv/.translations/ru.json b/homeassistant/components/directv/.translations/ru.json index 7fc53b8b8ea..08e18b89bf1 100644 --- a/homeassistant/components/directv/.translations/ru.json +++ b/homeassistant/components/directv/.translations/ru.json @@ -5,8 +5,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/sl.json b/homeassistant/components/directv/.translations/sl.json index ce3d6fac9eb..ab20a6ec424 100644 --- a/homeassistant/components/directv/.translations/sl.json +++ b/homeassistant/components/directv/.translations/sl.json @@ -5,8 +5,7 @@ "unknown": "Nepri\u010dakovana napaka" }, "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova", - "unknown": "Nepri\u010dakovana napaka" + "cannot_connect": "Povezava ni uspela, poskusite znova" }, "flow_title": "DirecTV: {name}", "step": { diff --git a/homeassistant/components/directv/.translations/zh-Hant.json b/homeassistant/components/directv/.translations/zh-Hant.json index 38b89b729ad..b7a1bb41f53 100644 --- a/homeassistant/components/directv/.translations/zh-Hant.json +++ b/homeassistant/components/directv/.translations/zh-Hant.json @@ -5,8 +5,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" }, "flow_title": "DirecTV\uff1a{name}", "step": { diff --git a/homeassistant/components/doorbird/.translations/ca.json b/homeassistant/components/doorbird/.translations/ca.json index 488481f9614..d26da82ad1e 100644 --- a/homeassistant/components/doorbird/.translations/ca.json +++ b/homeassistant/components/doorbird/.translations/ca.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "Aquest dispositiu DoorBird ja est\u00e0 configurat" + "already_configured": "Aquest dispositiu DoorBird ja est\u00e0 configurat", + "link_local_address": "L'enlla\u00e7 amb adreces locals no est\u00e0 perm\u00e8s", + "not_doorbird_device": "Aquest dispositiu no \u00e9s DoorBird" }, "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/de.json b/homeassistant/components/doorbird/.translations/de.json index 3709adaa69a..2992d066d4a 100644 --- a/homeassistant/components/doorbird/.translations/de.json +++ b/homeassistant/components/doorbird/.translations/de.json @@ -10,6 +10,7 @@ "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/en.json b/homeassistant/components/doorbird/.translations/en.json index f933b9c9929..87524cd7dd6 100644 --- a/homeassistant/components/doorbird/.translations/en.json +++ b/homeassistant/components/doorbird/.translations/en.json @@ -10,6 +10,7 @@ "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { @@ -21,8 +22,7 @@ "title": "Connect to the DoorBird" } }, - "title": "DoorBird", - "flow_title" : "DoorBird {name} ({host})" + "title": "DoorBird" }, "options": { "step": { @@ -34,4 +34,4 @@ } } } -} +} \ No newline at end of file diff --git a/homeassistant/components/doorbird/.translations/es.json b/homeassistant/components/doorbird/.translations/es.json index 93ab919cc03..4e2aa0414dc 100644 --- a/homeassistant/components/doorbird/.translations/es.json +++ b/homeassistant/components/doorbird/.translations/es.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "DoorBird ya est\u00e1 configurado", + "link_local_address": "No se admiten direcciones locales", "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { @@ -9,6 +10,7 @@ "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/ko.json b/homeassistant/components/doorbird/.translations/ko.json index 121262065fd..fff92c32188 100644 --- a/homeassistant/components/doorbird/.translations/ko.json +++ b/homeassistant/components/doorbird/.translations/ko.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc774 DoorBird \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "link_local_address": "\ub85c\uceec \uc8fc\uc18c \uc5f0\uacb0\uc740 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4", + "not_doorbird_device": "\uc774 \uae30\uae30\ub294 DoorBird \uac00 \uc544\ub2d9\ub2c8\ub2e4" }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/lb.json b/homeassistant/components/doorbird/.translations/lb.json index d0b94ed6c59..ba29b19df8a 100644 --- a/homeassistant/components/doorbird/.translations/lb.json +++ b/homeassistant/components/doorbird/.translations/lb.json @@ -10,6 +10,7 @@ "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, + "flow_title": "DoorBird {name{ ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/ru.json b/homeassistant/components/doorbird/.translations/ru.json index bca45c773b6..1c034d6d68b 100644 --- a/homeassistant/components/doorbird/.translations/ru.json +++ b/homeassistant/components/doorbird/.translations/ru.json @@ -1,13 +1,16 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "link_local_address": "\u0421\u0441\u044b\u043b\u043a\u0430 \u043d\u0430 \u043b\u043e\u043a\u0430\u043b\u044c\u043d\u044b\u0435 \u0430\u0434\u0440\u0435\u0441\u0430 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", + "not_doorbird_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 DoorBird." }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/doorbird/.translations/zh-Hant.json b/homeassistant/components/doorbird/.translations/zh-Hant.json index d8b6330b879..bb8b291f86b 100644 --- a/homeassistant/components/doorbird/.translations/zh-Hant.json +++ b/homeassistant/components/doorbird/.translations/zh-Hant.json @@ -10,6 +10,7 @@ "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { diff --git a/homeassistant/components/elgato/.translations/zh-Hant.json b/homeassistant/components/elgato/.translations/zh-Hant.json index b187abc5ccd..c0c638851a1 100644 --- a/homeassistant/components/elgato/.translations/zh-Hant.json +++ b/homeassistant/components/elgato/.translations/zh-Hant.json @@ -19,7 +19,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u5c07 Elgato Key \u7167\u660e\u5e8f\u865f `{serial_number}` \u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u767c\u73fe\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 Elgato Key \u7167\u660e\u8a2d\u5099" } }, "title": "Elgato Key \u7167\u660e" diff --git a/homeassistant/components/esphome/.translations/zh-Hant.json b/homeassistant/components/esphome/.translations/zh-Hant.json index 0386fd8c468..bc229d190a7 100644 --- a/homeassistant/components/esphome/.translations/zh-Hant.json +++ b/homeassistant/components/esphome/.translations/zh-Hant.json @@ -19,7 +19,7 @@ }, "discovery_confirm": { "description": "\u662f\u5426\u8981\u5c07 ESPHome \u7bc0\u9ede `{name}` \u65b0\u589e\u81f3 Home Assistant\uff1f", - "title": "\u767c\u73fe\u5230 ESPHome \u7bc0\u9ede" + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 ESPHome \u7bc0\u9ede" }, "user": { "data": { diff --git a/homeassistant/components/flunearyou/.translations/ca.json b/homeassistant/components/flunearyou/.translations/ca.json new file mode 100644 index 00000000000..dddf7dc2c88 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/ca.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Les coordenades ja estan registrades" + }, + "error": { + "general_error": "S'ha produ\u00eft un error desconegut." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Monitoritza informes basats en usuari i CDC per a parells de coordenades.", + "title": "Configuraci\u00f3 Flu Near You" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/de.json b/homeassistant/components/flunearyou/.translations/de.json new file mode 100644 index 00000000000..0ac83023896 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/de.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Diese Koordinaten sind bereits registriert." + }, + "error": { + "general_error": "Es gab einen unbekannten Fehler." + }, + "step": { + "user": { + "data": { + "latitude": "Breitengrad", + "longitude": "L\u00e4ngengrad" + }, + "title": "Konfigurieren Sie die Grippe in Ihrer N\u00e4he" + } + }, + "title": "Grippe in Ihrer N\u00e4he" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/en.json b/homeassistant/components/flunearyou/.translations/en.json new file mode 100644 index 00000000000..ca868b8ebd9 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/en.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "These coordinates are already registered." + }, + "error": { + "general_error": "There was an unknown error." + }, + "step": { + "user": { + "data": { + "latitude": "Latitude", + "longitude": "Longitude" + }, + "description": "Monitor user-based and CDC repots for a pair of coordinates.", + "title": "Configure Flu Near You" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/es.json b/homeassistant/components/flunearyou/.translations/es.json new file mode 100644 index 00000000000..df104c5405e --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/es.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Estas coordenadas ya est\u00e1n registradas." + }, + "error": { + "general_error": "Se ha producido un error desconocido." + }, + "step": { + "user": { + "data": { + "latitude": "Latitud", + "longitude": "Longitud" + }, + "description": "Monitorizar reportes de usuarios y del CDC para un par de coordenadas", + "title": "Configurar Flu Near You" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/ko.json b/homeassistant/components/flunearyou/.translations/ko.json new file mode 100644 index 00000000000..c155a7f6111 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/ko.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\uc88c\ud45c\uac12\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "general_error": "\uc54c \uc218 \uc5c6\ub294 \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "latitude": "\uc704\ub3c4", + "longitude": "\uacbd\ub3c4" + }, + "description": "\uc0ac\uc6a9\uc790 \uae30\ubc18 \ub370\uc774\ud130 \ubc0f CDC \ubcf4\uace0\uc11c\uc5d0\uc11c \uc88c\ud45c\ub97c \ubaa8\ub2c8\ud130\ub9c1\ud569\ub2c8\ub2e4.", + "title": "Flu Near You \uad6c\uc131" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/lb.json b/homeassistant/components/flunearyou/.translations/lb.json new file mode 100644 index 00000000000..03c8d0bce09 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/lb.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "D\u00ebs Koordinate si scho registr\u00e9iert" + }, + "error": { + "general_error": "Onbekannten Feeler" + }, + "step": { + "user": { + "data": { + "latitude": "Breedegrad", + "longitude": "L\u00e4ngegrad" + }, + "description": "Iwwerwach Benotzer-bas\u00e9iert an CDC Berichter fir Koordinaten.", + "title": "Flu Near You konfigur\u00e9ieren" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/ru.json b/homeassistant/components/flunearyou/.translations/ru.json new file mode 100644 index 00000000000..8e8b050ba7a --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/ru.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u041a\u043e\u043e\u0440\u0434\u0438\u043d\u0430\u0442\u044b \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u044b." + }, + "error": { + "general_error": "\u041d\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "latitude": "\u0428\u0438\u0440\u043e\u0442\u0430", + "longitude": "\u0414\u043e\u043b\u0433\u043e\u0442\u0430" + }, + "description": "\u041c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u0441\u043a\u0438\u0445 \u0438 CDC \u043e\u0442\u0447\u0435\u0442\u043e\u0432 \u0434\u043b\u044f \u0443\u043a\u0430\u0437\u0430\u043d\u043d\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", + "title": "Flu Near You" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/.translations/zh-Hant.json b/homeassistant/components/flunearyou/.translations/zh-Hant.json new file mode 100644 index 00000000000..50f31707a61 --- /dev/null +++ b/homeassistant/components/flunearyou/.translations/zh-Hant.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u4e9b\u5ea7\u6a19\u5df2\u8a3b\u518a\u3002" + }, + "error": { + "general_error": "\u767c\u751f\u672a\u77e5\u932f\u8aa4\u3002" + }, + "step": { + "user": { + "data": { + "latitude": "\u7def\u5ea6", + "longitude": "\u7d93\u5ea6" + }, + "description": "\u76e3\u6e2c\u4f7f\u7528\u8005\u8207 CDC \u56de\u5831\u5ea7\u6a19\u3002", + "title": "\u8a2d\u5b9a Flu Near You" + } + }, + "title": "Flu Near You" + } +} \ No newline at end of file diff --git a/homeassistant/components/freebox/.translations/lb.json b/homeassistant/components/freebox/.translations/lb.json index fdf08c0a5fe..21567b8f096 100644 --- a/homeassistant/components/freebox/.translations/lb.json +++ b/homeassistant/components/freebox/.translations/lb.json @@ -10,6 +10,7 @@ }, "step": { "link": { + "description": "Dr\u00e9ck \"Ofsch\u00e9cken\", dann dr\u00e9ck de rietse Feil um Router fir d'Freebox mam Home Assistant ze registr\u00e9ieren.\n\n![Location of button on the router](/static/images/config_freebox.png)", "title": "Freebox Router verbannen" }, "user": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/bg.json b/homeassistant/components/geonetnz_quakes/.translations/bg.json index 48d6eacda91..c907a6bafd9 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/bg.json +++ b/homeassistant/components/geonetnz_quakes/.translations/bg.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/ca.json b/homeassistant/components/geonetnz_quakes/.translations/ca.json index 7a88d3d2c72..c422c1768a7 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ca.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ca.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "La ubicaci\u00f3 ja est\u00e0 configurada." }, - "error": { - "identifier_exists": "Ubicaci\u00f3 ja registrada" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/da.json b/homeassistant/components/geonetnz_quakes/.translations/da.json index 0d0e927bc4b..15847cdadc9 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/da.json +++ b/homeassistant/components/geonetnz_quakes/.translations/da.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Placering allerede registreret" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/de.json b/homeassistant/components/geonetnz_quakes/.translations/de.json index 4f5a5cde750..a9d3c8dca79 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/de.json +++ b/homeassistant/components/geonetnz_quakes/.translations/de.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Der Standort ist bereits konfiguriert." }, - "error": { - "identifier_exists": "Standort bereits registriert" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/en.json b/homeassistant/components/geonetnz_quakes/.translations/en.json index ed83ab49436..41fafa5763b 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/en.json +++ b/homeassistant/components/geonetnz_quakes/.translations/en.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Location is already configured." }, - "error": { - "identifier_exists": "Location already registered" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/es.json b/homeassistant/components/geonetnz_quakes/.translations/es.json index f50823186c3..daab68f1111 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/es.json +++ b/homeassistant/components/geonetnz_quakes/.translations/es.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." }, - "error": { - "identifier_exists": "Ubicaci\u00f3n ya registrada" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/fr.json b/homeassistant/components/geonetnz_quakes/.translations/fr.json index 39aee7a6694..0a6fb793628 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/fr.json +++ b/homeassistant/components/geonetnz_quakes/.translations/fr.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "L'emplacement est d\u00e9j\u00e0 configur\u00e9." }, - "error": { - "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/it.json b/homeassistant/components/geonetnz_quakes/.translations/it.json index 7b65c27f161..2b2cac02737 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/it.json +++ b/homeassistant/components/geonetnz_quakes/.translations/it.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "La posizione \u00e8 gi\u00e0 configurata." }, - "error": { - "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/ko.json b/homeassistant/components/geonetnz_quakes/.translations/ko.json index 04adb36e5d2..30c534b18e0 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ko.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ko.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\uc704\uce58\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, - "error": { - "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/lb.json b/homeassistant/components/geonetnz_quakes/.translations/lb.json index ea9d1682eda..a4cbecc5818 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/lb.json +++ b/homeassistant/components/geonetnz_quakes/.translations/lb.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Standuert ass scho konfigu\u00e9iert." }, - "error": { - "identifier_exists": "Standuert ass scho registr\u00e9iert" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/nl.json b/homeassistant/components/geonetnz_quakes/.translations/nl.json index d6af28240eb..4495dee078d 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/nl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/nl.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Locatie al geregistreerd" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/no.json b/homeassistant/components/geonetnz_quakes/.translations/no.json index df69f6a3913..82160a4295f 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/no.json +++ b/homeassistant/components/geonetnz_quakes/.translations/no.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Plasseringen er allerede konfigurert." }, - "error": { - "identifier_exists": "Beliggenhet allerede er registrert" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/pl.json b/homeassistant/components/geonetnz_quakes/.translations/pl.json index 5de41e72ef6..b9763b61fcc 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pl.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Lokalizacja jest ju\u017c skonfigurowana." }, - "error": { - "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json index 7e3ee3b24da..1dcf264b3f6 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json +++ b/homeassistant/components/geonetnz_quakes/.translations/pt-BR.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Localiza\u00e7\u00e3o j\u00e1 registrada" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/ru.json b/homeassistant/components/geonetnz_quakes/.translations/ru.json index e8bf8499be6..0b3d23bfa3b 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/ru.json +++ b/homeassistant/components/geonetnz_quakes/.translations/ru.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, - "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/sl.json b/homeassistant/components/geonetnz_quakes/.translations/sl.json index 1176c08f453..03f265f2719 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/sl.json +++ b/homeassistant/components/geonetnz_quakes/.translations/sl.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "Lokacija je \u017ee nastavljena." }, - "error": { - "identifier_exists": "Lokacija je \u017ee registrirana" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/sv.json b/homeassistant/components/geonetnz_quakes/.translations/sv.json index 13058ad3ad2..3e27c340808 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/sv.json +++ b/homeassistant/components/geonetnz_quakes/.translations/sv.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Plats redan registrerad" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json index 3d312978bb2..f46e74a35bc 100644 --- a/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json +++ b/homeassistant/components/geonetnz_quakes/.translations/zh-Hant.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\u4f4d\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" }, - "error": { - "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/harmony/.translations/ca.json b/homeassistant/components/harmony/.translations/ca.json index 75fded469a8..f4e77752936 100644 --- a/homeassistant/components/harmony/.translations/ca.json +++ b/homeassistant/components/harmony/.translations/ca.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", - "invalid_auth": "Autenticaci\u00f3 inv\u00e0lida", "unknown": "Error inesperat" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/de.json b/homeassistant/components/harmony/.translations/de.json index 84187ef1d52..70a5c8707ce 100644 --- a/homeassistant/components/harmony/.translations/de.json +++ b/homeassistant/components/harmony/.translations/de.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", - "invalid_auth": "Ung\u00fcltige Authentifizierung", "unknown": "Unerwarteter Fehler" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/en.json b/homeassistant/components/harmony/.translations/en.json index 697d5572373..00054dbc51e 100644 --- a/homeassistant/components/harmony/.translations/en.json +++ b/homeassistant/components/harmony/.translations/en.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", "unknown": "Unexpected error" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/es.json b/homeassistant/components/harmony/.translations/es.json index f8e8bd9ea7e..300b2e4cb8d 100644 --- a/homeassistant/components/harmony/.translations/es.json +++ b/homeassistant/components/harmony/.translations/es.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo", - "invalid_auth": "Autenticaci\u00f3n no v\u00e1lida", "unknown": "Error inesperado" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/fr.json b/homeassistant/components/harmony/.translations/fr.json index e927254b9e2..60848bea459 100644 --- a/homeassistant/components/harmony/.translations/fr.json +++ b/homeassistant/components/harmony/.translations/fr.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "invalid_auth": "Authentification non valide", "unknown": "Erreur inattendue" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/it.json b/homeassistant/components/harmony/.translations/it.json index 36d06ef565c..4b88151f3d6 100644 --- a/homeassistant/components/harmony/.translations/it.json +++ b/homeassistant/components/harmony/.translations/it.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Impossibile connettersi, si prega di riprovare", - "invalid_auth": "Autenticazione non valida", "unknown": "Errore imprevisto" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/ko.json b/homeassistant/components/harmony/.translations/ko.json index 6106ce8a89d..392c06390aa 100644 --- a/homeassistant/components/harmony/.translations/ko.json +++ b/homeassistant/components/harmony/.translations/ko.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "invalid_auth": "\uc778\uc99d\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/lb.json b/homeassistant/components/harmony/.translations/lb.json index 64536a01407..6cd2ab7d7bf 100644 --- a/homeassistant/components/harmony/.translations/lb.json +++ b/homeassistant/components/harmony/.translations/lb.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", - "invalid_auth": "Ong\u00eblteg Authentifikatioun", "unknown": "Onerwaarte Feeler" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/no.json b/homeassistant/components/harmony/.translations/no.json index 6c989f8068b..4dd86965bfd 100644 --- a/homeassistant/components/harmony/.translations/no.json +++ b/homeassistant/components/harmony/.translations/no.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", - "invalid_auth": "Ugyldig godkjenning", "unknown": "Uventet feil" }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/pl.json b/homeassistant/components/harmony/.translations/pl.json index a9f611d0f35..e5ace2e0d1d 100644 --- a/homeassistant/components/harmony/.translations/pl.json +++ b/homeassistant/components/harmony/.translations/pl.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "invalid_auth": "Niepoprawne uwierzytelnienie.", "unknown": "Niespodziewany b\u0142\u0105d." }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/ru.json b/homeassistant/components/harmony/.translations/ru.json index cdeb809da12..b89296616b3 100644 --- a/homeassistant/components/harmony/.translations/ru.json +++ b/homeassistant/components/harmony/.translations/ru.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", - "invalid_auth": "\u041d\u0435\u0432\u0435\u0440\u043d\u0430\u044f \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u044f.", "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "flow_title": "Logitech Harmony Hub {name}", diff --git a/homeassistant/components/harmony/.translations/zh-Hant.json b/homeassistant/components/harmony/.translations/zh-Hant.json index 7cdbad1a70d..9e523c67290 100644 --- a/homeassistant/components/harmony/.translations/zh-Hant.json +++ b/homeassistant/components/harmony/.translations/zh-Hant.json @@ -5,7 +5,6 @@ }, "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", - "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "flow_title": "\u7f85\u6280 Harmony Hub {name}", diff --git a/homeassistant/components/heos/.translations/pl.json b/homeassistant/components/heos/.translations/pl.json index d427acc3a98..e494a6b34df 100644 --- a/homeassistant/components/heos/.translations/pl.json +++ b/homeassistant/components/heos/.translations/pl.json @@ -13,7 +13,7 @@ "host": "Host" }, "description": "Wprowad\u017a nazw\u0119 hosta lub adres IP urz\u0105dzenia Heos (najlepiej pod\u0142\u0105czonego przewodowo do sieci).", - "title": "Po\u0142\u0105cz si\u0119 z Heos" + "title": "Po\u0142\u0105czenie z Heos" } }, "title": "Heos" diff --git a/homeassistant/components/homekit_controller/.translations/hu.json b/homeassistant/components/homekit_controller/.translations/hu.json index 264e635d7f4..53ca9a39015 100644 --- a/homeassistant/components/homekit_controller/.translations/hu.json +++ b/homeassistant/components/homekit_controller/.translations/hu.json @@ -14,7 +14,7 @@ "busy_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel m\u00e1r p\u00e1ros\u00edtva van egy m\u00e1sik vez\u00e9rl\u0151vel.", "max_peers_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel nincs ingyenes p\u00e1ros\u00edt\u00e1si t\u00e1rhely.", "max_tries_error": "Az eszk\u00f6z megtagadta a p\u00e1ros\u00edt\u00e1s hozz\u00e1ad\u00e1s\u00e1t, mivel t\u00f6bb mint 100 sikertelen hiteles\u00edt\u00e9si k\u00eds\u00e9rletet kapott.", - "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy jelenleg nem t\u00e1mogatja az eszk\u00f6zt.", + "pairing_failed": "Nem kezelt hiba t\u00f6rt\u00e9nt az eszk\u00f6zzel val\u00f3 p\u00e1ros\u00edt\u00e1s sor\u00e1n. Lehet, hogy ez \u00e1tmeneti hiba, vagy az eszk\u00f6z jelenleg m\u00e9g nem t\u00e1mogatott.", "unable_to_pair": "Nem siker\u00fclt p\u00e1ros\u00edtani, pr\u00f3b\u00e1ld \u00fajra.", "unknown_error": "Az eszk\u00f6z ismeretlen hib\u00e1t jelentett. A p\u00e1ros\u00edt\u00e1s sikertelen." }, diff --git a/homeassistant/components/huawei_lte/.translations/lb.json b/homeassistant/components/huawei_lte/.translations/lb.json index 56d383edba3..d99c31d2d63 100644 --- a/homeassistant/components/huawei_lte/.translations/lb.json +++ b/homeassistant/components/huawei_lte/.translations/lb.json @@ -33,7 +33,7 @@ "step": { "init": { "data": { - "name": "Numm vum Notifikatioun's Service", + "name": "Numm vum Notifikatioun's Service (Restart n\u00e9ideg bei \u00c4nnerung)", "recipient": "Empf\u00e4nger vun SMS Notifikatioune", "track_new_devices": "Nei Apparater verfollegen" } diff --git a/homeassistant/components/hue/.translations/ca.json b/homeassistant/components/hue/.translations/ca.json index 471ce2181fb..53c248fe179 100644 --- a/homeassistant/components/hue/.translations/ca.json +++ b/homeassistant/components/hue/.translations/ca.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3", + "button_2": "Segon bot\u00f3", + "button_3": "Tercer bot\u00f3", + "button_4": "Quart bot\u00f3", + "dim_down": "Atenua la brillantor", + "dim_up": "Augmenta la brillantor", + "turn_off": "Apaga", + "turn_on": "Enc\u00e9n" + }, + "trigger_type": { + "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" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/da.json b/homeassistant/components/hue/.translations/da.json index afcfd7071e7..c00c19be42a 100644 --- a/homeassistant/components/hue/.translations/da.json +++ b/homeassistant/components/hue/.translations/da.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "F\u00f8rste knap", + "button_2": "Anden knap", + "button_3": "Tredje knap", + "button_4": "Fjerde knap", + "dim_down": "D\u00e6mp ned", + "dim_up": "D\u00e6mp op", + "turn_off": "Sluk", + "turn_on": "T\u00e6nd" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\"-knappen frigivet efter langt tryk", + "remote_button_short_press": "\"{subtype}\"-knappen trykket p\u00e5", + "remote_button_short_release": "\"{subtype}\"-knappen frigivet" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/de.json b/homeassistant/components/hue/.translations/de.json index 1907d9d23ca..a4ab9123b48 100644 --- a/homeassistant/components/hue/.translations/de.json +++ b/homeassistant/components/hue/.translations/de.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Erste Taste", + "button_2": "Zweite Taste", + "button_3": "Dritte Taste", + "button_4": "Vierte Taste", + "dim_down": "Dimmer runter", + "dim_up": "Dimmer hoch", + "turn_off": "Ausschalten", + "turn_on": "Einschalten" + }, + "trigger_type": { + "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" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/es.json b/homeassistant/components/hue/.translations/es.json index bc41d3d2df0..6a5074c6e4a 100644 --- a/homeassistant/components/hue/.translations/es.json +++ b/homeassistant/components/hue/.translations/es.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "dim_down": "Bajar la intensidad", + "dim_up": "Subir la intensidad", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_long_release": "Bot\u00f3n \"{subtype}\" soltado despu\u00e9s de una pulsaci\u00f3n larga", + "remote_button_short_press": "Bot\u00f3n \"{subtype}\" pulsado", + "remote_button_short_release": "Bot\u00f3n \"{subtype}\" soltado" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ko.json b/homeassistant/components/hue/.translations/ko.json index 7e837ca5ff9..8b1c413b205 100644 --- a/homeassistant/components/hue/.translations/ko.json +++ b/homeassistant/components/hue/.translations/ko.json @@ -27,5 +27,22 @@ } }, "title": "\ud544\ub9bd\uc2a4 Hue \ube0c\ub9bf\uc9c0" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\uccab \ubc88\uc9f8 \ubc84\ud2bc", + "button_2": "\ub450 \ubc88\uc9f8 \ubc84\ud2bc", + "button_3": "\uc138 \ubc88\uc9f8 \ubc84\ud2bc", + "button_4": "\ub124 \ubc88\uc9f8 \ubc84\ud2bc", + "dim_down": "\uc5b4\ub461\uac8c \ud558\uae30", + "dim_up": "\ubc1d\uac8c \ud558\uae30", + "turn_off": "\ub044\uae30", + "turn_on": "\ucf1c\uae30" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" \ubc84\ud2bc\uc774 \uae38\uac8c \ub20c\ub838\ub2e4\uac00 \uc190\uc744 \ub5c4 \ub54c", + "remote_button_short_press": "\"{subtype}\" \ubc84\ud2bc\uc774 \ub20c\ub9b4 \ub54c", + "remote_button_short_release": "\"{subtype}\" \ubc84\ud2bc\uc5d0\uc11c \uc190\uc744 \ub5c4 \ub54c" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/lb.json b/homeassistant/components/hue/.translations/lb.json index ac83609ff02..2b5b168817f 100644 --- a/homeassistant/components/hue/.translations/lb.json +++ b/homeassistant/components/hue/.translations/lb.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u00c9ischte Kn\u00e4ppchen", + "button_2": "Zweete Kn\u00e4ppchen", + "button_3": "Dr\u00ebtte Kn\u00e4ppchen", + "button_4": "V\u00e9ierte Kn\u00e4ppchen", + "dim_down": "Verd\u00e4ischteren", + "dim_up": "Erhellen", + "turn_off": "Ausschalten", + "turn_on": "Uschalten" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" Kn\u00e4ppche no laangem unhalen lassgelooss", + "remote_button_short_press": "\"{subtype}\" Kn\u00e4ppche gedr\u00e9ckt", + "remote_button_short_release": "\"{subtype}\" Kn\u00e4ppche lassgelooss" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/ru.json b/homeassistant/components/hue/.translations/ru.json index 3425cb82d01..fa2f2e55744 100644 --- a/homeassistant/components/hue/.translations/ru.json +++ b/homeassistant/components/hue/.translations/ru.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "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", + "button_4": "\u0427\u0435\u0442\u0432\u0435\u0440\u0442\u0430\u044f \u043a\u043d\u043e\u043f\u043a\u0430", + "dim_down": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u043c\u0435\u043d\u044c\u0448\u0430\u0435\u0442\u0441\u044f", + "dim_up": "\u042f\u0440\u043a\u043e\u0441\u0442\u044c \u0443\u0432\u0435\u043b\u0438\u0447\u0438\u0432\u0430\u0435\u0442\u0441\u044f", + "turn_off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", + "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f" + }, + "trigger_type": { + "remote_button_long_release": "\"{subtype}\" \u043e\u0442\u043f\u0443\u0449\u0435\u043d\u0430 \u043f\u043e\u0441\u043b\u0435 \u043d\u0435\u043f\u0440\u0435\u0440\u044b\u0432\u043d\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" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/.translations/zh-Hant.json b/homeassistant/components/hue/.translations/zh-Hant.json index 6bbe75a8019..0aa75438f7b 100644 --- a/homeassistant/components/hue/.translations/zh-Hant.json +++ b/homeassistant/components/hue/.translations/zh-Hant.json @@ -27,5 +27,22 @@ } }, "title": "Philips Hue" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "\u7b2c\u4e00\u500b\u6309\u9215", + "button_2": "\u7b2c\u4e8c\u500b\u6309\u9215", + "button_3": "\u7b2c\u4e09\u500b\u6309\u9215", + "button_4": "\u7b2c\u56db\u500b\u6309\u9215", + "dim_down": "\u8abf\u6697", + "dim_up": "\u8abf\u4eae", + "turn_off": "\u95dc\u9589", + "turn_on": "\u958b\u555f" + }, + "trigger_type": { + "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" + } } } \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/ca.json b/homeassistant/components/ipp/.translations/ca.json index 5708c8e638d..f193878d952 100644 --- a/homeassistant/components/ipp/.translations/ca.json +++ b/homeassistant/components/ipp/.translations/ca.json @@ -1,13 +1,31 @@ { "config": { + "abort": { + "already_configured": "Aquesta impressora ja est\u00e0 configurada.", + "connection_error": "No s'ha pogut connectar amb la impressora.", + "connection_upgrade": "No s'ha pogut connectar amb la impressora, es necessita actualitzar la connexi\u00f3." + }, + "error": { + "connection_error": "No s'ha pogut connectar amb la impressora." + }, "flow_title": "Impressora: {name}", "step": { "user": { "data": { + "base_path": "Ruta relativa a la impressora", "host": "Amfitri\u00f3 o adre\u00e7a IP", - "port": "Port" - } + "port": "Port", + "ssl": "La impressora \u00e9s compatible amb comunicaci\u00f3 SSL/TLS", + "verify_ssl": "La impressora utilitza un certificat SSL adequat" + }, + "description": "Configura la impressora amb el protocol d'impressi\u00f3 per Internet (IPP) per integrar-la amb Home Assistant.", + "title": "Enlla\u00e7 d'impressora" + }, + "zeroconf_confirm": { + "description": "Vols afegir la impressora {name} a Home Assistant?", + "title": "Impressora descoberta" } - } + }, + "title": "Protocol d'impressi\u00f3 per Internet (IPP)" } } \ No newline at end of file diff --git a/homeassistant/components/ipp/.translations/en.json b/homeassistant/components/ipp/.translations/en.json index df84cbefa29..c3fc9be6d45 100644 --- a/homeassistant/components/ipp/.translations/en.json +++ b/homeassistant/components/ipp/.translations/en.json @@ -3,7 +3,8 @@ "abort": { "already_configured": "This printer is already configured.", "connection_error": "Failed to connect to printer.", - "connection_upgrade": "Failed to connect to printer due to connection upgrade being required." + "connection_upgrade": "Failed to connect to printer due to connection upgrade being required.", + "parse_error": "Failed to parse response from printer." }, "error": { "connection_error": "Failed to connect to printer.", diff --git a/homeassistant/components/ipp/.translations/ko.json b/homeassistant/components/ipp/.translations/ko.json new file mode 100644 index 00000000000..ab556519e07 --- /dev/null +++ b/homeassistant/components/ipp/.translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\uc774 \ud504\ub9b0\ud130\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", + "connection_error": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud558\ub824\uba74 \uc5f0\uacb0\uc744 \uc5c5\uadf8\ub808\uc774\ub4dc\ud574\uc57c \ud569\ub2c8\ub2e4." + }, + "error": { + "connection_error": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", + "connection_upgrade": "\ud504\ub9b0\ud130\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. SSL/TLS \uc635\uc158\uc744 \ud655\uc778\ud558\uace0 \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." + }, + "flow_title": "\ud504\ub9b0\ud130: {name}", + "step": { + "user": { + "data": { + "base_path": "\ud504\ub9b0\ud130\uc758 \uc0c1\ub300 \uacbd\ub85c", + "host": "\ud638\uc2a4\ud2b8 \ub610\ub294 IP \uc8fc\uc18c", + "port": "\ud3ec\ud2b8", + "ssl": "\ud504\ub9b0\ud130\ub294 SSL/TLS \ub97c \ud1b5\ud55c \ud1b5\uc2e0\uc744 \uc9c0\uc6d0\ud569\ub2c8\ub2e4", + "verify_ssl": "\ud504\ub9b0\ud130\ub294 \uc62c\ubc14\ub978 SSL \uc778\uc99d\uc11c\ub97c \uc0ac\uc6a9\ud558\uace0 \uc788\uc2b5\ub2c8\ub2e4" + }, + "description": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP) \ub97c \ud1b5\ud574 \ud504\ub9b0\ud130\ub97c \uc124\uc815\ud558\uc5ec Home Assistant \uc640 \uc5f0\ub3d9\ud569\ub2c8\ub2e4.", + "title": "\ud504\ub9b0\ud130 \uc5f0\uacb0" + }, + "zeroconf_confirm": { + "description": "Home Assistant \uc5d0 `{name}` \ud504\ub9b0\ud130\ub97c \ucd94\uac00\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", + "title": "\ubc1c\uacac\ub41c \ud504\ub9b0\ud130" + } + }, + "title": "\uc778\ud130\ub137 \uc778\uc1c4 \ud504\ub85c\ud1a0\ucf5c (IPP)" + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/.translations/ca.json b/homeassistant/components/konnected/.translations/ca.json index d5fbb60ae71..80e7208391b 100644 --- a/homeassistant/components/konnected/.translations/ca.json +++ b/homeassistant/components/konnected/.translations/ca.json @@ -91,6 +91,7 @@ "data": { "activation": "Sortida quan estigui ON", "momentary": "Durada del pols (ms) (opcional)", + "more_states": "Configura estats addicionals per a aquesta zona", "name": "Nom (opcional)", "pause": "Pausa entre polsos (ms) (opcional)", "repeat": "Repeticions (-1 = infinit) (opcional)" diff --git a/homeassistant/components/konnected/.translations/es.json b/homeassistant/components/konnected/.translations/es.json index cfd05320e35..64069d4e756 100644 --- a/homeassistant/components/konnected/.translations/es.json +++ b/homeassistant/components/konnected/.translations/es.json @@ -34,6 +34,7 @@ "not_konn_panel": "No es un dispositivo Konnected.io reconocido" }, "error": { + "bad_host": "URL del host de la API de invalidaci\u00f3n no v\u00e1lida", "one": "", "other": "otros" }, @@ -86,7 +87,9 @@ }, "options_misc": { "data": { - "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado" + "api_host": "Invalidar la direcci\u00f3n URL del host de la API (opcional)", + "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", + "override_api_host": "Reemplazar la URL predeterminada del panel host de la API de Home Assistant" }, "description": "Seleccione el comportamiento deseado para su panel", "title": "Configurar miscel\u00e1neos" diff --git a/homeassistant/components/konnected/.translations/ko.json b/homeassistant/components/konnected/.translations/ko.json index 0c5e213ea0d..34dd01d06b6 100644 --- a/homeassistant/components/konnected/.translations/ko.json +++ b/homeassistant/components/konnected/.translations/ko.json @@ -33,6 +33,9 @@ "abort": { "not_konn_panel": "\uc778\uc2dd\ub41c Konnected.io \uae30\uae30\uac00 \uc544\ub2d9\ub2c8\ub2e4" }, + "error": { + "bad_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4" + "api_host": "API \ud638\uc2a4\ud2b8 URL \uc7ac\uc815\uc758 (\uc120\ud0dd \uc0ac\ud56d)", + "blink": "\uc0c1\ud0dc \ubcc0\uacbd\uc744 \ubcf4\ub0bc \ub54c \uae5c\ubc15\uc784 \ud328\ub110 LED \ub97c \ucf2d\ub2c8\ub2e4", + "override_api_host": "\uae30\ubcf8 Home Assistant API \ud638\uc2a4\ud2b8 \ud328\ub110 URL \uc7ac\uc815\uc758" }, "description": "\ud328\ub110\uc5d0 \uc6d0\ud558\ub294 \ub3d9\uc791\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "title": "\uae30\ud0c0 \uad6c\uc131" @@ -91,11 +96,12 @@ "data": { "activation": "\uc2a4\uc704\uce58\uac00 \ucf1c\uc9c8 \ub54c \ucd9c\ub825", "momentary": "\ud384\uc2a4 \uc9c0\uc18d\uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", + "more_states": "\uc774 \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd94\uac00 \uc0c1\ud0dc \uad6c\uc131", "name": "\uc774\ub984 (\uc120\ud0dd \uc0ac\ud56d)", "pause": "\ud384\uc2a4 \uac04 \uc77c\uc2dc\uc815\uc9c0 \uc2dc\uac04 (ms) (\uc120\ud0dd \uc0ac\ud56d)", "repeat": "\ubc18\ubcf5 \uc2dc\uac04 (-1 = \ubb34\ud55c) (\uc120\ud0dd \uc0ac\ud56d)" }, - "description": "{zone} \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", + "description": "{zone} \uad6c\uc5ed\uc5d0 \ub300\ud55c \ucd9c\ub825 \uc635\uc158\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694: \uc0c1\ud0dc {state}", "title": "\uc2a4\uc704\uce58 \ucd9c\ub825 \uad6c\uc131" } }, diff --git a/homeassistant/components/konnected/.translations/lb.json b/homeassistant/components/konnected/.translations/lb.json index 984e3b79f54..6ad04254611 100644 --- a/homeassistant/components/konnected/.translations/lb.json +++ b/homeassistant/components/konnected/.translations/lb.json @@ -11,7 +11,7 @@ }, "step": { "confirm": { - "description": "Modell: {model}\nHost: {host}\nPort: {port}\n\nDir k\u00ebnnt den I/O a Panel Verhaalen an de Konnected Alarm Panel Astellunge konfigur\u00e9ieren.", + "description": "Modell: {model}\nID: {id}\nHost: {host}\nPort: {port}\n\nDir k\u00ebnnt den I/O a Panel Verhaalen an de Konnected Alarm Panel Astellunge konfigur\u00e9ieren.", "title": "Konnected Apparat parat" }, "import_confirm": { diff --git a/homeassistant/components/konnected/.translations/ru.json b/homeassistant/components/konnected/.translations/ru.json index 75a879832a4..f3b7f4d6d24 100644 --- a/homeassistant/components/konnected/.translations/ru.json +++ b/homeassistant/components/konnected/.translations/ru.json @@ -33,6 +33,9 @@ "abort": { "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." }, + "error": { + "bad_host": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 URL \u043f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u0438\u044f \u0445\u043e\u0441\u0442\u0430 API." + }, "step": { "options_binary": { "data": { @@ -82,7 +85,9 @@ }, "options_misc": { "data": { - "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f" + "api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL \u0445\u043e\u0441\u0442\u0430 API (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", + "blink": "LED-\u0438\u043d\u0434\u0438\u043a\u0430\u0446\u0438\u044f \u043d\u0430 \u043f\u0430\u043d\u0435\u043b\u0438 \u043f\u0440\u0438 \u043e\u0442\u043f\u0440\u0430\u0432\u043a\u0435 \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u044f", + "override_api_host": "\u041f\u0435\u0440\u0435\u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0438\u0442\u044c URL-\u0430\u0434\u0440\u0435\u0441 \u0445\u043e\u0441\u0442-\u043f\u0430\u043d\u0435\u043b\u0438 Home Assistant API" }, "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0436\u0435\u043b\u0430\u0435\u043c\u043e\u0435 \u043f\u043e\u0432\u0435\u0434\u0435\u043d\u0438\u0435 \u0434\u043b\u044f \u0412\u0430\u0448\u0435\u0439 \u043f\u0430\u043d\u0435\u043b\u0438.", "title": "\u041f\u0440\u043e\u0447\u0438\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" @@ -96,7 +101,7 @@ "pause": "\u041f\u0430\u0443\u0437\u0430 \u043c\u0435\u0436\u0434\u0443 \u0438\u043c\u043f\u0443\u043b\u044c\u0441\u0430\u043c\u0438 (\u043c\u0441) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)", "repeat": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0432\u0442\u043e\u0440\u0435\u043d\u0438\u0439 (-1 = \u0431\u0435\u0441\u043a\u043e\u043d\u0435\u0447\u043d\u043e) (\u043d\u0435\u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e)" }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0445\u043e\u0434\u0430 \u0434\u043b\u044f {zone}.", + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b \u0432\u044b\u0445\u043e\u0434\u0430 \u0434\u043b\u044f {zone}: \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0435 {state}.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0435\u0440\u0435\u043a\u043b\u044e\u0447\u0430\u0435\u043c\u043e\u0433\u043e \u0432\u044b\u0445\u043e\u0434\u0430" } }, diff --git a/homeassistant/components/light/.translations/hu.json b/homeassistant/components/light/.translations/hu.json index 7d7e158f3cb..5192a8c7df2 100644 --- a/homeassistant/components/light/.translations/hu.json +++ b/homeassistant/components/light/.translations/hu.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "{entity_name} f\u00e9nyerej\u00e9nek cs\u00f6kkent\u00e9se", + "brightness_increase": "{entity_name} f\u00e9nyerej\u00e9nek n\u00f6vel\u00e9se", "toggle": "{entity_name} fel/lekapcsol\u00e1sa", "turn_off": "{entity_name} lekapcsol\u00e1sa", "turn_on": "{entity_name} felkapcsol\u00e1sa" diff --git a/homeassistant/components/light/.translations/lb.json b/homeassistant/components/light/.translations/lb.json index a7f807e8dcd..8ffa33a6a3b 100644 --- a/homeassistant/components/light/.translations/lb.json +++ b/homeassistant/components/light/.translations/lb.json @@ -1,6 +1,8 @@ { "device_automation": { "action_type": { + "brightness_decrease": "{entity_name} Hellegkeet reduz\u00e9ieren", + "brightness_increase": "{entity_name} Hellegkeet erh\u00e9ijen", "toggle": "{entity_name} \u00ebmschalten", "turn_off": "{entity_name} ausschalten", "turn_on": "{entity_name} uschalten" diff --git a/homeassistant/components/melcloud/.translations/pl.json b/homeassistant/components/melcloud/.translations/pl.json index 9abb68ca85a..60cc9843607 100644 --- a/homeassistant/components/melcloud/.translations/pl.json +++ b/homeassistant/components/melcloud/.translations/pl.json @@ -15,7 +15,7 @@ "username": "Adres e-mail u\u017cywany do logowania do MELCloud" }, "description": "Po\u0142\u0105cz u\u017cywaj\u0105c swojego konta MELCloud.", - "title": "Po\u0142\u0105cz si\u0119 z MELCloud" + "title": "Po\u0142\u0105czenie z MELCloud" } }, "title": "MELCloud" diff --git a/homeassistant/components/minecraft_server/.translations/ca.json b/homeassistant/components/minecraft_server/.translations/ca.json index 86856ac2d11..e205090d0cd 100644 --- a/homeassistant/components/minecraft_server/.translations/ca.json +++ b/homeassistant/components/minecraft_server/.translations/ca.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Amfitri\u00f3", - "name": "Nom", - "port": "Port" + "name": "Nom" }, "description": "Configuraci\u00f3 d'una inst\u00e0ncia de servidor de Minecraft per poder monitoritzar-lo.", "title": "Enlla\u00e7 del servidor de Minecraft" diff --git a/homeassistant/components/minecraft_server/.translations/da.json b/homeassistant/components/minecraft_server/.translations/da.json index bf930f2f277..e536234ffdb 100644 --- a/homeassistant/components/minecraft_server/.translations/da.json +++ b/homeassistant/components/minecraft_server/.translations/da.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "V\u00e6rt", - "name": "Navn", - "port": "Port" + "name": "Navn" }, "description": "Konfigurer din Minecraft-server-instans for at tillade overv\u00e5gning.", "title": "Forbind din Minecraft-server" diff --git a/homeassistant/components/minecraft_server/.translations/de.json b/homeassistant/components/minecraft_server/.translations/de.json index 00426308239..31f0fe2c0f0 100644 --- a/homeassistant/components/minecraft_server/.translations/de.json +++ b/homeassistant/components/minecraft_server/.translations/de.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Name", - "port": "Port" + "name": "Name" }, "description": "Richte deine Minecraft Server-Instanz ein, um es \u00fcberwachen zu k\u00f6nnen.", "title": "Verkn\u00fcpfe deinen Minecraft Server" diff --git a/homeassistant/components/minecraft_server/.translations/en.json b/homeassistant/components/minecraft_server/.translations/en.json index d0f7a5d6300..fa04208cac9 100644 --- a/homeassistant/components/minecraft_server/.translations/en.json +++ b/homeassistant/components/minecraft_server/.translations/en.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Name", - "port": "Port" + "name": "Name" }, "description": "Set up your Minecraft Server instance to allow monitoring.", "title": "Link your Minecraft Server" diff --git a/homeassistant/components/minecraft_server/.translations/es.json b/homeassistant/components/minecraft_server/.translations/es.json index 14831ef45e1..a4509ba68d4 100644 --- a/homeassistant/components/minecraft_server/.translations/es.json +++ b/homeassistant/components/minecraft_server/.translations/es.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Nombre", - "port": "Puerto" + "name": "Nombre" }, "description": "Configura tu instancia de Minecraft Server para permitir la supervisi\u00f3n.", "title": "Enlace su servidor Minecraft" diff --git a/homeassistant/components/minecraft_server/.translations/fr.json b/homeassistant/components/minecraft_server/.translations/fr.json index bf87c6f3d73..c52021806d8 100644 --- a/homeassistant/components/minecraft_server/.translations/fr.json +++ b/homeassistant/components/minecraft_server/.translations/fr.json @@ -7,8 +7,7 @@ "user": { "data": { "host": "H\u00f4te", - "name": "Nom", - "port": "Port" + "name": "Nom" }, "title": "Reliez votre serveur Minecraft" } diff --git a/homeassistant/components/minecraft_server/.translations/hu.json b/homeassistant/components/minecraft_server/.translations/hu.json index 9341bdbe4d1..4cf4a7a72fb 100644 --- a/homeassistant/components/minecraft_server/.translations/hu.json +++ b/homeassistant/components/minecraft_server/.translations/hu.json @@ -7,10 +7,9 @@ "user": { "data": { "host": "Kiszolg\u00e1l\u00f3", - "name": "N\u00e9v", - "port": "Port" + "name": "N\u00e9v" }, - "title": "Kapcsolja \u00f6ssze a Minecraft szervert" + "title": "Kapcsold \u00f6ssze a Minecraft szervered" } }, "title": "Minecraft szerver" diff --git a/homeassistant/components/minecraft_server/.translations/it.json b/homeassistant/components/minecraft_server/.translations/it.json index 5861eebcc9a..a17ed15a546 100644 --- a/homeassistant/components/minecraft_server/.translations/it.json +++ b/homeassistant/components/minecraft_server/.translations/it.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Nome", - "port": "Porta" + "name": "Nome" }, "description": "Configurare l'istanza del Server Minecraft per consentire il monitoraggio.", "title": "Collega il tuo Server Minecraft" diff --git a/homeassistant/components/minecraft_server/.translations/ko.json b/homeassistant/components/minecraft_server/.translations/ko.json index 66b281cc5d9..ee3ee24db70 100644 --- a/homeassistant/components/minecraft_server/.translations/ko.json +++ b/homeassistant/components/minecraft_server/.translations/ko.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "\ud638\uc2a4\ud2b8", - "name": "\uc774\ub984", - "port": "\ud3ec\ud2b8" + "name": "\uc774\ub984" }, "description": "\ubaa8\ub2c8\ud130\ub9c1\uc774 \uac00\ub2a5\ud558\ub3c4\ub85d Minecraft \uc11c\ubc84 \uc778\uc2a4\ud134\uc2a4\ub97c \uc124\uc815\ud574\uc8fc\uc138\uc694.", "title": "Minecraft \uc11c\ubc84 \uc5f0\uacb0" diff --git a/homeassistant/components/minecraft_server/.translations/lb.json b/homeassistant/components/minecraft_server/.translations/lb.json index f95dd062005..23157202469 100644 --- a/homeassistant/components/minecraft_server/.translations/lb.json +++ b/homeassistant/components/minecraft_server/.translations/lb.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Apparat", - "name": "Numm", - "port": "Port" + "name": "Numm" }, "description": "Riicht deng Minecraft Server Instanz a fir d'Iwwerwaachung z'erlaben", "title": "Verbann d\u00e4in Minecraft Server" diff --git a/homeassistant/components/minecraft_server/.translations/lv.json b/homeassistant/components/minecraft_server/.translations/lv.json index 7de2aaadfc8..a46db9e75e5 100644 --- a/homeassistant/components/minecraft_server/.translations/lv.json +++ b/homeassistant/components/minecraft_server/.translations/lv.json @@ -3,8 +3,7 @@ "step": { "user": { "data": { - "name": "Nosaukums", - "port": "Ports" + "name": "Nosaukums" } } } diff --git a/homeassistant/components/minecraft_server/.translations/nl.json b/homeassistant/components/minecraft_server/.translations/nl.json index 75e19bc2550..4f42a16362b 100644 --- a/homeassistant/components/minecraft_server/.translations/nl.json +++ b/homeassistant/components/minecraft_server/.translations/nl.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Naam", - "port": "Poort" + "name": "Naam" }, "description": "Stel uw Minecraft server in om monitoring toe te staan.", "title": "Koppel uw Minecraft server" diff --git a/homeassistant/components/minecraft_server/.translations/no.json b/homeassistant/components/minecraft_server/.translations/no.json index c49c76865e4..cd627cbe4ba 100644 --- a/homeassistant/components/minecraft_server/.translations/no.json +++ b/homeassistant/components/minecraft_server/.translations/no.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Vert", - "name": "Navn", - "port": "" + "name": "Navn" }, "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", "title": "Link din Minecraft Server" diff --git a/homeassistant/components/minecraft_server/.translations/pl.json b/homeassistant/components/minecraft_server/.translations/pl.json index f9c4a515566..e277579ea23 100644 --- a/homeassistant/components/minecraft_server/.translations/pl.json +++ b/homeassistant/components/minecraft_server/.translations/pl.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Nazwa", - "port": "Port" + "name": "Nazwa" }, "description": "Skonfiguruj instancj\u0119 serwera Minecraft, aby umo\u017cliwi\u0107 monitorowanie.", "title": "Po\u0142\u0105cz sw\u00f3j serwer Minecraft" diff --git a/homeassistant/components/minecraft_server/.translations/ru.json b/homeassistant/components/minecraft_server/.translations/ru.json index 916b342ee4a..a07b84077a9 100644 --- a/homeassistant/components/minecraft_server/.translations/ru.json +++ b/homeassistant/components/minecraft_server/.translations/ru.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "\u0425\u043e\u0441\u0442", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "port": "\u041f\u043e\u0440\u0442" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "description": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u044d\u0442\u043e\u0442 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442 \u0434\u043b\u044f \u043c\u043e\u043d\u0438\u0442\u043e\u0440\u0438\u043d\u0433\u0430 \u0412\u0430\u0448\u0435\u0433\u043e \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Minecraft.", "title": "Minecraft Server" diff --git a/homeassistant/components/minecraft_server/.translations/sl.json b/homeassistant/components/minecraft_server/.translations/sl.json index cf8a8af54ee..d1ed6a36c35 100644 --- a/homeassistant/components/minecraft_server/.translations/sl.json +++ b/homeassistant/components/minecraft_server/.translations/sl.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Gostitelj", - "name": "Ime", - "port": "Vrata" + "name": "Ime" }, "description": "Nastavite svoj Minecraft stre\u017enik, da omogo\u010dite spremljanje.", "title": "Pove\u017eite svoj Minecraft stre\u017enik" diff --git a/homeassistant/components/minecraft_server/.translations/sv.json b/homeassistant/components/minecraft_server/.translations/sv.json index acf941878dd..e95938f1590 100644 --- a/homeassistant/components/minecraft_server/.translations/sv.json +++ b/homeassistant/components/minecraft_server/.translations/sv.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "V\u00e4rd", - "name": "Namn", - "port": "Port" + "name": "Namn" }, "description": "St\u00e4ll in din Minecraft Server-instans f\u00f6r att till\u00e5ta \u00f6vervakning.", "title": "L\u00e4nka din Minecraft-server" diff --git a/homeassistant/components/minecraft_server/.translations/tr.json b/homeassistant/components/minecraft_server/.translations/tr.json index 595c1686982..fb76f697cd5 100644 --- a/homeassistant/components/minecraft_server/.translations/tr.json +++ b/homeassistant/components/minecraft_server/.translations/tr.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "Host", - "name": "Ad", - "port": "Port" + "name": "Ad" }, "description": "G\u00f6zetmeye izin vermek i\u00e7in Minecraft server nesnesini ayarla.", "title": "Minecraft Servern\u0131 ba\u011fla" diff --git a/homeassistant/components/minecraft_server/.translations/zh-Hant.json b/homeassistant/components/minecraft_server/.translations/zh-Hant.json index c451ad71065..fbcde2a6be1 100644 --- a/homeassistant/components/minecraft_server/.translations/zh-Hant.json +++ b/homeassistant/components/minecraft_server/.translations/zh-Hant.json @@ -12,8 +12,7 @@ "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", - "name": "\u540d\u7a31", - "port": "\u901a\u8a0a\u57e0" + "name": "\u540d\u7a31" }, "description": "\u8a2d\u5b9a Minecraft \u4f3a\u670d\u5668\u4ee5\u9032\u884c\u76e3\u63a7\u3002", "title": "\u9023\u7d50 Minecraft \u4f3a\u670d\u5668" diff --git a/homeassistant/components/mqtt/.translations/hu.json b/homeassistant/components/mqtt/.translations/hu.json index 26361b0e363..e45c287f44f 100644 --- a/homeassistant/components/mqtt/.translations/hu.json +++ b/homeassistant/components/mqtt/.translations/hu.json @@ -22,10 +22,22 @@ "data": { "discovery": "Felfedez\u00e9s enged\u00e9lyez\u00e9se" }, - "description": "Szeretn\u00e9d, hogy a Home Assistant csatlakozzon a hass.io addon {addon} \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez?", + "description": "Be szeretn\u00e9d konfigru\u00e1lni, hogy a Home Assistant a(z) {addon} Hass.io add-on \u00e1ltal biztos\u00edtott MQTT br\u00f3kerhez csatlakozzon?", "title": "MQTT Broker a Hass.io b\u0151v\u00edtm\u00e9nyen kereszt\u00fcl" } }, "title": "MQTT" + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Els\u0151 gomb", + "button_2": "M\u00e1sodik gomb", + "button_3": "Harmadik gomb", + "button_4": "Negyedik gomb", + "button_5": "\u00d6t\u00f6dik gomb", + "button_6": "Hatodik gomb", + "turn_off": "Kikapcsol\u00e1s", + "turn_on": "Bekapcsol\u00e1s" + } } } \ No newline at end of file diff --git a/homeassistant/components/notion/.translations/bg.json b/homeassistant/components/notion/.translations/bg.json index 33ce361958a..1c78180e2a8 100644 --- a/homeassistant/components/notion/.translations/bg.json +++ b/homeassistant/components/notion/.translations/bg.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e\u0442\u043e \u0438\u043c\u0435 \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e", "invalid_credentials": "\u041d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430", "no_devices": "\u041d\u0435 \u0441\u0430 \u043d\u0430\u043c\u0435\u0440\u0435\u043d\u0438 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0432 \u043f\u0440\u043e\u0444\u0438\u043b\u0430" }, diff --git a/homeassistant/components/notion/.translations/ca.json b/homeassistant/components/notion/.translations/ca.json index 09f598ef5d1..b6e73a5e209 100644 --- a/homeassistant/components/notion/.translations/ca.json +++ b/homeassistant/components/notion/.translations/ca.json @@ -4,7 +4,6 @@ "already_configured": "Aquest nom d'usuari ja est\u00e0 en \u00fas." }, "error": { - "identifier_exists": "Nom d'usuari ja registrat", "invalid_credentials": "Nom d'usuari o contrasenya incorrectes", "no_devices": "No s'han trobat dispositius al compte" }, diff --git a/homeassistant/components/notion/.translations/da.json b/homeassistant/components/notion/.translations/da.json index 784d106b94c..6b139fa6e66 100644 --- a/homeassistant/components/notion/.translations/da.json +++ b/homeassistant/components/notion/.translations/da.json @@ -4,7 +4,6 @@ "already_configured": "Dette brugernavn er allerede i brug." }, "error": { - "identifier_exists": "Brugernavn er allerede registreret", "invalid_credentials": "Ugyldigt brugernavn eller adgangskode", "no_devices": "Ingen enheder fundet i konto" }, diff --git a/homeassistant/components/notion/.translations/de.json b/homeassistant/components/notion/.translations/de.json index e11a16458c9..1ccd8c86bdc 100644 --- a/homeassistant/components/notion/.translations/de.json +++ b/homeassistant/components/notion/.translations/de.json @@ -4,7 +4,6 @@ "already_configured": "Dieser Benutzername wird bereits benutzt." }, "error": { - "identifier_exists": "Benutzername bereits registriert", "invalid_credentials": "Ung\u00fcltiger Benutzername oder Passwort", "no_devices": "Keine Ger\u00e4te im Konto gefunden" }, diff --git a/homeassistant/components/notion/.translations/en.json b/homeassistant/components/notion/.translations/en.json index 2476293a216..b729b368c37 100644 --- a/homeassistant/components/notion/.translations/en.json +++ b/homeassistant/components/notion/.translations/en.json @@ -4,7 +4,6 @@ "already_configured": "This username is already in use." }, "error": { - "identifier_exists": "Username already registered", "invalid_credentials": "Invalid username or password", "no_devices": "No devices found in account" }, diff --git a/homeassistant/components/notion/.translations/es-419.json b/homeassistant/components/notion/.translations/es-419.json index 1f4968f24e1..ad2f19b0668 100644 --- a/homeassistant/components/notion/.translations/es-419.json +++ b/homeassistant/components/notion/.translations/es-419.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Nombre de usuario ya registrado", "invalid_credentials": "Nombre de usuario o contrase\u00f1a inv\u00e1lidos", "no_devices": "No se han encontrado dispositivos en la cuenta." }, diff --git a/homeassistant/components/notion/.translations/es.json b/homeassistant/components/notion/.translations/es.json index 08d02bd7493..7293e8f229f 100644 --- a/homeassistant/components/notion/.translations/es.json +++ b/homeassistant/components/notion/.translations/es.json @@ -4,7 +4,6 @@ "already_configured": "Esta nombre de usuario ya est\u00e1 en uso." }, "error": { - "identifier_exists": "Nombre de usuario ya registrado", "invalid_credentials": "Usuario o contrase\u00f1a no v\u00e1lido", "no_devices": "No se han encontrado dispositivos en la cuenta" }, diff --git a/homeassistant/components/notion/.translations/fr.json b/homeassistant/components/notion/.translations/fr.json index 4477c692993..ae24ba70419 100644 --- a/homeassistant/components/notion/.translations/fr.json +++ b/homeassistant/components/notion/.translations/fr.json @@ -4,7 +4,6 @@ "already_configured": "Ce nom d'utilisateur est d\u00e9j\u00e0 utilis\u00e9." }, "error": { - "identifier_exists": "Nom d'utilisateur d\u00e9j\u00e0 enregistr\u00e9", "invalid_credentials": "Nom d'utilisateur ou mot de passe invalide", "no_devices": "Aucun appareil trouv\u00e9 sur le compte" }, diff --git a/homeassistant/components/notion/.translations/hr.json b/homeassistant/components/notion/.translations/hr.json index b20317a236a..93ab9a4bf51 100644 --- a/homeassistant/components/notion/.translations/hr.json +++ b/homeassistant/components/notion/.translations/hr.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Korisni\u010dko ime je ve\u0107 registrirano", "invalid_credentials": "Neispravno korisni\u010dko ime ili lozinka", "no_devices": "Nisu prona\u0111eni ure\u0111aji na ra\u010dunu" }, diff --git a/homeassistant/components/notion/.translations/hu.json b/homeassistant/components/notion/.translations/hu.json index 79878858ddc..285e6c7b485 100644 --- a/homeassistant/components/notion/.translations/hu.json +++ b/homeassistant/components/notion/.translations/hu.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Felhaszn\u00e1l\u00f3n\u00e9v m\u00e1r regisztr\u00e1lva van", "invalid_credentials": "\u00c9rv\u00e9nytelen felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3", "no_devices": "Nem tal\u00e1lhat\u00f3 eszk\u00f6z a fi\u00f3kban" }, diff --git a/homeassistant/components/notion/.translations/it.json b/homeassistant/components/notion/.translations/it.json index 18ad0987aa7..e33b50f1938 100644 --- a/homeassistant/components/notion/.translations/it.json +++ b/homeassistant/components/notion/.translations/it.json @@ -4,7 +4,6 @@ "already_configured": "Questo nome utente \u00e8 gi\u00e0 in uso." }, "error": { - "identifier_exists": "Nome utente gi\u00e0 registrato", "invalid_credentials": "Nome utente o password non validi", "no_devices": "Nessun dispositivo trovato nell'account" }, diff --git a/homeassistant/components/notion/.translations/ko.json b/homeassistant/components/notion/.translations/ko.json index 52c7b6339cb..c848684ab59 100644 --- a/homeassistant/components/notion/.translations/ko.json +++ b/homeassistant/components/notion/.translations/ko.json @@ -4,7 +4,6 @@ "already_configured": "\uc774 \uc0ac\uc6a9\uc790 \uc774\ub984\uc740 \uc774\ubbf8 \uc0ac\uc6a9 \uc911\uc785\ub2c8\ub2e4." }, "error": { - "identifier_exists": "\uc0ac\uc6a9\uc790 \uc774\ub984\uc774 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "invalid_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "no_devices": "\uacc4\uc815\uc5d0 \ub4f1\ub85d\ub41c \uae30\uae30\uac00 \uc874\uc7ac\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4" }, diff --git a/homeassistant/components/notion/.translations/lb.json b/homeassistant/components/notion/.translations/lb.json index bc9fa9633b2..b5d2eabd507 100644 --- a/homeassistant/components/notion/.translations/lb.json +++ b/homeassistant/components/notion/.translations/lb.json @@ -4,7 +4,6 @@ "already_configured": "D\u00ebse Benotzernumm g\u00ebtt scho benotzt." }, "error": { - "identifier_exists": "Benotzernumm ass scho registr\u00e9iert", "invalid_credentials": "Ong\u00ebltege Benotzernumm oder Passwuert", "no_devices": "Keng Apparater am Kont fonnt" }, diff --git a/homeassistant/components/notion/.translations/nl.json b/homeassistant/components/notion/.translations/nl.json index c26fb50e075..f45ea87f972 100644 --- a/homeassistant/components/notion/.translations/nl.json +++ b/homeassistant/components/notion/.translations/nl.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Gebruikersnaam al geregistreerd", "invalid_credentials": "Ongeldige gebruikersnaam of wachtwoord", "no_devices": "Geen apparaten gevonden in account" }, diff --git a/homeassistant/components/notion/.translations/no.json b/homeassistant/components/notion/.translations/no.json index 16105e680c5..302ef3f2b39 100644 --- a/homeassistant/components/notion/.translations/no.json +++ b/homeassistant/components/notion/.translations/no.json @@ -4,7 +4,6 @@ "already_configured": "Dette brukernavnet er allerede i bruk." }, "error": { - "identifier_exists": "Brukernavn er allerede registrert", "invalid_credentials": "Ugyldig brukernavn eller passord", "no_devices": "Ingen enheter funnet i kontoen" }, diff --git a/homeassistant/components/notion/.translations/pl.json b/homeassistant/components/notion/.translations/pl.json index 07facb21e93..fb9ffaad9c0 100644 --- a/homeassistant/components/notion/.translations/pl.json +++ b/homeassistant/components/notion/.translations/pl.json @@ -4,7 +4,6 @@ "already_configured": "Ta nazwa u\u017cytkownika jest ju\u017c w u\u017cyciu." }, "error": { - "identifier_exists": "Nazwa u\u017cytkownika jest ju\u017c zarejestrowana.", "invalid_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o", "no_devices": "Nie znaleziono urz\u0105dze\u0144 na koncie" }, diff --git a/homeassistant/components/notion/.translations/pt-BR.json b/homeassistant/components/notion/.translations/pt-BR.json index 4e81ac03665..5f790c02a40 100644 --- a/homeassistant/components/notion/.translations/pt-BR.json +++ b/homeassistant/components/notion/.translations/pt-BR.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Nome de usu\u00e1rio j\u00e1 registrado", "invalid_credentials": "Usu\u00e1rio ou senha inv\u00e1lidos", "no_devices": "Nenhum dispositivo encontrado na conta" }, diff --git a/homeassistant/components/notion/.translations/ru.json b/homeassistant/components/notion/.translations/ru.json index 6e64ebbe7aa..41627cc6ab0 100644 --- a/homeassistant/components/notion/.translations/ru.json +++ b/homeassistant/components/notion/.translations/ru.json @@ -4,7 +4,6 @@ "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u0443\u0435\u0442\u0441\u044f." }, "error": { - "identifier_exists": "\u0423\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u0430.", "invalid_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c.", "no_devices": "\u041d\u0435\u0442 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e." }, diff --git a/homeassistant/components/notion/.translations/sl.json b/homeassistant/components/notion/.translations/sl.json index c5577f52a24..5abe6164038 100644 --- a/homeassistant/components/notion/.translations/sl.json +++ b/homeassistant/components/notion/.translations/sl.json @@ -4,7 +4,6 @@ "already_configured": "To uporabni\u0161ko ime je \u017ee v uporabi." }, "error": { - "identifier_exists": "Uporabni\u0161ko ime je \u017ee registrirano", "invalid_credentials": "Neveljavno uporabni\u0161ko ime ali geslo", "no_devices": "V ra\u010dunu ni najdene nobene naprave" }, diff --git a/homeassistant/components/notion/.translations/sv.json b/homeassistant/components/notion/.translations/sv.json index 958cc48af28..89648180246 100644 --- a/homeassistant/components/notion/.translations/sv.json +++ b/homeassistant/components/notion/.translations/sv.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "Anv\u00e4ndarnamn \u00e4r redan anv\u00e4nt", "invalid_credentials": "Felaktigt anv\u00e4ndarnamn eller l\u00f6senord", "no_devices": "Inga enheter hittades p\u00e5 kontot" }, diff --git a/homeassistant/components/notion/.translations/zh-Hans.json b/homeassistant/components/notion/.translations/zh-Hans.json index 81d93727956..0e61657f615 100644 --- a/homeassistant/components/notion/.translations/zh-Hans.json +++ b/homeassistant/components/notion/.translations/zh-Hans.json @@ -1,7 +1,6 @@ { "config": { "error": { - "identifier_exists": "\u7528\u6237\u540d\u5df2\u6ce8\u518c", "invalid_credentials": "\u65e0\u6548\u7684\u7528\u6237\u540d\u6216\u5bc6\u7801", "no_devices": "\u5e10\u6237\u4e2d\u627e\u4e0d\u5230\u8bbe\u5907" }, diff --git a/homeassistant/components/notion/.translations/zh-Hant.json b/homeassistant/components/notion/.translations/zh-Hant.json index c426dfa3265..2767c504b78 100644 --- a/homeassistant/components/notion/.translations/zh-Hant.json +++ b/homeassistant/components/notion/.translations/zh-Hant.json @@ -4,7 +4,6 @@ "already_configured": "\u6b64\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u88ab\u4f7f\u7528\u3002" }, "error": { - "identifier_exists": "\u4f7f\u7528\u8005\u540d\u7a31\u5df2\u8a3b\u518a", "invalid_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u7121\u6548", "no_devices": "\u5e33\u865f\u4e2d\u627e\u4e0d\u5230\u4efb\u4f55\u8a2d\u5099" }, diff --git a/homeassistant/components/nut/.translations/ca.json b/homeassistant/components/nut/.translations/ca.json index 33d2268be5b..01a21920cfa 100644 --- a/homeassistant/components/nut/.translations/ca.json +++ b/homeassistant/components/nut/.translations/ca.json @@ -4,6 +4,7 @@ "already_configured": "El dispositiu ja est\u00e0 configurat" }, "error": { + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", "unknown": "Error inesperat" }, "step": { @@ -16,16 +17,19 @@ "port": "Port", "resources": "Recursos", "username": "Nom d'usuari" - } + }, + "title": "No s'ha pogut connectar amb el servidor NUT" } - } + }, + "title": "Eines de xarxa UPS (NUT)" }, "options": { "step": { "init": { "data": { "resources": "Recursos" - } + }, + "description": "Selecciona els recursos del sensor" } } } diff --git a/homeassistant/components/nut/.translations/ko.json b/homeassistant/components/nut/.translations/ko.json new file mode 100644 index 00000000000..f9fa46b6667 --- /dev/null +++ b/homeassistant/components/nut/.translations/ko.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + }, + "error": { + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", + "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "alias": "\ubcc4\uba85", + "host": "\ud638\uc2a4\ud2b8", + "name": "\uc774\ub984", + "password": "\ube44\ubc00\ubc88\ud638", + "port": "\ud3ec\ud2b8", + "resources": "\ub9ac\uc18c\uc2a4", + "username": "\uc0ac\uc6a9\uc790 \uc774\ub984" + }, + "description": "NUT \uc11c\ubc84\uc5d0 UPS \uac00 \uc5ec\ub7ec \uac1c \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294 \uacbd\uc6b0 '\ubcc4\uba85' \uc785\ub825\ub780\uc5d0 \uc870\ud68c\ud560 UPS \uc774\ub984\uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694.", + "title": "NUT \uc11c\ubc84\uc5d0 \uc5f0\uacb0\ud558\uae30" + } + }, + "title": "\ub124\ud2b8\uc6cc\ud06c UPS \ub3c4\uad6c (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\ub9ac\uc18c\uc2a4" + }, + "description": "\uc13c\uc11c \ub9ac\uc18c\uc2a4 \uc120\ud0dd" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/lb.json b/homeassistant/components/nut/.translations/lb.json index 416d5c49aee..7e9ec8ddd97 100644 --- a/homeassistant/components/nut/.translations/lb.json +++ b/homeassistant/components/nut/.translations/lb.json @@ -18,6 +18,7 @@ "resources": "Ressourcen", "username": "Benotzernumm" }, + "description": "Falls m\u00e9i w\u00e9i een UPS mat deem NUT Server verbonnen ass, g\u00e8eff den UPS Numm am 'Alias' Feld un fir ze sichen.", "title": "Mam NUT Server verbannen" } }, diff --git a/homeassistant/components/nut/.translations/pl.json b/homeassistant/components/nut/.translations/pl.json new file mode 100644 index 00000000000..ee9a67b243b --- /dev/null +++ b/homeassistant/components/nut/.translations/pl.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane." + }, + "error": { + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", + "unknown": "Niespodziewany b\u0142\u0105d." + }, + "step": { + "user": { + "data": { + "alias": "Alias", + "host": "Host", + "name": "Nazwa", + "password": "Has\u0142o", + "port": "Port", + "resources": "Zasoby", + "username": "Nazwa u\u017cytkownika" + }, + "description": "Je\u015bli do serwera NUT pod\u0142\u0105czonych jest wiele zasilaczy UPS, wprowad\u017a w polu Alias nazw\u0119 zasilacza UPS, kt\u00f3rego dotyczy zapytanie.", + "title": "Po\u0142\u0105cz z serwerem NUT" + } + }, + "title": "Sieciowe narz\u0119dzia UPS (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Zasoby" + }, + "description": "Wybierz zasoby sensor\u00f3w" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/.translations/ru.json b/homeassistant/components/nut/.translations/ru.json new file mode 100644 index 00000000000..7bc48ec2e3f --- /dev/null +++ b/homeassistant/components/nut/.translations/ru.json @@ -0,0 +1,37 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "alias": "\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0438\u043c", + "host": "\u0425\u043e\u0441\u0442", + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "port": "\u041f\u043e\u0440\u0442", + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b", + "username": "\u041b\u043e\u0433\u0438\u043d" + }, + "description": "\u0415\u0441\u043b\u0438 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 NUT \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043d\u0435\u0441\u043a\u043e\u043b\u044c\u043a\u043e \u0418\u0411\u041f, \u0432\u0432\u0435\u0434\u0438\u0442\u0435 \u0438\u043c\u044f \u0418\u0411\u041f \u0434\u043b\u044f \u0437\u0430\u043f\u0440\u043e\u0441\u0430 \u0432 \u043f\u043e\u043b\u0435 '\u041f\u0441\u0435\u0432\u0434\u043e\u043d\u0438\u043c'.", + "title": "\u041f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u0435 \u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0443 NUT" + } + }, + "title": "Network UPS Tools (NUT)" + }, + "options": { + "step": { + "init": { + "data": { + "resources": "\u0420\u0435\u0441\u0443\u0440\u0441\u044b" + }, + "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0435\u0441\u0443\u0440\u0441\u044b \u0441\u0435\u043d\u0441\u043e\u0440\u043e\u0432" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opentherm_gw/.translations/bg.json b/homeassistant/components/opentherm_gw/.translations/bg.json index cd109579f64..fe9a611f115 100644 --- a/homeassistant/components/opentherm_gw/.translations/bg.json +++ b/homeassistant/components/opentherm_gw/.translations/bg.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "\u041f\u044a\u0442 \u0438\u043b\u0438 URL \u0430\u0434\u0440\u0435\u0441", - "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043d\u0430 \u043f\u043e\u0434\u0430", "id": "ID", - "name": "\u0418\u043c\u0435", - "precision": "\u041f\u0440\u0435\u0446\u0438\u0437\u043d\u043e\u0441\u0442 \u043d\u0430 \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430\u0442\u0430 \u043d\u0430 \u043a\u043b\u0438\u043c\u0430\u0442\u0430" + "name": "\u0418\u043c\u0435" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/ca.json b/homeassistant/components/opentherm_gw/.translations/ca.json index 07567149063..4d39dec3662 100644 --- a/homeassistant/components/opentherm_gw/.translations/ca.json +++ b/homeassistant/components/opentherm_gw/.translations/ca.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Ruta o URL", - "floor_temperature": "Temperatura del pis", "id": "ID", - "name": "Nom", - "precision": "Precisi\u00f3 de la temperatura" + "name": "Nom" }, "title": "Passarel\u00b7la d'OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/.translations/da.json b/homeassistant/components/opentherm_gw/.translations/da.json index 743adb715f6..bbdec393ab0 100644 --- a/homeassistant/components/opentherm_gw/.translations/da.json +++ b/homeassistant/components/opentherm_gw/.translations/da.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Sti eller webadresse", - "floor_temperature": "Gulvklima-temperatur", "id": "Id", - "name": "Navn", - "precision": "Klimatemperatur-pr\u00e6cision" + "name": "Navn" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/de.json b/homeassistant/components/opentherm_gw/.translations/de.json index c29be320d20..92217c51c04 100644 --- a/homeassistant/components/opentherm_gw/.translations/de.json +++ b/homeassistant/components/opentherm_gw/.translations/de.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Pfad oder URL", - "floor_temperature": "Boden-Temperatur", "id": "ID", - "name": "Name", - "precision": "Genauigkeit der Temperatur" + "name": "Name" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/en.json b/homeassistant/components/opentherm_gw/.translations/en.json index a7e143505a8..5ba5d232bfc 100644 --- a/homeassistant/components/opentherm_gw/.translations/en.json +++ b/homeassistant/components/opentherm_gw/.translations/en.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Path or URL", - "floor_temperature": "Floor climate temperature", "id": "ID", - "name": "Name", - "precision": "Climate temperature precision" + "name": "Name" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/es.json b/homeassistant/components/opentherm_gw/.translations/es.json index bb8a8b20f36..9acfbb4bf67 100644 --- a/homeassistant/components/opentherm_gw/.translations/es.json +++ b/homeassistant/components/opentherm_gw/.translations/es.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Ruta o URL", - "floor_temperature": "Temperatura del suelo", "id": "ID", - "name": "Nombre", - "precision": "Precisi\u00f3n de la temperatura clim\u00e1tica" + "name": "Nombre" }, "title": "Gateway OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/.translations/fr.json b/homeassistant/components/opentherm_gw/.translations/fr.json index edde63d62b4..7508612580d 100644 --- a/homeassistant/components/opentherm_gw/.translations/fr.json +++ b/homeassistant/components/opentherm_gw/.translations/fr.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Chemin ou URL", - "floor_temperature": "Temp\u00e9rature du sol", "id": "ID", - "name": "Nom", - "precision": "Pr\u00e9cision de la temp\u00e9rature climatique" + "name": "Nom" }, "title": "Passerelle OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/.translations/hu.json b/homeassistant/components/opentherm_gw/.translations/hu.json index 8a0780581fd..1a00570d324 100644 --- a/homeassistant/components/opentherm_gw/.translations/hu.json +++ b/homeassistant/components/opentherm_gw/.translations/hu.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "El\u00e9r\u00e9si \u00fat vagy URL", - "floor_temperature": "Padl\u00f3 kl\u00edma h\u0151m\u00e9rs\u00e9klete", "id": "ID", - "name": "N\u00e9v", - "precision": "Kl\u00edma h\u0151m\u00e9rs\u00e9klet pontoss\u00e1ga" + "name": "N\u00e9v" }, "title": "OpenTherm \u00e1tj\u00e1r\u00f3" } diff --git a/homeassistant/components/opentherm_gw/.translations/it.json b/homeassistant/components/opentherm_gw/.translations/it.json index 73c3a8db970..c1392fdd077 100644 --- a/homeassistant/components/opentherm_gw/.translations/it.json +++ b/homeassistant/components/opentherm_gw/.translations/it.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Percorso o URL", - "floor_temperature": "Temperatura climatica del pavimento", "id": "ID", - "name": "Nome", - "precision": "Precisione della temperatura climatica" + "name": "Nome" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/ko.json b/homeassistant/components/opentherm_gw/.translations/ko.json index f370427625d..a51efdb197b 100644 --- a/homeassistant/components/opentherm_gw/.translations/ko.json +++ b/homeassistant/components/opentherm_gw/.translations/ko.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "\uacbd\ub85c \ub610\ub294 URL", - "floor_temperature": "\uc2e4\ub0b4\uc628\ub3c4 \uc18c\uc218\uc810 \ubc84\ub9bc", "id": "ID", - "name": "\uc774\ub984", - "precision": "\uc2e4\ub0b4\uc628\ub3c4 \uc815\ubc00\ub3c4" + "name": "\uc774\ub984" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/lb.json b/homeassistant/components/opentherm_gw/.translations/lb.json index 505815dcb4d..3a057ec4e3b 100644 --- a/homeassistant/components/opentherm_gw/.translations/lb.json +++ b/homeassistant/components/opentherm_gw/.translations/lb.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Pfad oder URL", - "floor_temperature": "Buedem Klima Temperatur", "id": "ID", - "name": "Numm", - "precision": "Klima Temperatur Prezisioun" + "name": "Numm" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/nl.json b/homeassistant/components/opentherm_gw/.translations/nl.json index dbed3326b4a..331307d3bca 100644 --- a/homeassistant/components/opentherm_gw/.translations/nl.json +++ b/homeassistant/components/opentherm_gw/.translations/nl.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Pad of URL", - "floor_temperature": "Vloertemperatuur", "id": "ID", - "name": "Naam", - "precision": "Klimaattemperatuur precisie" + "name": "Naam" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/no.json b/homeassistant/components/opentherm_gw/.translations/no.json index d05a8efe168..6b30b85931d 100644 --- a/homeassistant/components/opentherm_gw/.translations/no.json +++ b/homeassistant/components/opentherm_gw/.translations/no.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Bane eller URL-adresse", - "floor_temperature": "Gulv klimatemperatur", "id": "", - "name": "Navn", - "precision": "Klima temperaturpresisjon" + "name": "Navn" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/pl.json b/homeassistant/components/opentherm_gw/.translations/pl.json index 88791781e3f..9d945eac27e 100644 --- a/homeassistant/components/opentherm_gw/.translations/pl.json +++ b/homeassistant/components/opentherm_gw/.translations/pl.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "\u015acie\u017cka lub adres URL", - "floor_temperature": "Zaokr\u0105glanie warto\u015bci w d\u00f3\u0142", "id": "Identyfikator", - "name": "Nazwa", - "precision": "Precyzja temperatury" + "name": "Nazwa" }, "title": "Bramka OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/.translations/ru.json b/homeassistant/components/opentherm_gw/.translations/ru.json index 0719857a7d3..6ad69e23c23 100644 --- a/homeassistant/components/opentherm_gw/.translations/ru.json +++ b/homeassistant/components/opentherm_gw/.translations/ru.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "\u041f\u0443\u0442\u044c \u0438\u043b\u0438 URL-\u0430\u0434\u0440\u0435\u0441", - "floor_temperature": "\u0422\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u0430 \u043f\u043e\u043b\u0430", "id": "ID", - "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435", - "precision": "\u0422\u043e\u0447\u043d\u043e\u0441\u0442\u044c \u0442\u0435\u043c\u043f\u0435\u0440\u0430\u0442\u0443\u0440\u044b" + "name": "\u041d\u0430\u0437\u0432\u0430\u043d\u0438\u0435" }, "title": "OpenTherm" } diff --git a/homeassistant/components/opentherm_gw/.translations/sl.json b/homeassistant/components/opentherm_gw/.translations/sl.json index bba6421ed3d..8eabe6839bb 100644 --- a/homeassistant/components/opentherm_gw/.translations/sl.json +++ b/homeassistant/components/opentherm_gw/.translations/sl.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "Pot ali URL", - "floor_temperature": "Temperatura nadstropja", "id": "ID", - "name": "Ime", - "precision": "Natan\u010dnost temperature" + "name": "Ime" }, "title": "OpenTherm Prehod" } diff --git a/homeassistant/components/opentherm_gw/.translations/sv.json b/homeassistant/components/opentherm_gw/.translations/sv.json index 89ce4d75674..61562b9562f 100644 --- a/homeassistant/components/opentherm_gw/.translations/sv.json +++ b/homeassistant/components/opentherm_gw/.translations/sv.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "S\u00f6kv\u00e4g eller URL", - "floor_temperature": "Golvtemperatur", "id": "ID", - "name": "Namn", - "precision": "Klimatemperaturprecision" + "name": "Namn" }, "title": "OpenTherm Gateway" } diff --git a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json index 0d2842ce767..6c6db948156 100644 --- a/homeassistant/components/opentherm_gw/.translations/zh-Hant.json +++ b/homeassistant/components/opentherm_gw/.translations/zh-Hant.json @@ -10,10 +10,8 @@ "init": { "data": { "device": "\u8def\u5f91\u6216 URL", - "floor_temperature": "\u6a13\u5c64\u6eab\u5ea6", "id": "ID", - "name": "\u540d\u7a31", - "precision": "\u6eab\u63a7\u7cbe\u6e96\u5ea6" + "name": "\u540d\u7a31" }, "title": "OpenTherm \u9598\u9053\u5668" } diff --git a/homeassistant/components/plex/.translations/bg.json b/homeassistant/components/plex/.translations/bg.json index adfdd98ebaf..53d15e1205e 100644 --- a/homeassistant/components/plex/.translations/bg.json +++ b/homeassistant/components/plex/.translations/bg.json @@ -4,7 +4,6 @@ "all_configured": "\u0412\u0441\u0438\u0447\u043a\u0438 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441\u044a\u0440\u0432\u044a\u0440\u0438 \u0432\u0435\u0447\u0435 \u0441\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0438", "already_configured": "\u0422\u043e\u0437\u0438 Plex \u0441\u044a\u0440\u0432\u044a\u0440 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d", "already_in_progress": "Plex \u0441\u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430", - "discovery_no_file": "\u041d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d \u0441\u0442\u0430\u0440 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u0444\u0430\u0439\u043b", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u0430\u043d\u0430\u0442\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0435 \u043d\u0435\u0432\u0430\u043b\u0438\u0434\u043d\u0430", "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u0435\u043d \u0438\u043c\u043f\u043e\u0440\u0442", "token_request_timeout": "\u0418\u0437\u0442\u0435\u0447\u0435 \u0432\u0440\u0435\u043c\u0435\u0442\u043e \u0437\u0430 \u043f\u043e\u043b\u0443\u0447\u0430\u0432\u0430\u043d\u0435 \u043d\u0430 \u043a\u043e\u0434 \u0437\u0430 \u0434\u043e\u0441\u0442\u044a\u043f", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "\u041d\u0435\u0443\u0441\u043f\u0435\u0448\u043d\u0430 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f", "no_servers": "\u041d\u044f\u043c\u0430 \u0441\u044a\u0440\u0432\u044a\u0440\u0438, \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u0441 \u0442\u043e\u0437\u0438 \u0430\u043a\u0430\u0443\u043d\u0442", - "no_token": "\u0412\u044a\u0432\u0435\u0434\u0435\u0442\u0435 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u043e\u043d\u0435\u043d \u043a\u043e\u0434 \u0438\u043b\u0438 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0440\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", "not_found": "Plex \u0441\u044a\u0440\u0432\u044a\u0440\u044a\u0442 \u043d\u0435 \u0435 \u043d\u0430\u043c\u0435\u0440\u0435\u043d" }, "step": { - "manual_setup": { - "data": { - "host": "\u0410\u0434\u0440\u0435\u0441", - "port": "\u041f\u043e\u0440\u0442", - "ssl": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 SSL", - "token": "\u041a\u043e\u0434 (\u0430\u043a\u043e \u0441\u0435 \u0438\u0437\u0438\u0441\u043a\u0432\u0430)", - "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u043a\u0430 \u043d\u0430 SSL \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442" - }, - "title": "Plex \u0441\u044a\u0440\u0432\u044a\u0440" - }, "select_server": { "data": { "server": "\u0421\u044a\u0440\u0432\u044a\u0440" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv.", "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" - }, - "user": { - "data": { - "manual_setup": "\u0420\u044a\u0447\u043d\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", - "token": "Plex \u043a\u043e\u0434" - }, - "description": "\u041f\u0440\u043e\u0434\u044a\u043b\u0436\u0435\u0442\u0435 \u0441 \u043e\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044f\u0442\u0430 \u043d\u0430 plex.tv \u0438\u043b\u0438 \u0440\u044a\u0447\u043d\u043e \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 \u0441\u044a\u0440\u0432\u044a\u0440.", - "title": "\u0421\u0432\u044a\u0440\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 Plex \u0441\u044a\u0440\u0432\u044a\u0440" } }, "title": "Plex" @@ -53,7 +33,6 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u0432\u0441\u0438\u0447\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0438", "use_episode_art": "\u0418\u0437\u043f\u043e\u043b\u0437\u0432\u0430\u043d\u0435 \u043d\u0430 \u043f\u043b\u0430\u043a\u0430\u0442 \u0437\u0430 \u0435\u043f\u0438\u0437\u043e\u0434\u0430" }, "description": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 Plex Media Players" diff --git a/homeassistant/components/plex/.translations/ca.json b/homeassistant/components/plex/.translations/ca.json index d562d62b602..46b7759a04d 100644 --- a/homeassistant/components/plex/.translations/ca.json +++ b/homeassistant/components/plex/.translations/ca.json @@ -4,7 +4,6 @@ "all_configured": "Tots els servidors enlla\u00e7ats ja estan configurats", "already_configured": "Aquest servidor Plex ja est\u00e0 configurat", "already_in_progress": "S\u2019est\u00e0 configurant Plex", - "discovery_no_file": "No s'ha trobat cap fitxer de configuraci\u00f3 heretat", "invalid_import": "La configuraci\u00f3 importada \u00e9s inv\u00e0lida", "non-interactive": "Importaci\u00f3 no interactiva", "token_request_timeout": "S'ha acabat el temps d'espera durant l'obtenci\u00f3 del testimoni.", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Ha fallat l'autoritzaci\u00f3", "no_servers": "No hi ha servidors enlla\u00e7ats amb el compte", - "no_token": "Proporciona un testimoni d'autenticaci\u00f3 o selecciona configuraci\u00f3 manual", "not_found": "No s'ha trobat el servidor Plex" }, "step": { - "manual_setup": { - "data": { - "host": "Amfitri\u00f3", - "port": "Port", - "ssl": "Utilitza SSL", - "token": "Testimoni d'autenticaci\u00f3 (si \u00e9s necessari)", - "verify_ssl": "Verifica el certificat SSL" - }, - "title": "Servidor Plex" - }, "select_server": { "data": { "server": "Servidor" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Continua l'autoritzaci\u00f3 a plex.tv.", "title": "Connexi\u00f3 amb el servidor Plex" - }, - "user": { - "data": { - "manual_setup": "Configuraci\u00f3 manual", - "token": "Testimoni d'autenticaci\u00f3 Plex" - }, - "description": "Introdueix un testimoni d'autenticaci\u00f3 Plex per configurar-ho autom\u00e0ticament.", - "title": "Connexi\u00f3 amb el servidor Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignora els nous usuaris gestionats/compartits", "monitored_users": "Usuaris monitoritzats", - "show_all_controls": "Mostra tots els controls", "use_episode_art": "Utilitza imatges de l'episodi" }, "description": "Opcions dels reproductors multim\u00e8dia Plex" diff --git a/homeassistant/components/plex/.translations/cs.json b/homeassistant/components/plex/.translations/cs.json index e033cd5c514..dc84548da7f 100644 --- a/homeassistant/components/plex/.translations/cs.json +++ b/homeassistant/components/plex/.translations/cs.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "discovery_no_file": "Nebyl nalezen \u017e\u00e1dn\u00fd star\u0161\u00ed konfigura\u010dn\u00ed soubor" - }, "step": { "start_website_auth": { "description": "Pokra\u010dujte v autorizaci na plex.tv.", diff --git a/homeassistant/components/plex/.translations/da.json b/homeassistant/components/plex/.translations/da.json index 9b80373727d..7bfdda60b37 100644 --- a/homeassistant/components/plex/.translations/da.json +++ b/homeassistant/components/plex/.translations/da.json @@ -4,7 +4,6 @@ "all_configured": "Alle linkede servere er allerede konfigureret", "already_configured": "Denne Plex-server er allerede konfigureret", "already_in_progress": "Plex konfigureres", - "discovery_no_file": "Der blev ikke fundet nogen \u00e6ldre konfigurationsfil", "invalid_import": "Importeret konfiguration er ugyldig", "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Timeout ved hentning af token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Godkendelse mislykkedes", "no_servers": "Ingen servere knyttet til konto", - "no_token": "Angiv et token eller v\u00e6lg manuel ops\u00e6tning", "not_found": "Plex-server ikke fundet" }, "step": { - "manual_setup": { - "data": { - "host": "V\u00e6rt", - "port": "Port", - "ssl": "Brug SSL", - "token": "Token (hvis n\u00f8dvendigt)", - "verify_ssl": "Bekr\u00e6ft SSL-certifikat" - }, - "title": "Plex-server" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Forts\u00e6t for at godkende p\u00e5 plex.tv.", "title": "Forbind Plex-server" - }, - "user": { - "data": { - "manual_setup": "Manuel ops\u00e6tning", - "token": "Plex-token" - }, - "description": "Indtast et Plex-token til automatisk ops\u00e6tning eller konfigurerer en server manuelt.", - "title": "Tilslut Plex-server" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignorer nye administrerede/delte brugere", "monitored_users": "Monitorerede brugere", - "show_all_controls": "Vis alle kontrolelementer", "use_episode_art": "Brug episodekunst" }, "description": "Indstillinger for Plex-medieafspillere" diff --git a/homeassistant/components/plex/.translations/de.json b/homeassistant/components/plex/.translations/de.json index ea8f4b60de4..c86ffb97d3a 100644 --- a/homeassistant/components/plex/.translations/de.json +++ b/homeassistant/components/plex/.translations/de.json @@ -4,7 +4,6 @@ "all_configured": "Alle verkn\u00fcpften Server sind bereits konfiguriert", "already_configured": "Dieser Plex-Server ist bereits konfiguriert", "already_in_progress": "Plex wird konfiguriert", - "discovery_no_file": "Es wurde keine alte Konfigurationsdatei gefunden", "invalid_import": "Die importierte Konfiguration ist ung\u00fcltig", "non-interactive": "Nicht interaktiver Import", "token_request_timeout": "Zeit\u00fcberschreitung beim Erhalt des Tokens", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Autorisation fehlgeschlagen", "no_servers": "Keine Server sind mit dem Konto verbunden", - "no_token": "Bereitstellen eines Tokens oder Ausw\u00e4hlen der manuellen Einrichtung", "not_found": "Plex-Server nicht gefunden" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Port", - "ssl": "SSL verwenden", - "token": "Token (falls erforderlich)", - "verify_ssl": "SSL-Zertifikat \u00fcberpr\u00fcfen" - }, - "title": "Plex Server" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Weiter zur Autorisierung unter plex.tv.", "title": "Plex Server verbinden" - }, - "user": { - "data": { - "manual_setup": "Manuelle Einrichtung", - "token": "Plex Token" - }, - "description": "Fahre mit der Autorisierung unter plex.tv fort oder konfiguriere einen Server manuell.", - "title": "Plex Server verbinden" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignorieren neuer verwalteter/freigegebener Benutzer", "monitored_users": "\u00dcberwachte Benutzer", - "show_all_controls": "Alle Steuerelemente anzeigen", "use_episode_art": "Episode-Bilder verwenden" }, "description": "Optionen f\u00fcr Plex-Media-Player" diff --git a/homeassistant/components/plex/.translations/en.json b/homeassistant/components/plex/.translations/en.json index 4567171af77..b9ca9b355ee 100644 --- a/homeassistant/components/plex/.translations/en.json +++ b/homeassistant/components/plex/.translations/en.json @@ -4,7 +4,6 @@ "all_configured": "All linked servers already configured", "already_configured": "This Plex server is already configured", "already_in_progress": "Plex is being configured", - "discovery_no_file": "No legacy configuration file found", "invalid_import": "Imported configuration is invalid", "non-interactive": "Non-interactive import", "token_request_timeout": "Timed out obtaining token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Authorization failed", "no_servers": "No servers linked to account", - "no_token": "Provide a token or select manual setup", "not_found": "Plex server not found" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Port", - "ssl": "Use SSL", - "token": "Token (if required)", - "verify_ssl": "Verify SSL certificate" - }, - "title": "Plex server" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Continue to authorize at plex.tv.", "title": "Connect Plex server" - }, - "user": { - "data": { - "manual_setup": "Manual setup", - "token": "Plex token" - }, - "description": "Continue to authorize at plex.tv or manually configure a server.", - "title": "Connect Plex server" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignore new managed/shared users", "monitored_users": "Monitored users", - "show_all_controls": "Show all controls", "use_episode_art": "Use episode art" }, "description": "Options for Plex Media Players" diff --git a/homeassistant/components/plex/.translations/es-419.json b/homeassistant/components/plex/.translations/es-419.json index 2fc98a70ead..0546fcd7adf 100644 --- a/homeassistant/components/plex/.translations/es-419.json +++ b/homeassistant/components/plex/.translations/es-419.json @@ -11,33 +11,15 @@ "error": { "faulty_credentials": "Autorizaci\u00f3n fallida", "no_servers": "No hay servidores vinculados a la cuenta", - "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", "not_found": "Servidor Plex no encontrado" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Puerto", - "ssl": "Usar SSL", - "token": "Token (si es necesario)", - "verify_ssl": "Verificar el certificado SSL" - }, - "title": "Servidor Plex" - }, "select_server": { "data": { "server": "Servidor" }, "description": "M\u00faltiples servidores disponibles, seleccione uno:", "title": "Seleccionar servidor Plex" - }, - "user": { - "data": { - "manual_setup": "Configuraci\u00f3n manual", - "token": "Token Plex" - }, - "title": "Conectar servidor Plex" } }, "title": "Plex" @@ -45,9 +27,6 @@ "options": { "step": { "plex_mp_settings": { - "data": { - "show_all_controls": "Mostrar todos los controles" - }, "description": "Opciones para reproductores multimedia Plex" } } diff --git a/homeassistant/components/plex/.translations/es.json b/homeassistant/components/plex/.translations/es.json index 24127a7332c..3de562db21d 100644 --- a/homeassistant/components/plex/.translations/es.json +++ b/homeassistant/components/plex/.translations/es.json @@ -4,7 +4,6 @@ "all_configured": "Todos los servidores vinculados ya configurados", "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", - "discovery_no_file": "No se ha encontrado ning\u00fan archivo de configuraci\u00f3n antiguo", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Tiempo de espera agotado para la obtenci\u00f3n del token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Error en la autorizaci\u00f3n", "no_servers": "No hay servidores vinculados a la cuenta", - "no_token": "Proporcione un token o seleccione la configuraci\u00f3n manual", "not_found": "No se ha encontrado el servidor Plex" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Puerto", - "ssl": "Usar SSL", - "token": "Token (es necesario)", - "verify_ssl": "Verificar certificado SSL" - }, - "title": "Servidor Plex" - }, "select_server": { "data": { "server": "Servidor" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Contin\u00fae en plex.tv para autorizar", "title": "Conectar servidor Plex" - }, - "user": { - "data": { - "manual_setup": "Configuraci\u00f3n manual", - "token": "Token Plex" - }, - "description": "Introduzca un token Plex para la configuraci\u00f3n autom\u00e1tica o configure manualmente un servidor.", - "title": "Conectar servidor Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", "monitored_users": "Usuarios monitorizados", - "show_all_controls": "Mostrar todos los controles", "use_episode_art": "Usar el arte de episodios" }, "description": "Opciones para reproductores multimedia Plex" diff --git a/homeassistant/components/plex/.translations/fr.json b/homeassistant/components/plex/.translations/fr.json index 4c1af21aaf1..354a5eaecf9 100644 --- a/homeassistant/components/plex/.translations/fr.json +++ b/homeassistant/components/plex/.translations/fr.json @@ -4,7 +4,6 @@ "all_configured": "Tous les serveurs li\u00e9s sont d\u00e9j\u00e0 configur\u00e9s", "already_configured": "Ce serveur Plex est d\u00e9j\u00e0 configur\u00e9", "already_in_progress": "Plex en cours de configuration", - "discovery_no_file": "Aucun fichier de configuration h\u00e9rit\u00e9 trouv\u00e9", "invalid_import": "La configuration import\u00e9e est invalide", "non-interactive": "Importation non interactive", "token_request_timeout": "D\u00e9lai d'obtention du jeton", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "L'autorisation \u00e0 \u00e9chou\u00e9e", "no_servers": "Aucun serveur li\u00e9 au compte", - "no_token": "Fournir un jeton ou s\u00e9lectionner l'installation manuelle", "not_found": "Serveur Plex introuvable" }, "step": { - "manual_setup": { - "data": { - "host": "H\u00f4te", - "port": "Port", - "ssl": "Utiliser SSL", - "token": "Jeton (si n\u00e9cessaire)", - "verify_ssl": "V\u00e9rifier le certificat SSL" - }, - "title": "Serveur Plex" - }, "select_server": { "data": { "server": "Serveur" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Continuer d'autoriser sur plex.tv.", "title": "Connecter un serveur Plex" - }, - "user": { - "data": { - "manual_setup": "Installation manuelle", - "token": "Jeton plex" - }, - "description": "Continuez pour autoriser plex.tv ou configurez manuellement un serveur.", - "title": "Connecter un serveur Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignorer les nouveaux utilisateurs g\u00e9r\u00e9s/partag\u00e9s", "monitored_users": "Utilisateurs surveill\u00e9s", - "show_all_controls": "Afficher tous les contr\u00f4les", "use_episode_art": "Utiliser l'art de l'\u00e9pisode" }, "description": "Options pour lecteurs multim\u00e9dia Plex" diff --git a/homeassistant/components/plex/.translations/hu.json b/homeassistant/components/plex/.translations/hu.json index 4712fb37b55..c59e31a3b95 100644 --- a/homeassistant/components/plex/.translations/hu.json +++ b/homeassistant/components/plex/.translations/hu.json @@ -4,7 +4,6 @@ "all_configured": "Az \u00f6sszes \u00f6sszekapcsolt szerver m\u00e1r konfigur\u00e1lva van", "already_configured": "Ez a Plex szerver m\u00e1r konfigur\u00e1lva van", "already_in_progress": "A Plex konfigur\u00e1l\u00e1sa folyamatban van", - "discovery_no_file": "Nem tal\u00e1lhat\u00f3 r\u00e9gi konfigur\u00e1ci\u00f3s f\u00e1jl", "invalid_import": "Az import\u00e1lt konfigur\u00e1ci\u00f3 \u00e9rv\u00e9nytelen", "non-interactive": "Nem interakt\u00edv import\u00e1l\u00e1s", "token_request_timeout": "Token k\u00e9r\u00e9sre sz\u00e1nt id\u0151 lej\u00e1rt", @@ -16,12 +15,6 @@ "not_found": "A Plex szerver nem tal\u00e1lhat\u00f3" }, "step": { - "manual_setup": { - "data": { - "host": "Kiszolg\u00e1l\u00f3", - "port": "Port" - } - }, "select_server": { "data": { "server": "szerver" @@ -32,13 +25,6 @@ "start_website_auth": { "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen.", "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" - }, - "user": { - "data": { - "token": "Plex token" - }, - "description": "Folytassa az enged\u00e9lyez\u00e9st a plex.tv webhelyen, vagy manu\u00e1lisan konfigur\u00e1lja a szervert.", - "title": "Plex-kiszolg\u00e1l\u00f3 csatlakoztat\u00e1sa" } }, "title": "Plex" @@ -47,7 +33,6 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Az \u00f6sszes vez\u00e9rl\u0151 megjelen\u00edt\u00e9se", "use_episode_art": "Haszn\u00e1lja az epiz\u00f3d bor\u00edt\u00f3j\u00e1t" }, "description": "Plex media lej\u00e1tsz\u00f3k be\u00e1ll\u00edt\u00e1sai" diff --git a/homeassistant/components/plex/.translations/it.json b/homeassistant/components/plex/.translations/it.json index e5ff4e01dc0..bb48d95bc51 100644 --- a/homeassistant/components/plex/.translations/it.json +++ b/homeassistant/components/plex/.translations/it.json @@ -4,7 +4,6 @@ "all_configured": "Tutti i server collegati sono gi\u00e0 configurati", "already_configured": "Questo server Plex \u00e8 gi\u00e0 configurato", "already_in_progress": "Plex \u00e8 in fase di configurazione", - "discovery_no_file": "Non \u00e8 stato trovato nessun file di configurazione da sostituire", "invalid_import": "La configurazione importata non \u00e8 valida", "non-interactive": "Importazione non interattiva", "token_request_timeout": "Timeout per l'ottenimento del token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Autorizzazione non riuscita", "no_servers": "Nessun server collegato all'account", - "no_token": "Fornire un token o selezionare la configurazione manuale", "not_found": "Server Plex non trovato" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Porta", - "ssl": "Usa SSL", - "token": "Token (se richiesto)", - "verify_ssl": "Verificare il certificato SSL" - }, - "title": "Server Plex" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Continuare ad autorizzare su plex.tv.", "title": "Collegare il server Plex" - }, - "user": { - "data": { - "manual_setup": "Configurazione manuale", - "token": "Token Plex" - }, - "description": "Continuare ad autorizzare plex.tv o configurare manualmente un server.", - "title": "Collegare il server Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignora nuovi utenti gestiti/condivisi", "monitored_users": "Utenti monitorati", - "show_all_controls": "Mostra tutti i controlli", "use_episode_art": "Usa la grafica dell'episodio" }, "description": "Opzioni per i lettori multimediali Plex" diff --git a/homeassistant/components/plex/.translations/ko.json b/homeassistant/components/plex/.translations/ko.json index 3292fab0a8e..5cb49836f4d 100644 --- a/homeassistant/components/plex/.translations/ko.json +++ b/homeassistant/components/plex/.translations/ko.json @@ -4,7 +4,6 @@ "all_configured": "\uc774\ubbf8 \uad6c\uc131\ub41c \ubaa8\ub4e0 \uc5f0\uacb0\ub41c \uc11c\ubc84", "already_configured": "\uc774 Plex \uc11c\ubc84\ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "already_in_progress": "Plex \ub97c \uad6c\uc131 \uc911\uc785\ub2c8\ub2e4", - "discovery_no_file": "\ub808\uac70\uc2dc \uad6c\uc131 \ud30c\uc77c\uc744 \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", "invalid_import": "\uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uc774 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4", "non-interactive": "\ube44 \ub300\ud654\ud615 \uac00\uc838\uc624\uae30", "token_request_timeout": "\ud1a0\ud070 \ud68d\ub4dd \uc2dc\uac04\uc774 \ucd08\uacfc\ud588\uc2b5\ub2c8\ub2e4", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "\uc778\uc99d\uc5d0 \uc2e4\ud328\ud588\uc2b5\ub2c8\ub2e4", "no_servers": "\uacc4\uc815\uc5d0 \uc5f0\uacb0\ub41c \uc11c\ubc84\uac00 \uc5c6\uc2b5\ub2c8\ub2e4", - "no_token": "\ud1a0\ud070\uc744 \uc785\ub825\ud558\uac70\ub098 \uc218\ub3d9 \uc124\uc815\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694", "not_found": "Plex \uc11c\ubc84\ub97c \ucc3e\uc744 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" }, "step": { - "manual_setup": { - "data": { - "host": "\ud638\uc2a4\ud2b8", - "port": "\ud3ec\ud2b8", - "ssl": "SSL \uc0ac\uc6a9", - "token": "\ud1a0\ud070 (\ud544\uc694\ud55c \uacbd\uc6b0)", - "verify_ssl": "SSL \uc778\uc99d\uc11c \uac80\uc99d" - }, - "title": "Plex \uc11c\ubc84" - }, "select_server": { "data": { "server": "\uc11c\ubc84" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud574\uc8fc\uc138\uc694.", "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" - }, - "user": { - "data": { - "manual_setup": "\uc218\ub3d9 \uc124\uc815", - "token": "Plex \ud1a0\ud070" - }, - "description": "plex.tv \uc5d0\uc11c \uc778\uc99d\uc744 \uc9c4\ud589\ud558\uac70\ub098 \uc11c\ubc84\ub97c \uc218\ub3d9\uc73c\ub85c \uc124\uc815\ud574\uc8fc\uc138\uc694.", - "title": "Plex \uc11c\ubc84 \uc5f0\uacb0" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "\uc0c8\ub85c\uc6b4 \uad00\ub9ac/\uacf5\uc720 \uc0ac\uc6a9\uc790 \ubb34\uc2dc", "monitored_users": "\ubaa8\ub2c8\ud130\ub9c1\ub418\ub294 \uc0ac\uc6a9\uc790", - "show_all_controls": "\ubaa8\ub4e0 \ucee8\ud2b8\ub864 \ud45c\uc2dc\ud558\uae30", "use_episode_art": "\uc5d0\ud53c\uc18c\ub4dc \uc544\ud2b8 \uc0ac\uc6a9" }, "description": "Plex \ubbf8\ub514\uc5b4 \ud50c\ub808\uc774\uc5b4 \uc635\uc158" diff --git a/homeassistant/components/plex/.translations/lb.json b/homeassistant/components/plex/.translations/lb.json index 6ed9d372fc1..c8b910b6dc5 100644 --- a/homeassistant/components/plex/.translations/lb.json +++ b/homeassistant/components/plex/.translations/lb.json @@ -4,7 +4,6 @@ "all_configured": "All verbonne Server sinn scho konfigur\u00e9iert", "already_configured": "D\u00ebse Plex Server ass scho konfigur\u00e9iert", "already_in_progress": "Plex g\u00ebtt konfigur\u00e9iert", - "discovery_no_file": "Kee Konfiguratioun Fichier am ale Format fonnt.", "invalid_import": "D\u00e9i importiert Konfiguratioun ass ong\u00eblteg", "non-interactive": "Net interaktiven Import", "token_request_timeout": "Z\u00e4it Iwwerschreidung beim kr\u00e9ien vum Jeton", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Feeler beider Autorisatioun", "no_servers": "Kee Server as mam Kont verbonnen", - "no_token": "Gitt en Token un oder wielt manuelle Setup", "not_found": "Kee Plex Server fonnt" }, "step": { - "manual_setup": { - "data": { - "host": "Apparat", - "port": "Port", - "ssl": "SSL benotzen", - "token": "Jeton (falls n\u00e9ideg)", - "verify_ssl": "SSL Zertifikat iwwerpr\u00e9iwen" - }, - "title": "Plex Server" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Weiderfueren op plex.tv fir d'Autorisatioun.", "title": "Plex Server verbannen" - }, - "user": { - "data": { - "manual_setup": "Manuell Konfiguratioun", - "token": "Jeton fir de Plex" - }, - "description": "Gitt een Jeton fir de Plex un fir eng automatesch Konfiguratioun", - "title": "Plex Server verbannen" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Nei verwalt / gedeelt Benotzer ignor\u00e9ieren", "monitored_users": "Iwwerwaachte Benotzer", - "show_all_controls": "Weis all Kontrollen", "use_episode_art": "Benotz Biller vun der Episode" }, "description": "Optioune fir Plex Medie Spiller" diff --git a/homeassistant/components/plex/.translations/lv.json b/homeassistant/components/plex/.translations/lv.json index 23cda3fce4b..39d4b3d7096 100644 --- a/homeassistant/components/plex/.translations/lv.json +++ b/homeassistant/components/plex/.translations/lv.json @@ -7,14 +7,6 @@ "not_found": "Plex serveris nav atrasts" }, "step": { - "manual_setup": { - "data": { - "port": "Ports", - "ssl": "Izmantot SSL", - "verify_ssl": "P\u0101rbaud\u012bt SSL sertifik\u0101tu" - }, - "title": "Plex serveris" - }, "select_server": { "data": { "server": "Serveris" diff --git a/homeassistant/components/plex/.translations/nl.json b/homeassistant/components/plex/.translations/nl.json index 515ee8798c7..79ae6506d86 100644 --- a/homeassistant/components/plex/.translations/nl.json +++ b/homeassistant/components/plex/.translations/nl.json @@ -4,7 +4,6 @@ "all_configured": "Alle gekoppelde servers zijn al geconfigureerd", "already_configured": "Deze Plex-server is al geconfigureerd", "already_in_progress": "Plex wordt geconfigureerd", - "discovery_no_file": "Geen legacy configuratiebestand gevonden", "invalid_import": "Ge\u00efmporteerde configuratie is ongeldig", "non-interactive": "Niet-interactieve import", "token_request_timeout": "Time-out verkrijgen van token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Autorisatie mislukt", "no_servers": "Geen servers gekoppeld aan account", - "no_token": "Geef een token op of selecteer handmatige installatie", "not_found": "Plex-server niet gevonden" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Poort", - "ssl": "Gebruik SSL", - "token": "Token (indien nodig)", - "verify_ssl": "Controleer SSL-certificaat" - }, - "title": "Plex server" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Ga verder met autoriseren bij plex.tv.", "title": "Verbind de Plex server" - }, - "user": { - "data": { - "manual_setup": "Handmatig setup", - "token": "Plex token" - }, - "description": "Ga verder met autoriseren bij plex.tv of configureer een server.", - "title": "Verbind de Plex server" } }, "title": "Plex" @@ -53,7 +33,6 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Toon alle bedieningselementen", "use_episode_art": "Gebruik aflevering kunst" }, "description": "Opties voor Plex-mediaspelers" diff --git a/homeassistant/components/plex/.translations/no.json b/homeassistant/components/plex/.translations/no.json index 29d43cb8275..be76411d8ac 100644 --- a/homeassistant/components/plex/.translations/no.json +++ b/homeassistant/components/plex/.translations/no.json @@ -4,7 +4,6 @@ "all_configured": "Alle knyttet servere som allerede er konfigurert", "already_configured": "Denne Plex-serveren er allerede konfigurert", "already_in_progress": "Plex blir konfigurert", - "discovery_no_file": "Ingen eldre konfigurasjonsfil funnet", "invalid_import": "Den importerte konfigurasjonen er ugyldig", "non-interactive": "Ikke-interaktiv import", "token_request_timeout": "Tidsavbrudd ved innhenting av token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Autorisasjonen mislyktes", "no_servers": "Ingen servere koblet til kontoen", - "no_token": "Angi et token eller velg manuelt oppsett", "not_found": "Plex-server ikke funnet" }, "step": { - "manual_setup": { - "data": { - "host": "Vert", - "port": "", - "ssl": "Bruk SSL", - "token": "Token (hvis n\u00f8dvendig)", - "verify_ssl": "Verifisere SSL-sertifikat" - }, - "title": "Plex-server" - }, "select_server": { "data": { "server": "" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv.", "title": "Koble til Plex-server" - }, - "user": { - "data": { - "manual_setup": "Manuelt oppsett", - "token": "Plex token" - }, - "description": "Fortsett \u00e5 autorisere p\u00e5 plex.tv eller manuelt konfigurere en server.", - "title": "Koble til Plex-server" } }, "title": "" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignorer nye administrerte/delte brukere", "monitored_users": "Overv\u00e5kede brukere", - "show_all_controls": "Vis alle kontroller", "use_episode_art": "Bruk episode bilde" }, "description": "Alternativer for Plex Media Players" diff --git a/homeassistant/components/plex/.translations/pl.json b/homeassistant/components/plex/.translations/pl.json index 6531b552000..8b21562a87e 100644 --- a/homeassistant/components/plex/.translations/pl.json +++ b/homeassistant/components/plex/.translations/pl.json @@ -4,7 +4,6 @@ "all_configured": "Wszystkie znalezione serwery s\u0105 ju\u017c skonfigurowane.", "already_configured": "Ten serwer Plex jest ju\u017c skonfigurowany.", "already_in_progress": "Plex jest konfigurowany", - "discovery_no_file": "Nie znaleziono pliku konfiguracyjnego", "invalid_import": "Zaimportowana konfiguracja jest nieprawid\u0142owa", "non-interactive": "Nieinteraktywny import", "token_request_timeout": "Przekroczono limit czasu na uzyskanie tokena.", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Autoryzacja nie powiod\u0142a si\u0119", "no_servers": "Brak serwer\u00f3w po\u0142\u0105czonych z kontem", - "no_token": "Wprowad\u017a token lub wybierz konfiguracj\u0119 r\u0119czn\u0105", "not_found": "Nie znaleziono serwera Plex" }, "step": { - "manual_setup": { - "data": { - "host": "Host", - "port": "Port", - "ssl": "U\u017cyj SSL", - "token": "Token (je\u015bli wymagany)", - "verify_ssl": "Weryfikacja certyfikatu SSL" - }, - "title": "Serwer Plex" - }, "select_server": { "data": { "server": "Serwer" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Kontynuuj, by dokona\u0107 autoryzacji w plex.tv.", "title": "Po\u0142\u0105cz z serwerem Plex" - }, - "user": { - "data": { - "manual_setup": "Konfiguracja r\u0119czna", - "token": "Token Plex" - }, - "description": "Wprowad\u017a token Plex do automatycznej konfiguracji.", - "title": "Po\u0142\u0105cz z serwerem Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignoruj nowych zarz\u0105dzanych/wsp\u00f3\u0142dzielonych u\u017cytkownik\u00f3w", "monitored_users": "Monitorowani u\u017cytkownicy", - "show_all_controls": "Poka\u017c wszystkie elementy steruj\u0105ce", "use_episode_art": "U\u017cyj grafiki odcinka" }, "description": "Opcje dla odtwarzaczy multimedialnych Plex" diff --git a/homeassistant/components/plex/.translations/pt-BR.json b/homeassistant/components/plex/.translations/pt-BR.json index be97c7fdcb7..0248fc94857 100644 --- a/homeassistant/components/plex/.translations/pt-BR.json +++ b/homeassistant/components/plex/.translations/pt-BR.json @@ -8,7 +8,6 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Mostrar todos os controles", "use_episode_art": "Usar arte epis\u00f3dio" }, "description": "Op\u00e7\u00f5es para Plex Media Players" diff --git a/homeassistant/components/plex/.translations/ru.json b/homeassistant/components/plex/.translations/ru.json index 2da10b1e8c4..851a2f16ae1 100644 --- a/homeassistant/components/plex/.translations/ru.json +++ b/homeassistant/components/plex/.translations/ru.json @@ -4,7 +4,6 @@ "all_configured": "\u0412\u0441\u0435 \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0435 \u0441\u0435\u0440\u0432\u0435\u0440\u044b \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u044b.", "already_configured": "\u042d\u0442\u043e\u0442 \u0441\u0435\u0440\u0432\u0435\u0440 Plex \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d.", "already_in_progress": "\u0412\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430.", - "discovery_no_file": "\u0421\u0442\u0430\u0440\u044b\u0439 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u044b\u0439 \u0444\u0430\u0439\u043b \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d.", "invalid_import": "\u0418\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u0430\u044f \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u043d\u0435\u0432\u0435\u0440\u043d\u0430.", "non-interactive": "\u041d\u0435\u0438\u043d\u0442\u0435\u0440\u0430\u043a\u0442\u0438\u0432\u043d\u044b\u0439 \u0438\u043c\u043f\u043e\u0440\u0442.", "token_request_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0442\u043e\u043a\u0435\u043d\u0430.", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", "no_servers": "\u041d\u0435\u0442 \u0441\u0435\u0440\u0432\u0435\u0440\u043e\u0432, \u0441\u0432\u044f\u0437\u0430\u043d\u043d\u044b\u0445 \u0441 \u0443\u0447\u0451\u0442\u043d\u043e\u0439 \u0437\u0430\u043f\u0438\u0441\u044c\u044e.", - "no_token": "\u0412\u0432\u0435\u0434\u0438\u0442\u0435 \u0442\u043e\u043a\u0435\u043d \u0438\u043b\u0438 \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0440\u0443\u0447\u043d\u0443\u044e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443.", "not_found": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d." }, "step": { - "manual_setup": { - "data": { - "host": "\u0425\u043e\u0441\u0442", - "port": "\u041f\u043e\u0440\u0442", - "ssl": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c SSL", - "token": "\u0422\u043e\u043a\u0435\u043d (\u0435\u0441\u043b\u0438 \u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f)", - "verify_ssl": "\u041f\u0440\u043e\u0432\u0435\u0440\u044f\u0442\u044c \u0441\u0435\u0440\u0442\u0438\u0444\u0438\u043a\u0430\u0442 SSL" - }, - "title": "\u0421\u0435\u0440\u0432\u0435\u0440 Plex" - }, "select_server": { "data": { "server": "\u0421\u0435\u0440\u0432\u0435\u0440" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "\u041f\u0440\u043e\u0439\u0434\u0438\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv.", "title": "Plex" - }, - "user": { - "data": { - "manual_setup": "\u0420\u0443\u0447\u043d\u0430\u044f \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430", - "token": "\u0422\u043e\u043a\u0435\u043d" - }, - "description": "\u041f\u0440\u043e\u0434\u043e\u043b\u0436\u0430\u0439\u0442\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u044e \u043d\u0430 plex.tv \u0438\u043b\u0438 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u0442\u0435 \u0441\u0435\u0440\u0432\u0435\u0440 \u0432\u0440\u0443\u0447\u043d\u0443\u044e.", - "title": "Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "\u0418\u0433\u043d\u043e\u0440\u0438\u0440\u043e\u0432\u0430\u0442\u044c \u043d\u043e\u0432\u044b\u0445 \u0443\u043f\u0440\u0430\u0432\u043b\u044f\u0435\u043c\u044b\u0445/\u043e\u0431\u0449\u0438\u0445 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0435\u0439", "monitored_users": "\u041e\u0442\u0441\u043b\u0435\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u0438", - "show_all_controls": "\u041f\u043e\u043a\u0430\u0437\u044b\u0432\u0430\u0442\u044c \u0432\u0441\u0435 \u044d\u043b\u0435\u043c\u0435\u043d\u0442\u044b \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f", "use_episode_art": "\u0418\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u043e\u0431\u043b\u043e\u0436\u043a\u0438 \u044d\u043f\u0438\u0437\u043e\u0434\u043e\u0432" }, "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b" diff --git a/homeassistant/components/plex/.translations/sl.json b/homeassistant/components/plex/.translations/sl.json index 40ba84b9f41..20ad2ca0a02 100644 --- a/homeassistant/components/plex/.translations/sl.json +++ b/homeassistant/components/plex/.translations/sl.json @@ -4,7 +4,6 @@ "all_configured": "Vsi povezani stre\u017eniki so \u017ee konfigurirani", "already_configured": "Ta stre\u017enik Plex je \u017ee konfiguriran", "already_in_progress": "Plex se konfigurira", - "discovery_no_file": "Podedovane konfiguracijske datoteke ni bilo", "invalid_import": "Uvo\u017eena konfiguracija ni veljavna", "non-interactive": "Neinteraktivni uvoz", "token_request_timeout": "Potekla \u010dasovna omejitev za pridobitev \u017eetona", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Avtorizacija ni uspela", "no_servers": "Ni stre\u017enikov povezanih z ra\u010dunom", - "no_token": "Vnesite \u017eeton ali izberite ro\u010dno nastavitev", "not_found": "Plex stre\u017enika ni mogo\u010de najti" }, "step": { - "manual_setup": { - "data": { - "host": "Gostitelj", - "port": "Vrata", - "ssl": "Uporaba SSL", - "token": "\u017deton (po potrebi)", - "verify_ssl": "Preverite SSL potrdilo" - }, - "title": "Plex stre\u017enik" - }, "select_server": { "data": { "server": "Stre\u017enik" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Nadaljujte z avtorizacijo na plex.tv.", "title": "Pove\u017eite stre\u017enik Plex" - }, - "user": { - "data": { - "manual_setup": "Ro\u010dna nastavitev", - "token": "Plex \u017eeton" - }, - "description": "Nadaljujte z avtorizacijo na plex.tv ali ro\u010dno konfigurirajte stre\u017enik.", - "title": "Pove\u017eite stre\u017enik Plex" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "Ignorirajte nove upravljane/deljene uporabnike", "monitored_users": "Nadzorovani uporabniki", - "show_all_controls": "Poka\u017ei vse kontrole", "use_episode_art": "Uporabi naslovno sliko epizode" }, "description": "Mo\u017enosti za predvajalnike Plex" diff --git a/homeassistant/components/plex/.translations/sv.json b/homeassistant/components/plex/.translations/sv.json index 25152e9dc81..42afc3eeaa9 100644 --- a/homeassistant/components/plex/.translations/sv.json +++ b/homeassistant/components/plex/.translations/sv.json @@ -4,7 +4,6 @@ "all_configured": "Alla l\u00e4nkade servrar har redan konfigurerats", "already_configured": "Denna Plex-server \u00e4r redan konfigurerad", "already_in_progress": "Plex konfigureras", - "discovery_no_file": "Ingen \u00e4ldre konfigurationsfil hittades", "invalid_import": "Importerad konfiguration \u00e4r ogiltig", "non-interactive": "Icke-interaktiv import", "token_request_timeout": "Timeout att erh\u00e5lla token", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "Auktoriseringen misslyckades", "no_servers": "Inga servrar l\u00e4nkade till konto", - "no_token": "Ange en token eller v\u00e4lj manuell inst\u00e4llning", "not_found": "Plex-server hittades inte" }, "step": { - "manual_setup": { - "data": { - "host": "V\u00e4rd", - "port": "Port", - "ssl": "Anv\u00e4nd SSL", - "token": "Token (om det beh\u00f6vs)", - "verify_ssl": "Verifiera SSL-certifikat" - }, - "title": "Plex-server" - }, "select_server": { "data": { "server": "Server" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv.", "title": "Anslut Plex-servern" - }, - "user": { - "data": { - "manual_setup": "Manuell inst\u00e4llning", - "token": "Plex-token" - }, - "description": "Forts\u00e4tt att auktorisera p\u00e5 plex.tv eller konfigurera en server manuellt.", - "title": "Anslut Plex-servern" } }, "title": "Plex" @@ -53,7 +33,6 @@ "step": { "plex_mp_settings": { "data": { - "show_all_controls": "Visa alla kontroller", "use_episode_art": "Anv\u00e4nd avsnittsbild" }, "description": "Alternativ f\u00f6r Plex-mediaspelare" diff --git a/homeassistant/components/plex/.translations/zh-Hant.json b/homeassistant/components/plex/.translations/zh-Hant.json index 436333b0a79..6d46b8bc154 100644 --- a/homeassistant/components/plex/.translations/zh-Hant.json +++ b/homeassistant/components/plex/.translations/zh-Hant.json @@ -4,7 +4,6 @@ "all_configured": "\u6240\u6709\u7d81\u5b9a\u4f3a\u670d\u5668\u90fd\u5df2\u8a2d\u5b9a\u5b8c\u6210", "already_configured": "Plex \u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "Plex \u5df2\u7d93\u8a2d\u5b9a", - "discovery_no_file": "\u627e\u4e0d\u5230\u820a\u7248\u8a2d\u5b9a\u6a94\u6848", "invalid_import": "\u532f\u5165\u4e4b\u8a2d\u5b9a\u7121\u6548", "non-interactive": "\u7121\u4e92\u52d5\u532f\u5165", "token_request_timeout": "\u53d6\u5f97\u5bc6\u9470\u903e\u6642", @@ -13,20 +12,9 @@ "error": { "faulty_credentials": "\u9a57\u8b49\u5931\u6557", "no_servers": "\u6b64\u5e33\u865f\u672a\u7d81\u5b9a\u4f3a\u670d\u5668", - "no_token": "\u63d0\u4f9b\u5bc6\u9470\u6216\u9078\u64c7\u624b\u52d5\u8a2d\u5b9a", "not_found": "\u627e\u4e0d\u5230 Plex \u4f3a\u670d\u5668" }, "step": { - "manual_setup": { - "data": { - "host": "\u4e3b\u6a5f\u7aef", - "port": "\u901a\u8a0a\u57e0", - "ssl": "\u4f7f\u7528 SSL", - "token": "\u5bc6\u9470\uff08\u5982\u679c\u9700\u8981\uff09", - "verify_ssl": "\u78ba\u8a8d SSL \u8a8d\u8b49" - }, - "title": "Plex \u4f3a\u670d\u5668" - }, "select_server": { "data": { "server": "\u4f3a\u670d\u5668" @@ -37,14 +25,6 @@ "start_website_auth": { "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u3002", "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" - }, - "user": { - "data": { - "manual_setup": "\u624b\u52d5\u8a2d\u5b9a", - "token": "Plex \u5bc6\u9470" - }, - "description": "\u7e7c\u7e8c\u65bc Plex.tv \u9032\u884c\u8a8d\u8b49\u6216\u624b\u52d5\u8a2d\u5b9a\u4f3a\u670d\u5668\u3002", - "title": "\u9023\u7dda\u81f3 Plex \u4f3a\u670d\u5668" } }, "title": "Plex" @@ -55,7 +35,6 @@ "data": { "ignore_new_shared_users": "\u5ffd\u7565\u65b0\u589e\u7ba1\u7406/\u5206\u4eab\u4f7f\u7528\u8005", "monitored_users": "\u5df2\u76e3\u63a7\u4f7f\u7528\u8005", - "show_all_controls": "\u986f\u793a\u6240\u6709\u63a7\u5236", "use_episode_art": "\u4f7f\u7528\u5f71\u96c6\u5287\u7167" }, "description": "Plex \u64ad\u653e\u5668\u9078\u9805" diff --git a/homeassistant/components/rachio/.translations/pl.json b/homeassistant/components/rachio/.translations/pl.json index b186a764cd1..3c07ea850c0 100644 --- a/homeassistant/components/rachio/.translations/pl.json +++ b/homeassistant/components/rachio/.translations/pl.json @@ -14,7 +14,7 @@ "api_key": "Klucz API dla konta Rachio." }, "description": "B\u0119dziesz potrzebowa\u0142 klucza API ze strony https://app.rach.io/. Wybierz 'Account Settings', a nast\u0119pnie kliknij 'GET API KEY'.", - "title": "Po\u0142\u0105cz si\u0119 z urz\u0105dzeniem Rachio" + "title": "Po\u0142\u0105czenie z urz\u0105dzeniem Rachio" } }, "title": "Rachio" diff --git a/homeassistant/components/roku/.translations/ca.json b/homeassistant/components/roku/.translations/ca.json index 727c4e79d73..3f9897784f9 100644 --- a/homeassistant/components/roku/.translations/ca.json +++ b/homeassistant/components/roku/.translations/ca.json @@ -5,8 +5,7 @@ "unknown": "Error inesperat" }, "error": { - "cannot_connect": "No s'ha pogut connectar, torna-ho a provar", - "unknown": "Error inesperat" + "cannot_connect": "No s'ha pogut connectar, torna-ho a provar" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/de.json b/homeassistant/components/roku/.translations/de.json index d3c02cc1373..3954d9d549d 100644 --- a/homeassistant/components/roku/.translations/de.json +++ b/homeassistant/components/roku/.translations/de.json @@ -5,8 +5,7 @@ "unknown": "Unerwarteter Fehler" }, "error": { - "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut", - "unknown": "Unerwarteter Fehler" + "cannot_connect": "Verbindung fehlgeschlagen, versuchen Sie es erneut" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/en.json b/homeassistant/components/roku/.translations/en.json index a92570c7019..30c53e1d89e 100644 --- a/homeassistant/components/roku/.translations/en.json +++ b/homeassistant/components/roku/.translations/en.json @@ -5,8 +5,7 @@ "unknown": "Unexpected error" }, "error": { - "cannot_connect": "Failed to connect, please try again", - "unknown": "Unexpected error" + "cannot_connect": "Failed to connect, please try again" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/es.json b/homeassistant/components/roku/.translations/es.json index a472d079efb..ffa850f6ebe 100644 --- a/homeassistant/components/roku/.translations/es.json +++ b/homeassistant/components/roku/.translations/es.json @@ -5,8 +5,7 @@ "unknown": "Error inesperado" }, "error": { - "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo.", - "unknown": "Error inesperado" + "cannot_connect": "No se ha podido conectar, por favor, int\u00e9ntalo de nuevo." }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/fr.json b/homeassistant/components/roku/.translations/fr.json index ff24f46e921..a76f68f2f61 100644 --- a/homeassistant/components/roku/.translations/fr.json +++ b/homeassistant/components/roku/.translations/fr.json @@ -5,8 +5,7 @@ "unknown": "Erreur inattendue" }, "error": { - "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "unknown": "Erreur inattendue" + "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/it.json b/homeassistant/components/roku/.translations/it.json index 37567913700..c530504c6ec 100644 --- a/homeassistant/components/roku/.translations/it.json +++ b/homeassistant/components/roku/.translations/it.json @@ -5,8 +5,7 @@ "unknown": "Errore imprevisto" }, "error": { - "cannot_connect": "Impossibile connettersi, si prega di riprovare", - "unknown": "Errore imprevisto" + "cannot_connect": "Impossibile connettersi, si prega di riprovare" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/ko.json b/homeassistant/components/roku/.translations/ko.json index 75045d14865..d7cad509da1 100644 --- a/homeassistant/components/roku/.translations/ko.json +++ b/homeassistant/components/roku/.translations/ko.json @@ -5,8 +5,7 @@ "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" }, "error": { - "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694.", - "unknown": "\uc608\uc0c1\uce58 \ubabb\ud55c \uc624\ub958\uac00 \ubc1c\uc0dd\ud588\uc2b5\ub2c8\ub2e4" + "cannot_connect": "\uc5f0\uacb0\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4. \ub2e4\uc2dc \uc2dc\ub3c4\ud574\uc8fc\uc138\uc694." }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/lb.json b/homeassistant/components/roku/.translations/lb.json index 789dac2eed7..da6136334ce 100644 --- a/homeassistant/components/roku/.translations/lb.json +++ b/homeassistant/components/roku/.translations/lb.json @@ -5,8 +5,7 @@ "unknown": "Onerwaarte Feeler" }, "error": { - "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol.", - "unknown": "Onerwaarte Feeler" + "cannot_connect": "Feeler beim verbannen, prob\u00e9ier w.e.g. nach emol." }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/no.json b/homeassistant/components/roku/.translations/no.json index cabc68de3f7..df56aa5d35d 100644 --- a/homeassistant/components/roku/.translations/no.json +++ b/homeassistant/components/roku/.translations/no.json @@ -5,8 +5,7 @@ "unknown": "Uventet feil" }, "error": { - "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", - "unknown": "Uventet feil" + "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/pl.json b/homeassistant/components/roku/.translations/pl.json index db3ef261f07..b92aab58df6 100644 --- a/homeassistant/components/roku/.translations/pl.json +++ b/homeassistant/components/roku/.translations/pl.json @@ -4,8 +4,7 @@ "already_configured": "Urz\u0105dzenie Roku jest ju\u017c skonfigurowane." }, "error": { - "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie.", - "unknown": "Niespodziewany b\u0142\u0105d." + "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia, spr\u00f3buj ponownie." }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/ru.json b/homeassistant/components/roku/.translations/ru.json index b1825654c9d..0db5f9718aa 100644 --- a/homeassistant/components/roku/.translations/ru.json +++ b/homeassistant/components/roku/.translations/ru.json @@ -5,8 +5,7 @@ "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." }, "error": { - "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437.", - "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f, \u043f\u043e\u043f\u0440\u043e\u0431\u0443\u0439\u0442\u0435 \u0435\u0449\u0435 \u0440\u0430\u0437." }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/sl.json b/homeassistant/components/roku/.translations/sl.json index f47067b3392..0745151cb0a 100644 --- a/homeassistant/components/roku/.translations/sl.json +++ b/homeassistant/components/roku/.translations/sl.json @@ -5,8 +5,7 @@ "unknown": "Nepri\u010dakovana napaka" }, "error": { - "cannot_connect": "Povezava ni uspela, poskusite znova", - "unknown": "Nepri\u010dakovana napaka" + "cannot_connect": "Povezava ni uspela, poskusite znova" }, "flow_title": "Roku: {name}", "step": { diff --git a/homeassistant/components/roku/.translations/zh-Hant.json b/homeassistant/components/roku/.translations/zh-Hant.json index 2d6a606ef77..529fcb604c7 100644 --- a/homeassistant/components/roku/.translations/zh-Hant.json +++ b/homeassistant/components/roku/.translations/zh-Hant.json @@ -5,8 +5,7 @@ "unknown": "\u672a\u9810\u671f\u932f\u8aa4" }, "error": { - "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21", - "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + "cannot_connect": "\u9023\u7dda\u5931\u6557\uff0c\u8acb\u518d\u8a66\u4e00\u6b21" }, "flow_title": "Roku\uff1a{name}", "step": { diff --git a/homeassistant/components/samsungtv/.translations/ca.json b/homeassistant/components/samsungtv/.translations/ca.json index 7ca5879a5c0..a742cc546b8 100644 --- a/homeassistant/components/samsungtv/.translations/ca.json +++ b/homeassistant/components/samsungtv/.translations/ca.json @@ -4,7 +4,6 @@ "already_configured": "La Samsung TV ja configurada.", "already_in_progress": "La configuraci\u00f3 de la Samsung TV ja est\u00e0 en curs.", "auth_missing": "Home Assistant no est\u00e0 autenticat per connectar-se amb aquesta Samsung TV.", - "not_found": "No s'han trobat Samsung TV's compatibles a la xarxa.", "not_successful": "No s'ha pogut connectar amb el dispositiu Samsung TV.", "not_supported": "Actualment aquest dispositiu Samsung TV no \u00e9s compatible." }, diff --git a/homeassistant/components/samsungtv/.translations/da.json b/homeassistant/components/samsungtv/.translations/da.json index 379fd5d8b6d..7a6b5540c59 100644 --- a/homeassistant/components/samsungtv/.translations/da.json +++ b/homeassistant/components/samsungtv/.translations/da.json @@ -4,7 +4,6 @@ "already_configured": "Dette Samsung-tv er allerede konfigureret.", "already_in_progress": "Samsung-tv-konfiguration er allerede i gang.", "auth_missing": "Home Assistant er ikke godkendt til at oprette forbindelse til dette Samsung-tv. Tjek dit tvs indstillinger for at godkende Home Assistant.", - "not_found": "Der blev ikke fundet nogen underst\u00f8ttede Samsung-tv-enheder p\u00e5 netv\u00e6rket.", "not_successful": "Kan ikke oprette forbindelse til denne Samsung tv-enhed.", "not_supported": "Dette Samsung TV underst\u00f8ttes i \u00f8jeblikket ikke." }, diff --git a/homeassistant/components/samsungtv/.translations/de.json b/homeassistant/components/samsungtv/.translations/de.json index e5e8611362c..24afa67038d 100644 --- a/homeassistant/components/samsungtv/.translations/de.json +++ b/homeassistant/components/samsungtv/.translations/de.json @@ -4,7 +4,6 @@ "already_configured": "Dieser Samsung TV ist bereits konfiguriert", "already_in_progress": "Der Konfigurationsablauf f\u00fcr Samsung TV wird bereits ausgef\u00fchrt.", "auth_missing": "Home Assistant ist nicht berechtigt, eine Verbindung zu diesem Samsung TV herzustellen. \u00dcberpr\u00fcfe die Einstellungen deines Fernsehger\u00e4ts, um Home Assistant zu autorisieren.", - "not_found": "Keine unterst\u00fctzten Samsung TV-Ger\u00e4te im Netzwerk gefunden.", "not_successful": "Es kann keine Verbindung zu diesem Samsung-Fernsehger\u00e4t hergestellt werden.", "not_supported": "Dieses Samsung TV-Ger\u00e4t wird derzeit nicht unterst\u00fctzt." }, diff --git a/homeassistant/components/samsungtv/.translations/en.json b/homeassistant/components/samsungtv/.translations/en.json index 2d3856fbaff..37dc84d3e30 100644 --- a/homeassistant/components/samsungtv/.translations/en.json +++ b/homeassistant/components/samsungtv/.translations/en.json @@ -4,7 +4,6 @@ "already_configured": "This Samsung TV is already configured.", "already_in_progress": "Samsung TV configuration is already in progress.", "auth_missing": "Home Assistant is not authorized to connect to this Samsung TV. Please check your TV's settings to authorize Home Assistant.", - "not_found": "No supported Samsung TV devices found on the network.", "not_successful": "Unable to connect to this Samsung TV device.", "not_supported": "This Samsung TV device is currently not supported." }, diff --git a/homeassistant/components/samsungtv/.translations/es.json b/homeassistant/components/samsungtv/.translations/es.json index 4466b329a2a..91581de59a1 100644 --- a/homeassistant/components/samsungtv/.translations/es.json +++ b/homeassistant/components/samsungtv/.translations/es.json @@ -4,7 +4,6 @@ "already_configured": "Este televisor Samsung ya est\u00e1 configurado.", "already_in_progress": "La configuraci\u00f3n del televisor Samsung ya est\u00e1 en progreso.", "auth_missing": "Home Assistant no est\u00e1 autenticado para conectarse a este televisor Samsung.", - "not_found": "No se encontraron televisiones Samsung compatibles en la red.", "not_successful": "No se puede conectar a este dispositivo Samsung TV.", "not_supported": "Esta televisi\u00f3n Samsung actualmente no es compatible." }, diff --git a/homeassistant/components/samsungtv/.translations/fr.json b/homeassistant/components/samsungtv/.translations/fr.json index e381660a3e2..8e722a7add0 100644 --- a/homeassistant/components/samsungtv/.translations/fr.json +++ b/homeassistant/components/samsungtv/.translations/fr.json @@ -4,7 +4,6 @@ "already_configured": "Ce t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 configur\u00e9.", "already_in_progress": "La configuration du t\u00e9l\u00e9viseur Samsung est d\u00e9j\u00e0 en cours.", "auth_missing": "Home Assistant n'est pas authentifi\u00e9 pour se connecter \u00e0 ce t\u00e9l\u00e9viseur Samsung.", - "not_found": "Aucun t\u00e9l\u00e9viseur Samsung pris en charge trouv\u00e9 sur le r\u00e9seau.", "not_successful": "Impossible de se connecter \u00e0 cet appareil Samsung TV.", "not_supported": "Ce t\u00e9l\u00e9viseur Samsung n'est actuellement pas pris en charge." }, diff --git a/homeassistant/components/samsungtv/.translations/hu.json b/homeassistant/components/samsungtv/.translations/hu.json index c7a046428bc..6ed1d806739 100644 --- a/homeassistant/components/samsungtv/.translations/hu.json +++ b/homeassistant/components/samsungtv/.translations/hu.json @@ -4,7 +4,6 @@ "already_configured": "Ez a Samsung TV m\u00e1r konfigur\u00e1lva van.", "already_in_progress": "A Samsung TV konfigur\u00e1l\u00e1sa m\u00e1r folyamatban van.", "auth_missing": "A Home Assistant nem jogosult csatlakozni ehhez a Samsung TV-hez. Ellen\u0151rizze a TV-k\u00e9sz\u00fcl\u00e9k\u00e9ben a Home Assistant enged\u00e9lyez\u00e9si be\u00e1ll\u00edt\u00e1sait.", - "not_found": "A h\u00e1l\u00f3zaton nem tal\u00e1lhat\u00f3 t\u00e1mogatott Samsung TV-eszk\u00f6z.", "not_successful": "Nem lehet csatlakozni ehhez a Samsung TV k\u00e9sz\u00fcl\u00e9khez.", "not_supported": "Ez a Samsung TV k\u00e9sz\u00fcl\u00e9k jelenleg nem t\u00e1mogatott." }, diff --git a/homeassistant/components/samsungtv/.translations/it.json b/homeassistant/components/samsungtv/.translations/it.json index 3d2d4dd8e11..692f91efea9 100644 --- a/homeassistant/components/samsungtv/.translations/it.json +++ b/homeassistant/components/samsungtv/.translations/it.json @@ -4,7 +4,6 @@ "already_configured": "Questo Samsung TV \u00e8 gi\u00e0 configurato.", "already_in_progress": "La configurazione di Samsung TV \u00e8 gi\u00e0 in corso.", "auth_missing": "Home Assistant non \u00e8 autorizzato a connettersi a questo Samsung TV. Controlla le impostazioni del tuo TV per autorizzare Home Assistant.", - "not_found": "Nessun dispositivo Samsung TV supportato trovato sulla rete.", "not_successful": "Impossibile connettersi a questo dispositivo Samsung TV.", "not_supported": "Questo dispositivo Samsung TV non \u00e8 attualmente supportato." }, diff --git a/homeassistant/components/samsungtv/.translations/ko.json b/homeassistant/components/samsungtv/.translations/ko.json index 0226fd52dc0..20b4496b428 100644 --- a/homeassistant/components/samsungtv/.translations/ko.json +++ b/homeassistant/components/samsungtv/.translations/ko.json @@ -4,7 +4,6 @@ "already_configured": "\uc774 \uc0bc\uc131 TV \ub294 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", "already_in_progress": "\uc0bc\uc131 TV \uad6c\uc131\uc774 \uc774\ubbf8 \uc9c4\ud589\uc911\uc785\ub2c8\ub2e4.", "auth_missing": "Home Assistant \uac00 \ud574\ub2f9 \uc0bc\uc131 TV \uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc788\ub294 \uad8c\ud55c\uc774 \uc5c6\uc2b5\ub2c8\ub2e4. TV \uc124\uc815\uc744 \ud655\uc778\ud558\uc5ec Home Assistant \ub97c \uc2b9\uc778\ud574\uc8fc\uc138\uc694.", - "not_found": "\uc9c0\uc6d0\ub418\ub294 \uc0bc\uc131 TV \ubaa8\ub378\uc774 \ub124\ud2b8\uc6cc\ud06c\uc5d0\uc11c \ubc1c\uacac\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4.", "not_successful": "\uc0bc\uc131 TV \uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4.", "not_supported": "\uc774 \uc0bc\uc131 TV \ubaa8\ub378\uc740 \ud604\uc7ac \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4." }, diff --git a/homeassistant/components/samsungtv/.translations/lb.json b/homeassistant/components/samsungtv/.translations/lb.json index b3a94a1a2a6..39ec28d6992 100644 --- a/homeassistant/components/samsungtv/.translations/lb.json +++ b/homeassistant/components/samsungtv/.translations/lb.json @@ -4,7 +4,6 @@ "already_configured": "D\u00ebs Samsung TV ass scho konfigur\u00e9iert.", "already_in_progress": "Konfiguratioun fir d\u00ebs Samsung TV ass schonn am gaang.", "auth_missing": "Home Assistant ass net authentifiz\u00e9iert fir sech mat d\u00ebsem Samsung TV ze verbannen.", - "not_found": "Keng \u00ebnnerst\u00ebtzte Samsung TV am Netzwierk fonnt.", "not_successful": "Keng Verbindung mat d\u00ebsem Samsung TV Apparat m\u00e9iglech.", "not_supported": "D\u00ebsen Samsung TV Modell g\u00ebtt momentan net \u00ebnnerst\u00ebtzt" }, diff --git a/homeassistant/components/samsungtv/.translations/nl.json b/homeassistant/components/samsungtv/.translations/nl.json index 09c0bba05a3..3dcb9e59d74 100644 --- a/homeassistant/components/samsungtv/.translations/nl.json +++ b/homeassistant/components/samsungtv/.translations/nl.json @@ -4,7 +4,6 @@ "already_configured": "Deze Samsung TV is al geconfigureerd.", "already_in_progress": "Samsung TV configuratie is al in uitvoering.", "auth_missing": "Home Assistant is niet geautoriseerd om verbinding te maken met deze Samsung TV.", - "not_found": "Geen ondersteunde Samsung TV-apparaten gevonden op het netwerk.", "not_successful": "Niet in staat om verbinding te maken met dit Samsung TV toestel.", "not_supported": "Deze Samsung TV wordt momenteel niet ondersteund." }, diff --git a/homeassistant/components/samsungtv/.translations/no.json b/homeassistant/components/samsungtv/.translations/no.json index 55a03edc728..6e02251f271 100644 --- a/homeassistant/components/samsungtv/.translations/no.json +++ b/homeassistant/components/samsungtv/.translations/no.json @@ -4,7 +4,6 @@ "already_configured": "Denne Samsung TV-en er allerede konfigurert.", "already_in_progress": "Samsung TV-konfigurasjon p\u00e5g\u00e5r allerede.", "auth_missing": "Home Assistant er ikke autorisert til \u00e5 koble til denne Samsung-TV. Vennligst kontroller innstillingene for TV-en for \u00e5 autorisere Home Assistent.", - "not_found": "Ingen st\u00f8ttede Samsung TV-enheter funnet i nettverket.", "not_successful": "Kan ikke koble til denne Samsung TV-enheten.", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, diff --git a/homeassistant/components/samsungtv/.translations/pl.json b/homeassistant/components/samsungtv/.translations/pl.json index 200d8d2cf9a..02231169b65 100644 --- a/homeassistant/components/samsungtv/.translations/pl.json +++ b/homeassistant/components/samsungtv/.translations/pl.json @@ -4,7 +4,6 @@ "already_configured": "Ten telewizor Samsung jest ju\u017c skonfigurowany.", "already_in_progress": "Konfiguracja telewizora Samsung jest ju\u017c w toku.", "auth_missing": "Home Assistant nie jest uwierzytelniony, aby po\u0142\u0105czy\u0107 si\u0119 z tym telewizorem Samsung.", - "not_found": "W sieci nie znaleziono obs\u0142ugiwanych telewizor\u00f3w Samsung.", "not_successful": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 urz\u0105dzeniem Samsung TV.", "not_supported": "Ten telewizor Samsung nie jest obecnie obs\u0142ugiwany." }, diff --git a/homeassistant/components/samsungtv/.translations/ru.json b/homeassistant/components/samsungtv/.translations/ru.json index 14f772c5e1d..016979eb330 100644 --- a/homeassistant/components/samsungtv/.translations/ru.json +++ b/homeassistant/components/samsungtv/.translations/ru.json @@ -4,7 +4,6 @@ "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "auth_missing": "Home Assistant \u043d\u0435 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u043e\u0432\u0430\u043d \u0434\u043b\u044f \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u043a \u044d\u0442\u043e\u043c\u0443 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u043e\u0432\u0435\u0440\u044c\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430.", - "not_found": "\u0412 \u0441\u0435\u0442\u0438 \u043d\u0435 \u043d\u0430\u0439\u0434\u0435\u043d\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u043c\u044b\u0445 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", "not_successful": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443.", "not_supported": "\u042d\u0442\u0430 \u043c\u043e\u0434\u0435\u043b\u044c \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u0430 \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f." }, diff --git a/homeassistant/components/samsungtv/.translations/sl.json b/homeassistant/components/samsungtv/.translations/sl.json index 95286476ed0..bbf39de3409 100644 --- a/homeassistant/components/samsungtv/.translations/sl.json +++ b/homeassistant/components/samsungtv/.translations/sl.json @@ -4,7 +4,6 @@ "already_configured": "Ta televizor Samsung je \u017ee konfiguriran.", "already_in_progress": "Konfiguracija Samsung TV je \u017ee v teku.", "auth_missing": "Home Assistant nima dovoljenja za povezavo s tem televizorjem Samsung. Preverite nastavitve televizorja, da ga pooblastite.", - "not_found": "V omre\u017eju ni bilo najdenih nobenih podprtih naprav Samsung TV.", "not_successful": "Povezave s to napravo Samsung TV ni mogo\u010de vzpostaviti.", "not_supported": "Ta naprava Samsung TV trenutno ni podprta." }, diff --git a/homeassistant/components/samsungtv/.translations/sv.json b/homeassistant/components/samsungtv/.translations/sv.json index f75e8238506..423bf61a750 100644 --- a/homeassistant/components/samsungtv/.translations/sv.json +++ b/homeassistant/components/samsungtv/.translations/sv.json @@ -4,7 +4,6 @@ "already_configured": "Denna Samsung TV \u00e4r redan konfigurerad.", "already_in_progress": "Samsung TV-konfiguration p\u00e5g\u00e5r redan.", "auth_missing": "Home Assistant har inte beh\u00f6righet att ansluta till denna Samsung TV. Kontrollera tv:ns inst\u00e4llningar f\u00f6r att godk\u00e4nna Home Assistant.", - "not_found": "Inga Samsung TV-enheter som st\u00f6ds finns i n\u00e4tverket.", "not_successful": "Det g\u00e5r inte att ansluta till denna Samsung TV-enhet.", "not_supported": "Denna Samsung TV-enhet st\u00f6ds f\u00f6r n\u00e4rvarande inte." }, diff --git a/homeassistant/components/samsungtv/.translations/tr.json b/homeassistant/components/samsungtv/.translations/tr.json index 3cf1f135e1f..e23969be8a2 100644 --- a/homeassistant/components/samsungtv/.translations/tr.json +++ b/homeassistant/components/samsungtv/.translations/tr.json @@ -4,7 +4,6 @@ "already_configured": "Bu Samsung TV zaten ayarlanm\u0131\u015f.", "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.", - "not_found": "A\u011fda desteklenen Samsung TV cihaz\u0131 bulunamad\u0131.", "not_successful": "Bu Samsung TV cihaz\u0131na ba\u011flan\u0131lam\u0131yor.", "not_supported": "Bu Samsung TV cihaz\u0131 \u015fu anda desteklenmiyor." }, diff --git a/homeassistant/components/samsungtv/.translations/zh-Hant.json b/homeassistant/components/samsungtv/.translations/zh-Hant.json index 80cfa32a6bf..d12d47551c8 100644 --- a/homeassistant/components/samsungtv/.translations/zh-Hant.json +++ b/homeassistant/components/samsungtv/.translations/zh-Hant.json @@ -4,7 +4,6 @@ "already_configured": "\u4e09\u661f\u96fb\u8996\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", "already_in_progress": "\u4e09\u661f\u96fb\u8996\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "auth_missing": "Home Assistant \u672a\u7372\u5f97\u9a57\u8b49\u4ee5\u9023\u7dda\u81f3\u6b64\u4e09\u661f\u96fb\u8996\u3002\u8acb\u6aa2\u67e5\u60a8\u7684\u96fb\u8996\u8a2d\u5b9a\u4ee5\u76e1\u8208\u9a57\u8b49\u3002", - "not_found": "\u5728\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u652f\u63f4\u7684\u4e09\u661f\u96fb\u8996\u3002", "not_successful": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e09\u661f\u96fb\u8996\u8a2d\u5099\u3002", "not_supported": "\u4e0d\u652f\u63f4\u6b64\u6b3e\u4e09\u661f\u96fb\u8996\u3002" }, diff --git a/homeassistant/components/sensor/.translations/lb.json b/homeassistant/components/sensor/.translations/lb.json index 01a4e89c9f4..f999e3c16f0 100644 --- a/homeassistant/components/sensor/.translations/lb.json +++ b/homeassistant/components/sensor/.translations/lb.json @@ -1,26 +1,26 @@ { "device_automation": { "condition_type": { - "is_battery_level": "{entity_name} Batterie niveau", - "is_humidity": "{entity_name} Fiichtegkeet", - "is_illuminance": "{entity_name} Beliichtung", - "is_power": "{entity_name} Leeschtung", - "is_pressure": "{entity_name} Drock", - "is_signal_strength": "{entity_name} Signal St\u00e4erkt", - "is_temperature": "{entity_name} Temperatur", - "is_timestamp": "{entity_name} Z\u00e4itstempel", - "is_value": "{entity_name} W\u00e4ert" + "is_battery_level": "Aktuell {entity_name} Batterie niveau", + "is_humidity": "Aktuell {entity_name} Fiichtegkeet", + "is_illuminance": "Aktuell {entity_name} Beliichtung", + "is_power": "Aktuell {entity_name} Leeschtung", + "is_pressure": "Aktuell {entity_name} Drock", + "is_signal_strength": "Aktuell {entity_name} Signal St\u00e4erkt", + "is_temperature": "Aktuell {entity_name} Temperatur", + "is_timestamp": "Aktuelle {entity_name} Z\u00e4itstempel", + "is_value": "Aktuelle {entity_name} W\u00e4ert" }, "trigger_type": { - "battery_level": "{entity_name} Batterie niveau", - "humidity": "{entity_name} Fiichtegkeet", - "illuminance": "{entity_name} Beliichtung", - "power": "{entity_name} Leeschtung", - "pressure": "{entity_name} Drock", - "signal_strength": "{entity_name} Signal St\u00e4erkt", - "temperature": "{entity_name} Temperatur", - "timestamp": "{entity_name} Z\u00e4itstempel", - "value": "{entity_name} W\u00e4ert" + "battery_level": "{entity_name} Batterie niveau \u00e4nnert", + "humidity": "{entity_name} Fiichtegkeet \u00e4nnert", + "illuminance": "{entity_name} Beliichtung \u00e4nnert", + "power": "{entity_name} Leeschtung \u00e4nnert", + "pressure": "{entity_name} Drock \u00e4nnert", + "signal_strength": "{entity_name} Signal St\u00e4erkt \u00e4nnert", + "temperature": "{entity_name} Temperatur \u00e4nnert", + "timestamp": "{entity_name} Z\u00e4itstempel \u00e4nnert", + "value": "{entity_name} W\u00e4ert \u00e4nnert" } } } \ No newline at end of file diff --git a/homeassistant/components/simplisafe/.translations/bg.json b/homeassistant/components/simplisafe/.translations/bg.json index 4f15cc674b0..0ec8fd3c6b1 100644 --- a/homeassistant/components/simplisafe/.translations/bg.json +++ b/homeassistant/components/simplisafe/.translations/bg.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "\u041a\u043e\u0434 (\u0437\u0430 Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u0430", "username": "E-mail \u0430\u0434\u0440\u0435\u0441" }, diff --git a/homeassistant/components/simplisafe/.translations/ca.json b/homeassistant/components/simplisafe/.translations/ca.json index 1f772071b59..f2d9db5797d 100644 --- a/homeassistant/components/simplisafe/.translations/ca.json +++ b/homeassistant/components/simplisafe/.translations/ca.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Codi (pel Home Assistant)", "password": "Contrasenya", "username": "Correu electr\u00f2nic" }, diff --git a/homeassistant/components/simplisafe/.translations/cs.json b/homeassistant/components/simplisafe/.translations/cs.json index f4a47c5c344..2160dc226d9 100644 --- a/homeassistant/components/simplisafe/.translations/cs.json +++ b/homeassistant/components/simplisafe/.translations/cs.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "K\u00f3d (pro Home Assistant)", "password": "Heslo", "username": "E-mailov\u00e1 adresa" }, diff --git a/homeassistant/components/simplisafe/.translations/da.json b/homeassistant/components/simplisafe/.translations/da.json index ccd82979520..39324fe5f51 100644 --- a/homeassistant/components/simplisafe/.translations/da.json +++ b/homeassistant/components/simplisafe/.translations/da.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Kode (til Home Assistant)", "password": "Adgangskode", "username": "Emailadresse" }, diff --git a/homeassistant/components/simplisafe/.translations/de.json b/homeassistant/components/simplisafe/.translations/de.json index 8c615a80c3f..08d5b31d202 100644 --- a/homeassistant/components/simplisafe/.translations/de.json +++ b/homeassistant/components/simplisafe/.translations/de.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Code (f\u00fcr Home Assistant)", "password": "Passwort", "username": "E-Mail-Adresse" }, diff --git a/homeassistant/components/simplisafe/.translations/en.json b/homeassistant/components/simplisafe/.translations/en.json index c9d92c9e445..60c3784ee9d 100644 --- a/homeassistant/components/simplisafe/.translations/en.json +++ b/homeassistant/components/simplisafe/.translations/en.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Code (for Home Assistant)", "password": "Password", "username": "Email Address" }, diff --git a/homeassistant/components/simplisafe/.translations/es-419.json b/homeassistant/components/simplisafe/.translations/es-419.json index 709d045c348..bf4127fbd84 100644 --- a/homeassistant/components/simplisafe/.translations/es-419.json +++ b/homeassistant/components/simplisafe/.translations/es-419.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "C\u00f3digo (para Home Assistant)", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico" }, diff --git a/homeassistant/components/simplisafe/.translations/es.json b/homeassistant/components/simplisafe/.translations/es.json index dfd87be2721..fe159cf9fa8 100644 --- a/homeassistant/components/simplisafe/.translations/es.json +++ b/homeassistant/components/simplisafe/.translations/es.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "C\u00f3digo (para Home Assistant)", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico" }, diff --git a/homeassistant/components/simplisafe/.translations/fr.json b/homeassistant/components/simplisafe/.translations/fr.json index 0f5049ecce4..e204fa96f1b 100644 --- a/homeassistant/components/simplisafe/.translations/fr.json +++ b/homeassistant/components/simplisafe/.translations/fr.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Code (pour Home Assistant)", "password": "Mot de passe", "username": "Adresse e-mail" }, diff --git a/homeassistant/components/simplisafe/.translations/it.json b/homeassistant/components/simplisafe/.translations/it.json index 80f684cff7c..71581e845f4 100644 --- a/homeassistant/components/simplisafe/.translations/it.json +++ b/homeassistant/components/simplisafe/.translations/it.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Codice (Home Assistant)", "password": "Password", "username": "Indirizzo E-mail" }, diff --git a/homeassistant/components/simplisafe/.translations/ko.json b/homeassistant/components/simplisafe/.translations/ko.json index 17d50bc1508..53e67cd5506 100644 --- a/homeassistant/components/simplisafe/.translations/ko.json +++ b/homeassistant/components/simplisafe/.translations/ko.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "\ucf54\ub4dc (Home Assistant \uc6a9)", "password": "\ube44\ubc00\ubc88\ud638", "username": "\uc774\uba54\uc77c \uc8fc\uc18c" }, diff --git a/homeassistant/components/simplisafe/.translations/lb.json b/homeassistant/components/simplisafe/.translations/lb.json index 81f4c82fcc7..a7e56f817d5 100644 --- a/homeassistant/components/simplisafe/.translations/lb.json +++ b/homeassistant/components/simplisafe/.translations/lb.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Code (fir Home Assistant)", "password": "Passwuert", "username": "E-Mail Adress" }, diff --git a/homeassistant/components/simplisafe/.translations/nl.json b/homeassistant/components/simplisafe/.translations/nl.json index c84593c0b23..bad1c408144 100644 --- a/homeassistant/components/simplisafe/.translations/nl.json +++ b/homeassistant/components/simplisafe/.translations/nl.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "Code (voor Home Assistant)", "password": "Wachtwoord", "username": "E-mailadres" }, diff --git a/homeassistant/components/simplisafe/.translations/no.json b/homeassistant/components/simplisafe/.translations/no.json index 83a3f8cbaf9..436fba8fd06 100644 --- a/homeassistant/components/simplisafe/.translations/no.json +++ b/homeassistant/components/simplisafe/.translations/no.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Kode (for Home Assistant)", "password": "Passord", "username": "E-postadresse" }, diff --git a/homeassistant/components/simplisafe/.translations/pl.json b/homeassistant/components/simplisafe/.translations/pl.json index 75a38230e88..b673d28a7ca 100644 --- a/homeassistant/components/simplisafe/.translations/pl.json +++ b/homeassistant/components/simplisafe/.translations/pl.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Kod (dla Home Assistant'a)", "password": "Has\u0142o", "username": "Adres e-mail" }, diff --git a/homeassistant/components/simplisafe/.translations/pt-BR.json b/homeassistant/components/simplisafe/.translations/pt-BR.json index 819cb1d95e0..2f1fe9ca10a 100644 --- a/homeassistant/components/simplisafe/.translations/pt-BR.json +++ b/homeassistant/components/simplisafe/.translations/pt-BR.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "C\u00f3digo (para o Home Assistant)", "password": "Senha", "username": "Endere\u00e7o de e-mail" }, diff --git a/homeassistant/components/simplisafe/.translations/pt.json b/homeassistant/components/simplisafe/.translations/pt.json index 47929161976..809c8fc29a4 100644 --- a/homeassistant/components/simplisafe/.translations/pt.json +++ b/homeassistant/components/simplisafe/.translations/pt.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "C\u00f3digo (para Home Assistant)", "password": "Palavra-passe", "username": "Endere\u00e7o de e-mail" }, diff --git a/homeassistant/components/simplisafe/.translations/ro.json b/homeassistant/components/simplisafe/.translations/ro.json index b7e281a2bc2..33f284e93c2 100644 --- a/homeassistant/components/simplisafe/.translations/ro.json +++ b/homeassistant/components/simplisafe/.translations/ro.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "Cod (pentru Home Assistant)", "password": "Parola", "username": "Adresa de email" }, diff --git a/homeassistant/components/simplisafe/.translations/ru.json b/homeassistant/components/simplisafe/.translations/ru.json index 070ac3f3425..1e06319672a 100644 --- a/homeassistant/components/simplisafe/.translations/ru.json +++ b/homeassistant/components/simplisafe/.translations/ru.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "\u041a\u043e\u0434 (\u0434\u043b\u044f Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441 \u044d\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0439 \u043f\u043e\u0447\u0442\u044b" }, diff --git a/homeassistant/components/simplisafe/.translations/sl.json b/homeassistant/components/simplisafe/.translations/sl.json index fde16021d69..15131fb1198 100644 --- a/homeassistant/components/simplisafe/.translations/sl.json +++ b/homeassistant/components/simplisafe/.translations/sl.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "Koda (za Home Assistant)", "password": "Geslo", "username": "E-po\u0161tni naslov" }, diff --git a/homeassistant/components/simplisafe/.translations/sv.json b/homeassistant/components/simplisafe/.translations/sv.json index 4666a9ea182..28ae99c1dc4 100644 --- a/homeassistant/components/simplisafe/.translations/sv.json +++ b/homeassistant/components/simplisafe/.translations/sv.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "Kod (f\u00f6r Home Assistant)", "password": "L\u00f6senord", "username": "E-postadress" }, diff --git a/homeassistant/components/simplisafe/.translations/uk.json b/homeassistant/components/simplisafe/.translations/uk.json index 4dee0ed5f4d..c7938df009e 100644 --- a/homeassistant/components/simplisafe/.translations/uk.json +++ b/homeassistant/components/simplisafe/.translations/uk.json @@ -3,7 +3,6 @@ "step": { "user": { "data": { - "code": "\u041a\u043e\u0434 (\u0434\u043b\u044f Home Assistant)", "password": "\u041f\u0430\u0440\u043e\u043b\u044c", "username": "\u0410\u0434\u0440\u0435\u0441\u0430 \u0435\u043b\u0435\u043a\u0442\u0440\u043e\u043d\u043d\u043e\u0457 \u043f\u043e\u0448\u0442\u0438" }, diff --git a/homeassistant/components/simplisafe/.translations/zh-Hans.json b/homeassistant/components/simplisafe/.translations/zh-Hans.json index 4c57baea77f..2981ee71634 100644 --- a/homeassistant/components/simplisafe/.translations/zh-Hans.json +++ b/homeassistant/components/simplisafe/.translations/zh-Hans.json @@ -7,7 +7,6 @@ "step": { "user": { "data": { - "code": "\u4ee3\u7801\uff08\u7528\u4e8eHome Assistant\uff09", "password": "\u5bc6\u7801", "username": "\u7535\u5b50\u90ae\u4ef6\u5730\u5740" }, diff --git a/homeassistant/components/simplisafe/.translations/zh-Hant.json b/homeassistant/components/simplisafe/.translations/zh-Hant.json index 981fa5b59cf..bbe44a4fdea 100644 --- a/homeassistant/components/simplisafe/.translations/zh-Hant.json +++ b/homeassistant/components/simplisafe/.translations/zh-Hant.json @@ -10,7 +10,6 @@ "step": { "user": { "data": { - "code": "\u9a57\u8b49\u78bc\uff08Home Assistant \u7528\uff09", "password": "\u5bc6\u78bc", "username": "\u96fb\u5b50\u90f5\u4ef6\u5730\u5740" }, diff --git a/homeassistant/components/switch/.translations/bg.json b/homeassistant/components/switch/.translations/bg.json index 64a3ea94e1b..19a853dba97 100644 --- a/homeassistant/components/switch/.translations/bg.json +++ b/homeassistant/components/switch/.translations/bg.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} \u0435 \u0438\u0437\u043a\u043b\u044e\u0447\u0435\u043d", - "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d", - "turn_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", - "turn_on": "\u0412\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}" + "is_on": "{entity_name} \u0435 \u0432\u043a\u043b\u044e\u0447\u0435\u043d" }, "trigger_type": { "turned_off": "\u0418\u0437\u043a\u043b\u044e\u0447\u0432\u0430\u043d\u0435 \u043d\u0430 {entity_name}", diff --git a/homeassistant/components/switch/.translations/ca.json b/homeassistant/components/switch/.translations/ca.json index dbf5e152656..0f1101eca75 100644 --- a/homeassistant/components/switch/.translations/ca.json +++ b/homeassistant/components/switch/.translations/ca.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} est\u00e0 apagat", - "is_on": "{entity_name} est\u00e0 enc\u00e8s", - "turn_off": "{entity_name} desactivat", - "turn_on": "{entity_name} activat" + "is_on": "{entity_name} est\u00e0 enc\u00e8s" }, "trigger_type": { "turned_off": "{entity_name} desactivat", diff --git a/homeassistant/components/switch/.translations/da.json b/homeassistant/components/switch/.translations/da.json index 2514a56a010..eefa1e8bb6e 100644 --- a/homeassistant/components/switch/.translations/da.json +++ b/homeassistant/components/switch/.translations/da.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} er fra", - "is_on": "{entity_name} er til", - "turn_off": "{entity_name} slukket", - "turn_on": "{entity_name} t\u00e6ndt" + "is_on": "{entity_name} er til" }, "trigger_type": { "turned_off": "{entity_name} slukkede", diff --git a/homeassistant/components/switch/.translations/de.json b/homeassistant/components/switch/.translations/de.json index 5396facadd7..76496da6dc8 100644 --- a/homeassistant/components/switch/.translations/de.json +++ b/homeassistant/components/switch/.translations/de.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} ist ausgeschaltet", - "is_on": "{entity_name} ist eingeschaltet", - "turn_off": "{entity_name} ausgeschaltet", - "turn_on": "{entity_name} eingeschaltet" + "is_on": "{entity_name} ist eingeschaltet" }, "trigger_type": { "turned_off": "{entity_name} ausgeschaltet", diff --git a/homeassistant/components/switch/.translations/en.json b/homeassistant/components/switch/.translations/en.json index 391a071cb8f..3f37de5331e 100644 --- a/homeassistant/components/switch/.translations/en.json +++ b/homeassistant/components/switch/.translations/en.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} is off", - "is_on": "{entity_name} is on", - "turn_off": "{entity_name} turned off", - "turn_on": "{entity_name} turned on" + "is_on": "{entity_name} is on" }, "trigger_type": { "turned_off": "{entity_name} turned off", diff --git a/homeassistant/components/switch/.translations/es-419.json b/homeassistant/components/switch/.translations/es-419.json index f9607852036..b42b2ce56fa 100644 --- a/homeassistant/components/switch/.translations/es-419.json +++ b/homeassistant/components/switch/.translations/es-419.json @@ -6,9 +6,7 @@ }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagado", - "is_on": "{entity_name} est\u00e1 encendido", - "turn_off": "{entity_name} apagado", - "turn_on": "{entity_name} encendido" + "is_on": "{entity_name} est\u00e1 encendido" }, "trigger_type": { "turned_off": "{entity_name} apagado", diff --git a/homeassistant/components/switch/.translations/es.json b/homeassistant/components/switch/.translations/es.json index 24dbc2cdc1f..c6790619182 100644 --- a/homeassistant/components/switch/.translations/es.json +++ b/homeassistant/components/switch/.translations/es.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagada", - "is_on": "{entity_name} est\u00e1 encendida", - "turn_off": "{entity_name} apagado", - "turn_on": "{entity_name} encendido" + "is_on": "{entity_name} est\u00e1 encendida" }, "trigger_type": { "turned_off": "{entity_name} apagado", diff --git a/homeassistant/components/switch/.translations/fr.json b/homeassistant/components/switch/.translations/fr.json index 807b85c5fb5..adc91477a23 100644 --- a/homeassistant/components/switch/.translations/fr.json +++ b/homeassistant/components/switch/.translations/fr.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} est \u00e9teint", - "is_on": "{entity_name} est allum\u00e9", - "turn_off": "{entity_name} \u00e9teint", - "turn_on": "{entity_name} allum\u00e9" + "is_on": "{entity_name} est allum\u00e9" }, "trigger_type": { "turned_off": "{entity_name} \u00e9teint", diff --git a/homeassistant/components/switch/.translations/hu.json b/homeassistant/components/switch/.translations/hu.json index c3ea3190694..3fba61a4848 100644 --- a/homeassistant/components/switch/.translations/hu.json +++ b/homeassistant/components/switch/.translations/hu.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} ki van kapcsolva", - "is_on": "{entity_name} be van kapcsolva", - "turn_off": "{entity_name} ki lett kapcsolva", - "turn_on": "{entity_name} be lett kapcsolva" + "is_on": "{entity_name} be van kapcsolva" }, "trigger_type": { "turned_off": "{entity_name} ki lett kapcsolva", diff --git a/homeassistant/components/switch/.translations/it.json b/homeassistant/components/switch/.translations/it.json index ec742e4113b..32f479b8b5c 100644 --- a/homeassistant/components/switch/.translations/it.json +++ b/homeassistant/components/switch/.translations/it.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} \u00e8 disattivato", - "is_on": "{entity_name} \u00e8 attivo", - "turn_off": "{entity_name} disattivato", - "turn_on": "{entity_name} attivato" + "is_on": "{entity_name} \u00e8 attivo" }, "trigger_type": { "turned_off": "{entity_name} disattivato", diff --git a/homeassistant/components/switch/.translations/ko.json b/homeassistant/components/switch/.translations/ko.json index d3b9b1dd169..b923fdb210e 100644 --- a/homeassistant/components/switch/.translations/ko.json +++ b/homeassistant/components/switch/.translations/ko.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74", - "turn_off": "{entity_name} \uc774(\uac00) \uaebc\uc838 \uc788\uc73c\uba74", - "turn_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" + "is_on": "{entity_name} \uc774(\uac00) \ucf1c\uc838 \uc788\uc73c\uba74" }, "trigger_type": { "turned_off": "{entity_name} \uc774(\uac00) \uaebc\uc9c8 \ub54c", diff --git a/homeassistant/components/switch/.translations/lb.json b/homeassistant/components/switch/.translations/lb.json index 8e974a0a8de..a7f807e8dcd 100644 --- a/homeassistant/components/switch/.translations/lb.json +++ b/homeassistant/components/switch/.translations/lb.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} ass aus", - "is_on": "{entity_name} ass un", - "turn_off": "{entity_name} gouf ausgeschalt", - "turn_on": "{entity_name} gouf ugeschalt" + "is_on": "{entity_name} ass un" }, "trigger_type": { "turned_off": "{entity_name} gouf ausgeschalt", diff --git a/homeassistant/components/switch/.translations/lv.json b/homeassistant/components/switch/.translations/lv.json index 784a9a37afa..7668dfa5ac8 100644 --- a/homeassistant/components/switch/.translations/lv.json +++ b/homeassistant/components/switch/.translations/lv.json @@ -1,9 +1,5 @@ { "device_automation": { - "condition_type": { - "turn_off": "{entity_name} tika izsl\u0113gta", - "turn_on": "{entity_name} tika iesl\u0113gta" - }, "trigger_type": { "turned_off": "{entity_name} tika izsl\u0113gta", "turned_on": "{entity_name} tika iesl\u0113gta" diff --git a/homeassistant/components/switch/.translations/nl.json b/homeassistant/components/switch/.translations/nl.json index 5e2aa6747a4..905ad413090 100644 --- a/homeassistant/components/switch/.translations/nl.json +++ b/homeassistant/components/switch/.translations/nl.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} is uitgeschakeld", - "is_on": "{entity_name} is ingeschakeld", - "turn_off": "{entity_name} uitgeschakeld", - "turn_on": "{entity_name} ingeschakeld" + "is_on": "{entity_name} is ingeschakeld" }, "trigger_type": { "turned_off": "{entity_name} uitgeschakeld", diff --git a/homeassistant/components/switch/.translations/no.json b/homeassistant/components/switch/.translations/no.json index 3469079f230..785e9ca2912 100644 --- a/homeassistant/components/switch/.translations/no.json +++ b/homeassistant/components/switch/.translations/no.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} er av", - "is_on": "{entity_name} er p\u00e5", - "turn_off": "{entity_name} sl\u00e5tt av", - "turn_on": "{entity_name} sl\u00e5tt p\u00e5" + "is_on": "{entity_name} er p\u00e5" }, "trigger_type": { "turned_off": "{entity_name} sl\u00e5tt av", diff --git a/homeassistant/components/switch/.translations/pl.json b/homeassistant/components/switch/.translations/pl.json index 3d352aa2b58..930694de8ca 100644 --- a/homeassistant/components/switch/.translations/pl.json +++ b/homeassistant/components/switch/.translations/pl.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "prze\u0142\u0105cznik {entity_name} jest wy\u0142\u0105czony", - "is_on": "prze\u0142\u0105cznik {entity_name} jest w\u0142\u0105czony", - "turn_off": "prze\u0142\u0105cznik {entity_name} wy\u0142\u0105czony", - "turn_on": "prze\u0142\u0105cznik {entity_name} w\u0142\u0105czony" + "is_on": "prze\u0142\u0105cznik {entity_name} jest w\u0142\u0105czony" }, "trigger_type": { "turned_off": "nast\u0105pi wy\u0142\u0105czenie {entity_name}", diff --git a/homeassistant/components/switch/.translations/ru.json b/homeassistant/components/switch/.translations/ru.json index 74503eea60b..8ca964606ae 100644 --- a/homeassistant/components/switch/.translations/ru.json +++ b/homeassistant/components/switch/.translations/ru.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", - "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", - "turn_off": "{entity_name} \u0432 \u0432\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438", - "turn_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" + "is_on": "{entity_name} \u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043d\u043e\u043c \u0441\u043e\u0441\u0442\u043e\u044f\u043d\u0438\u0438" }, "trigger_type": { "turned_off": "{entity_name} \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0435\u0442\u0441\u044f", diff --git a/homeassistant/components/switch/.translations/sl.json b/homeassistant/components/switch/.translations/sl.json index f1b851b05b6..bef4f1583b6 100644 --- a/homeassistant/components/switch/.translations/sl.json +++ b/homeassistant/components/switch/.translations/sl.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} je izklopljen", - "is_on": "{entity_name} je vklopljen", - "turn_off": "{entity_name} izklopljen", - "turn_on": "{entity_name} vklopljen" + "is_on": "{entity_name} je vklopljen" }, "trigger_type": { "turned_off": "{entity_name} izklopljen", diff --git a/homeassistant/components/switch/.translations/sv.json b/homeassistant/components/switch/.translations/sv.json index 3ec36265e52..ed5367e0013 100644 --- a/homeassistant/components/switch/.translations/sv.json +++ b/homeassistant/components/switch/.translations/sv.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name} \u00e4r avst\u00e4ngd", - "is_on": "{entity_name} \u00e4r p\u00e5", - "turn_off": "{entity_name} st\u00e4ngdes av", - "turn_on": "{entity_name} slogs p\u00e5" + "is_on": "{entity_name} \u00e4r p\u00e5" }, "trigger_type": { "turned_off": "{entity_name} st\u00e4ngdes av", diff --git a/homeassistant/components/switch/.translations/zh-Hant.json b/homeassistant/components/switch/.translations/zh-Hant.json index 3eaac840497..d8bda90de85 100644 --- a/homeassistant/components/switch/.translations/zh-Hant.json +++ b/homeassistant/components/switch/.translations/zh-Hant.json @@ -7,9 +7,7 @@ }, "condition_type": { "is_off": "{entity_name}\u5df2\u95dc\u9589", - "is_on": "{entity_name}\u5df2\u958b\u555f", - "turn_off": "{entity_name}\u5df2\u95dc\u9589", - "turn_on": "{entity_name}\u5df2\u958b\u555f" + "is_on": "{entity_name}\u5df2\u958b\u555f" }, "trigger_type": { "turned_off": "{entity_name}\u5df2\u95dc\u9589", diff --git a/homeassistant/components/transmission/.translations/bg.json b/homeassistant/components/transmission/.translations/bg.json index 98160b89925..3278f7a3a4c 100644 --- a/homeassistant/components/transmission/.translations/bg.json +++ b/homeassistant/components/transmission/.translations/bg.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u0410\u0434\u0440\u0435\u0441\u044a\u0442 \u0432\u0435\u0447\u0435 \u0435 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d.", - "one_instance_allowed": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u0430 \u0435 \u0441\u0430\u043c\u043e \u0435\u0434\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f." + "already_configured": "\u0410\u0434\u0440\u0435\u0441\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 \u0441 \u0430\u0434\u0440\u0435\u0441\u0430", @@ -10,12 +9,6 @@ "wrong_credentials": "\u0413\u0440\u0435\u0448\u043d\u043e \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u043e \u0438\u043c\u0435 \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u0430" }, "step": { - "options": { - "data": { - "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" - }, - "title": "\u041e\u043f\u0446\u0438\u0438 \u0437\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435" - }, "user": { "data": { "host": "\u0410\u0434\u0440\u0435\u0441", @@ -35,7 +28,6 @@ "data": { "scan_interval": "\u0427\u0435\u0441\u0442\u043e\u0442\u0430 \u043d\u0430 \u0430\u043a\u0442\u0443\u0430\u043b\u0438\u0437\u0438\u0440\u0430\u043d\u0435" }, - "description": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 Transmission", "title": "\u041a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u043d\u0435 \u043d\u0430 \u043e\u043f\u0446\u0438\u0438\u0442\u0435 \u0437\u0430 Transmission" } } diff --git a/homeassistant/components/transmission/.translations/ca.json b/homeassistant/components/transmission/.translations/ca.json index f621574683f..7630b50cdcf 100644 --- a/homeassistant/components/transmission/.translations/ca.json +++ b/homeassistant/components/transmission/.translations/ca.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat.", - "one_instance_allowed": "Nom\u00e9s cal una sola inst\u00e0ncia." + "already_configured": "L'amfitri\u00f3 ja est\u00e0 configurat." }, "error": { "cannot_connect": "No s'ha pogut connectar amb l'amfitri\u00f3", @@ -10,12 +9,6 @@ "wrong_credentials": "Nom d'usuari o contrasenya incorrectes" }, "step": { - "options": { - "data": { - "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" - }, - "title": "Opcions de configuraci\u00f3" - }, "user": { "data": { "host": "Amfitri\u00f3", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Freq\u00fc\u00e8ncia d\u2019actualitzaci\u00f3" }, - "description": "Opcions de configuraci\u00f3 de Transmission", "title": "Opcions de configuraci\u00f3 de Transmission" } } diff --git a/homeassistant/components/transmission/.translations/da.json b/homeassistant/components/transmission/.translations/da.json index e84ec938ee2..feabb364344 100644 --- a/homeassistant/components/transmission/.translations/da.json +++ b/homeassistant/components/transmission/.translations/da.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "V\u00e6rten er allerede konfigureret.", - "one_instance_allowed": "Kun en enkelt instans er n\u00f8dvendig." + "already_configured": "V\u00e6rten er allerede konfigureret." }, "error": { "cannot_connect": "Kunne ikke oprette forbindelse til v\u00e6rt", @@ -10,12 +9,6 @@ "wrong_credentials": "Ugyldigt brugernavn eller adgangskode" }, "step": { - "options": { - "data": { - "scan_interval": "Opdateringsfrekvens" - }, - "title": "Konfigurationsmuligheder" - }, "user": { "data": { "host": "V\u00e6rt", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Opdateringsfrekvens" }, - "description": "Konfigurationsindstillinger for Transmission", "title": "Konfigurationsindstillinger for Transmission" } } diff --git a/homeassistant/components/transmission/.translations/de.json b/homeassistant/components/transmission/.translations/de.json index 736a6d72659..c3d912e5e77 100644 --- a/homeassistant/components/transmission/.translations/de.json +++ b/homeassistant/components/transmission/.translations/de.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Host ist bereits konfiguriert.", - "one_instance_allowed": "Nur eine einzige Instanz ist notwendig." + "already_configured": "Host ist bereits konfiguriert." }, "error": { "cannot_connect": "Verbindung zum Host nicht m\u00f6glich", @@ -10,12 +9,6 @@ "wrong_credentials": "Falscher Benutzername oder Kennwort" }, "step": { - "options": { - "data": { - "scan_interval": "Aktualisierungsfrequenz" - }, - "title": "Konfigurationsoptionen" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Aktualisierungsfrequenz" }, - "description": "Konfigurieren von Optionen f\u00fcr Transmission", "title": "Konfiguriere die Optionen f\u00fcr die \u00dcbertragung" } } diff --git a/homeassistant/components/transmission/.translations/en.json b/homeassistant/components/transmission/.translations/en.json index aa8b99a4914..3605f21e140 100644 --- a/homeassistant/components/transmission/.translations/en.json +++ b/homeassistant/components/transmission/.translations/en.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Host is already configured.", - "one_instance_allowed": "Only a single instance is necessary." + "already_configured": "Host is already configured." }, "error": { "cannot_connect": "Unable to Connect to host", @@ -10,12 +9,6 @@ "wrong_credentials": "Wrong username or password" }, "step": { - "options": { - "data": { - "scan_interval": "Update frequency" - }, - "title": "Configure Options" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Update frequency" }, - "description": "Configure options for Transmission", "title": "Configure options for Transmission" } } diff --git a/homeassistant/components/transmission/.translations/es.json b/homeassistant/components/transmission/.translations/es.json index 06ea19e72b8..a1d0f364769 100644 --- a/homeassistant/components/transmission/.translations/es.json +++ b/homeassistant/components/transmission/.translations/es.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "El host ya est\u00e1 configurado.", - "one_instance_allowed": "S\u00f3lo se necesita una sola instancia." + "already_configured": "El host ya est\u00e1 configurado." }, "error": { "cannot_connect": "No se puede conectar al host", @@ -10,12 +9,6 @@ "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" }, "step": { - "options": { - "data": { - "scan_interval": "Frecuencia de actualizaci\u00f3n" - }, - "title": "Configurar opciones" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Frecuencia de actualizaci\u00f3n" }, - "description": "Configurar opciones para la transmisi\u00f3n", "title": "Configurar opciones para la transmisi\u00f3n" } } diff --git a/homeassistant/components/transmission/.translations/fr.json b/homeassistant/components/transmission/.translations/fr.json index 3c267b36a08..c7a78201797 100644 --- a/homeassistant/components/transmission/.translations/fr.json +++ b/homeassistant/components/transmission/.translations/fr.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9.", - "one_instance_allowed": "Une seule instance est n\u00e9cessaire." + "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." }, "error": { "cannot_connect": "Impossible de se connecter \u00e0 l'h\u00f4te", @@ -10,12 +9,6 @@ "wrong_credentials": "Mauvais nom d'utilisateur ou mot de passe" }, "step": { - "options": { - "data": { - "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" - }, - "title": "Configurer les options" - }, "user": { "data": { "host": "H\u00f4te", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Fr\u00e9quence de mise \u00e0 jour" }, - "description": "Configurer les options pour Transmission", "title": "Configurer les options pour Transmission" } } diff --git a/homeassistant/components/transmission/.translations/hu.json b/homeassistant/components/transmission/.translations/hu.json index 14bf5c28bdf..cbd2f44c340 100644 --- a/homeassistant/components/transmission/.translations/hu.json +++ b/homeassistant/components/transmission/.translations/hu.json @@ -1,20 +1,11 @@ { "config": { - "abort": { - "one_instance_allowed": "Csak egyetlen p\u00e9ld\u00e1nyra van sz\u00fcks\u00e9g." - }, "error": { "cannot_connect": "Nem lehet csatlakozni az \u00e1llom\u00e1shoz", "name_exists": "A n\u00e9v m\u00e1r l\u00e9tezik", "wrong_credentials": "Rossz felhaszn\u00e1l\u00f3n\u00e9v vagy jelsz\u00f3" }, "step": { - "options": { - "data": { - "scan_interval": "Friss\u00edt\u00e9si gyakoris\u00e1g" - }, - "title": "Be\u00e1ll\u00edt\u00e1sok konfigur\u00e1l\u00e1sa" - }, "user": { "data": { "host": "Kiszolg\u00e1l\u00f3", diff --git a/homeassistant/components/transmission/.translations/it.json b/homeassistant/components/transmission/.translations/it.json index a7c4c675856..8a1f01783c1 100644 --- a/homeassistant/components/transmission/.translations/it.json +++ b/homeassistant/components/transmission/.translations/it.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "L'host \u00e8 gi\u00e0 configurato.", - "one_instance_allowed": "\u00c8 necessaria solo una singola istanza." + "already_configured": "L'host \u00e8 gi\u00e0 configurato." }, "error": { "cannot_connect": "Impossibile connettersi all'host", @@ -10,12 +9,6 @@ "wrong_credentials": "Nome utente o password non validi" }, "step": { - "options": { - "data": { - "scan_interval": "Frequenza di aggiornamento" - }, - "title": "Configura opzioni" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Frequenza di aggiornamento" }, - "description": "Configurare le opzioni per Trasmissione", "title": "Configurare le opzioni per Transmission" } } diff --git a/homeassistant/components/transmission/.translations/ko.json b/homeassistant/components/transmission/.translations/ko.json index 507d4e84789..4d3537818b7 100644 --- a/homeassistant/components/transmission/.translations/ko.json +++ b/homeassistant/components/transmission/.translations/ko.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "one_instance_allowed": "\ud558\ub098\uc758 \uc778\uc2a4\ud134\uc2a4\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4." + "already_configured": "\ud638\uc2a4\ud2b8\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "cannot_connect": "\ud638\uc2a4\ud2b8\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4", @@ -10,12 +9,6 @@ "wrong_credentials": "\uc0ac\uc6a9\uc790 \uc774\ub984 \ub610\ub294 \ube44\ubc00\ubc88\ud638\uac00 \uc798\ubabb\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { - "options": { - "data": { - "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" - }, - "title": "\uc635\uc158 \uc124\uc815" - }, "user": { "data": { "host": "\ud638\uc2a4\ud2b8", @@ -35,7 +28,6 @@ "data": { "scan_interval": "\uc5c5\ub370\uc774\ud2b8 \ube48\ub3c4" }, - "description": "Transmission \uc635\uc158 \uc124\uc815", "title": "Transmission \uc635\uc158 \uc124\uc815" } } diff --git a/homeassistant/components/transmission/.translations/lb.json b/homeassistant/components/transmission/.translations/lb.json index a012bcd8cde..0533574efb0 100644 --- a/homeassistant/components/transmission/.translations/lb.json +++ b/homeassistant/components/transmission/.translations/lb.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Apparat ass scho konfigur\u00e9iert", - "one_instance_allowed": "N\u00ebmmen eng eenzeg Instanz ass n\u00e9ideg." + "already_configured": "Apparat ass scho konfigur\u00e9iert" }, "error": { "cannot_connect": "Kann sech net mam Server verbannen.", @@ -10,12 +9,6 @@ "wrong_credentials": "Falsche Benotzernumm oder Passwuert" }, "step": { - "options": { - "data": { - "scan_interval": "Intervalle vun de Mise \u00e0 jour" - }, - "title": "Optioune konfigur\u00e9ieren" - }, "user": { "data": { "host": "Server", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Intervalle vun de Mise \u00e0 jour" }, - "description": "Optioune fir Transmission konfigur\u00e9ieren", "title": "Optioune fir Transmission konfigur\u00e9ieren" } } diff --git a/homeassistant/components/transmission/.translations/nl.json b/homeassistant/components/transmission/.translations/nl.json index ccb9c569562..5abf25e286c 100644 --- a/homeassistant/components/transmission/.translations/nl.json +++ b/homeassistant/components/transmission/.translations/nl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Host is al geconfigureerd.", - "one_instance_allowed": "Slechts \u00e9\u00e9n instantie is nodig." + "already_configured": "Host is al geconfigureerd." }, "error": { "cannot_connect": "Kan geen verbinding maken met host", @@ -10,12 +9,6 @@ "wrong_credentials": "verkeerde gebruikersnaam of wachtwoord" }, "step": { - "options": { - "data": { - "scan_interval": "Update frequentie" - }, - "title": "Configureer opties" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Update frequentie" }, - "description": "Configureer opties voor Transmission", "title": "Configureer de opties voor Transmission" } } diff --git a/homeassistant/components/transmission/.translations/no.json b/homeassistant/components/transmission/.translations/no.json index c46a6d782ea..d18a854d6e3 100644 --- a/homeassistant/components/transmission/.translations/no.json +++ b/homeassistant/components/transmission/.translations/no.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Verten er allerede konfigurert.", - "one_instance_allowed": "Bare en enkel instans er n\u00f8dvendig." + "already_configured": "Verten er allerede konfigurert." }, "error": { "cannot_connect": "Kan ikke koble til vert", @@ -10,12 +9,6 @@ "wrong_credentials": "Ugyldig brukernavn eller passord" }, "step": { - "options": { - "data": { - "scan_interval": "Oppdater frekvens" - }, - "title": "Konfigurer alternativer" - }, "user": { "data": { "host": "Vert", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Oppdater frekvens" }, - "description": "Konfigurer alternativer for Transmission", "title": "Konfigurer alternativer for Transmission" } } diff --git a/homeassistant/components/transmission/.translations/pl.json b/homeassistant/components/transmission/.translations/pl.json index 5aac538766b..f3e8c01f3d7 100644 --- a/homeassistant/components/transmission/.translations/pl.json +++ b/homeassistant/components/transmission/.translations/pl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Host jest ju\u017c skonfigurowany.", - "one_instance_allowed": "Wymagana jest tylko jedna instancja." + "already_configured": "Host jest ju\u017c skonfigurowany." }, "error": { "cannot_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z hostem", @@ -10,12 +9,6 @@ "wrong_credentials": "Nieprawid\u0142owa nazwa u\u017cytkownika lub has\u0142o" }, "step": { - "options": { - "data": { - "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" - }, - "title": "Opcje" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Cz\u0119stotliwo\u015b\u0107 aktualizacji" }, - "description": "Konfiguracja opcji dla Transmission", "title": "Konfiguracja opcji dla Transmission" } } diff --git a/homeassistant/components/transmission/.translations/pt-BR.json b/homeassistant/components/transmission/.translations/pt-BR.json index de854e1273c..2c162e66ce7 100644 --- a/homeassistant/components/transmission/.translations/pt-BR.json +++ b/homeassistant/components/transmission/.translations/pt-BR.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "O host j\u00e1 est\u00e1 configurado.", - "one_instance_allowed": "Apenas uma \u00fanica inst\u00e2ncia \u00e9 necess\u00e1ria." + "already_configured": "O host j\u00e1 est\u00e1 configurado." }, "error": { "cannot_connect": "N\u00e3o foi poss\u00edvel conectar ao host", @@ -10,12 +9,6 @@ "wrong_credentials": "Nome de usu\u00e1rio ou senha incorretos" }, "step": { - "options": { - "data": { - "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" - }, - "title": "Op\u00e7\u00f5es de configura\u00e7\u00e3o" - }, "user": { "data": { "host": "Host", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Frequ\u00eancia de atualiza\u00e7\u00e3o" }, - "description": "Configurar op\u00e7\u00f5es para transmiss\u00e3o", "title": "Configurar op\u00e7\u00f5es para Transmission" } } diff --git a/homeassistant/components/transmission/.translations/ru.json b/homeassistant/components/transmission/.translations/ru.json index 9f876dde505..ad43d3ee600 100644 --- a/homeassistant/components/transmission/.translations/ru.json +++ b/homeassistant/components/transmission/.translations/ru.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "one_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\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 \u043a \u0445\u043e\u0441\u0442\u0443.", @@ -10,12 +9,6 @@ "wrong_credentials": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043b\u043e\u0433\u0438\u043d \u0438\u043b\u0438 \u043f\u0430\u0440\u043e\u043b\u044c." }, "step": { - "options": { - "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\u0438 Transmission" - }, "user": { "data": { "host": "\u0425\u043e\u0441\u0442", @@ -35,7 +28,6 @@ "data": { "scan_interval": "\u0427\u0430\u0441\u0442\u043e\u0442\u0430 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f" }, - "description": "\u0414\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Transmission", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 Transmission" } } diff --git a/homeassistant/components/transmission/.translations/sl.json b/homeassistant/components/transmission/.translations/sl.json index 37ce27e19f4..765fb284c3a 100644 --- a/homeassistant/components/transmission/.translations/sl.json +++ b/homeassistant/components/transmission/.translations/sl.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "Gostitelj je \u017ee konfiguriran.", - "one_instance_allowed": "Potrebna je samo ena instanca." + "already_configured": "Gostitelj je \u017ee konfiguriran." }, "error": { "cannot_connect": "Ni mogo\u010de vzpostaviti povezave z gostiteljem", @@ -10,12 +9,6 @@ "wrong_credentials": "Napa\u010dno uporabni\u0161ko ime ali geslo" }, "step": { - "options": { - "data": { - "scan_interval": "Pogostost posodabljanja" - }, - "title": "Nastavite mo\u017enosti" - }, "user": { "data": { "host": "Gostitelj", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Pogostost posodabljanja" }, - "description": "Nastavite mo\u017enosti za Transmission", "title": "Nastavite mo\u017enosti za Transmission" } } diff --git a/homeassistant/components/transmission/.translations/sv.json b/homeassistant/components/transmission/.translations/sv.json index b2a00771e85..289c9f985e3 100644 --- a/homeassistant/components/transmission/.translations/sv.json +++ b/homeassistant/components/transmission/.translations/sv.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "V\u00e4rden \u00e4r redan konfigurerad.", - "one_instance_allowed": "Endast en enda instans \u00e4r n\u00f6dv\u00e4ndig." + "already_configured": "V\u00e4rden \u00e4r redan konfigurerad." }, "error": { "cannot_connect": "Det g\u00e5r inte att ansluta till v\u00e4rden", @@ -10,12 +9,6 @@ "wrong_credentials": "Fel anv\u00e4ndarnamn eller l\u00f6senord" }, "step": { - "options": { - "data": { - "scan_interval": "Uppdateringsfrekvens" - }, - "title": "Konfigurera alternativ" - }, "user": { "data": { "host": "V\u00e4rd", @@ -35,7 +28,6 @@ "data": { "scan_interval": "Uppdateringsfrekvens" }, - "description": "Konfigurera alternativ f\u00f6r Transmission", "title": "Konfigurera alternativ f\u00f6r Transmission" } } diff --git a/homeassistant/components/transmission/.translations/zh-Hant.json b/homeassistant/components/transmission/.translations/zh-Hant.json index 304babc991e..6ae573211c6 100644 --- a/homeassistant/components/transmission/.translations/zh-Hant.json +++ b/homeassistant/components/transmission/.translations/zh-Hant.json @@ -1,8 +1,7 @@ { "config": { "abort": { - "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", - "one_instance_allowed": "\u50c5\u9700\u8a2d\u5b9a\u4e00\u7d44\u7269\u4ef6\u5373\u53ef\u3002" + "already_configured": "\u4e3b\u6a5f\u7aef\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "error": { "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u4e3b\u6a5f\u7aef", @@ -10,12 +9,6 @@ "wrong_credentials": "\u4f7f\u7528\u8005\u540d\u7a31\u6216\u5bc6\u78bc\u932f\u8aa4" }, "step": { - "options": { - "data": { - "scan_interval": "\u66f4\u65b0\u983b\u7387" - }, - "title": "\u8a2d\u5b9a\u9078\u9805" - }, "user": { "data": { "host": "\u4e3b\u6a5f\u7aef", @@ -35,7 +28,6 @@ "data": { "scan_interval": "\u66f4\u65b0\u983b\u7387" }, - "description": "Transmission \u8a2d\u5b9a\u9078\u9805", "title": "Transmission \u8a2d\u5b9a\u9078\u9805" } } diff --git a/homeassistant/components/unifi/.translations/ca.json b/homeassistant/components/unifi/.translations/ca.json index 3a6e147e867..7eefb77b3d2 100644 --- a/homeassistant/components/unifi/.translations/ca.json +++ b/homeassistant/components/unifi/.translations/ca.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "Clients controlats amb acc\u00e9s a la xarxa", - "new_client": "Afegeix un client nou per al control d\u2019acc\u00e9s a la xarxa" + "new_client": "Afegeix un client nou per al control d\u2019acc\u00e9s a la xarxa", + "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" diff --git a/homeassistant/components/unifi/.translations/de.json b/homeassistant/components/unifi/.translations/de.json index 655000662ec..afdea87956b 100644 --- a/homeassistant/components/unifi/.translations/de.json +++ b/homeassistant/components/unifi/.translations/de.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "Clients mit Netzwerkzugriffskontrolle", - "new_client": "F\u00fcgen Sie einen neuen Client f\u00fcr die Netzwerkzugangskontrolle hinzu" + "new_client": "F\u00fcgen Sie einen neuen Client f\u00fcr die Netzwerkzugangskontrolle hinzu", + "poe_clients": "POE-Kontrolle von Clients zulassen" }, "description": "Konfigurieren Sie Client-Steuerelemente \n\nErstellen Sie Switches f\u00fcr Seriennummern, f\u00fcr die Sie den Netzwerkzugriff steuern m\u00f6chten.", "title": "UniFi-Optionen 2/3" diff --git a/homeassistant/components/unifi/.translations/en.json b/homeassistant/components/unifi/.translations/en.json index 8fdde34470b..d42a647c82f 100644 --- a/homeassistant/components/unifi/.translations/en.json +++ b/homeassistant/components/unifi/.translations/en.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "Network access controlled clients", - "new_client": "Add new client for network access control" + "new_client": "Add new client for network access control", + "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" diff --git a/homeassistant/components/unifi/.translations/es.json b/homeassistant/components/unifi/.translations/es.json index b6713bb09bb..31c7e6c0bcd 100644 --- a/homeassistant/components/unifi/.translations/es.json +++ b/homeassistant/components/unifi/.translations/es.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "Clientes con acceso controlado a la red", - "new_client": "A\u00f1adir nuevo cliente para el control de acceso a la red" + "new_client": "A\u00f1adir nuevo cliente para el control de acceso a la red", + "poe_clients": "Permitir control PoE de clientes" }, "description": "Configurar controles de cliente\n\nCrea conmutadores para los n\u00fameros de serie para los que deseas controlar el acceso a la red.", "title": "Opciones UniFi 2/3" diff --git a/homeassistant/components/unifi/.translations/ko.json b/homeassistant/components/unifi/.translations/ko.json index 5c45e272e91..d57d80c7911 100644 --- a/homeassistant/components/unifi/.translations/ko.json +++ b/homeassistant/components/unifi/.translations/ko.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4 \ud074\ub77c\uc774\uc5b8\ud2b8", - "new_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4\ub97c \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uac00" + "new_client": "\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4 \uc81c\uc5b4\ub97c \uc704\ud55c \uc0c8\ub85c\uc6b4 \ud074\ub77c\uc774\uc5b8\ud2b8 \ucd94\uac00", + "poe_clients": "\ud074\ub77c\uc774\uc5b8\ud2b8\uc758 POE \uc81c\uc5b4 \ud5c8\uc6a9" }, "description": "\ud074\ub77c\uc774\uc5b8\ud2b8 \ucee8\ud2b8\ub864 \uad6c\uc131 \n\n\ub124\ud2b8\uc6cc\ud06c \uc561\uc138\uc2a4\ub97c \uc81c\uc5b4\ud558\ub824\ub294 \uc2dc\ub9ac\uc5bc \ubc88\ud638\uc5d0 \ub300\ud55c \uc2a4\uc704\uce58\ub97c \ub9cc\ub4ed\ub2c8\ub2e4.", "title": "UniFi \uc635\uc158 2/3" diff --git a/homeassistant/components/unifi/.translations/lb.json b/homeassistant/components/unifi/.translations/lb.json index 13bb40dd25e..a3d7d685ed2 100644 --- a/homeassistant/components/unifi/.translations/lb.json +++ b/homeassistant/components/unifi/.translations/lb.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Ong\u00eblteg Login Informatioune", - "service_unavailable": "Keen Service disponibel" + "service_unavailable": "Keen Service disponibel", + "unknown_client_mac": "Kee Cliwent mat der MAC Adress disponibel" }, "step": { "user": { @@ -23,19 +24,30 @@ }, "title": "Unifi Kontroller" }, + "error": { + "unknown_client_mac": "Kee Client am Unifi disponibel mat der MAC Adress" + }, "options": { "step": { "client_control": { + "data": { + "block_client": "Netzwierk Zougang kontroll\u00e9iert Clienten", + "new_client": "Neie Client fir Netzwierk Zougang Kontroll b\u00e4isetzen", + "poe_clients": "POE Kontroll vun Clienten erlaben" + }, + "description": "Client Kontroll konfigur\u00e9ieren\n\nErstell Schalter fir Serienummer d\u00e9i sollen fir Netzwierk Zougangs Kontroll kontroll\u00e9iert ginn.", "title": "UniFi Optiounen 2/3" }, "device_tracker": { "data": { "detection_time": "Z\u00e4it a Sekonne vum leschten Z\u00e4itpunkt un bis den Apparat als \u00ebnnerwee consider\u00e9iert g\u00ebtt", + "ssid_filter": "SSIDs auswielen fir Clienten ze verfollegen", "track_clients": "Netzwierk Cliente verfollegen", "track_devices": "Netzwierk Apparater (Ubiquiti Apparater) verfollegen", "track_wired_clients": "Kabel Netzwierk Cliente abez\u00e9ien" }, - "title": "UniFi Optiounen" + "description": "Apparate verfollegen ariichten", + "title": "UniFi Optiounen 1/3" }, "init": { "data": { @@ -48,7 +60,7 @@ "allow_bandwidth_sensors": "Bandbreet Benotzung Sensore fir Netzwierk Cliente erstellen" }, "description": "Statistik Sensoren konfigur\u00e9ieren", - "title": "UniFi Optiounen" + "title": "UniFi Optiounen 3/3" } } } diff --git a/homeassistant/components/unifi/.translations/ru.json b/homeassistant/components/unifi/.translations/ru.json index b73c6b9c2fb..6cd09a947eb 100644 --- a/homeassistant/components/unifi/.translations/ru.json +++ b/homeassistant/components/unifi/.translations/ru.json @@ -29,7 +29,8 @@ "client_control": { "data": { "block_client": "\u041a\u043b\u0438\u0435\u043d\u0442\u044b \u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u0435\u043c \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", - "new_client": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430" + "new_client": "\u0414\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u043d\u043e\u0432\u043e\u0433\u043e \u043a\u043b\u0438\u0435\u043d\u0442\u0430 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044f \u0441\u0435\u0442\u0435\u0432\u043e\u0433\u043e \u0434\u043e\u0441\u0442\u0443\u043f\u0430", + "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" }, "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 UniFi. \u0428\u0430\u0433 2" }, diff --git a/homeassistant/components/unifi/.translations/zh-Hant.json b/homeassistant/components/unifi/.translations/zh-Hant.json index ca920abd492..e91bfca407c 100644 --- a/homeassistant/components/unifi/.translations/zh-Hant.json +++ b/homeassistant/components/unifi/.translations/zh-Hant.json @@ -32,7 +32,8 @@ "client_control": { "data": { "block_client": "\u7db2\u8def\u5b58\u53d6\u63a7\u5236\u5ba2\u6236\u7aef", - "new_client": "\u65b0\u589e\u9396\u8981\u63a7\u5236\u7db2\u8def\u5b58\u53d6\u7684\u5ba2\u6236\u7aef" + "new_client": "\u65b0\u589e\u9396\u8981\u63a7\u5236\u7db2\u8def\u5b58\u53d6\u7684\u5ba2\u6236\u7aef", + "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" diff --git a/homeassistant/components/vera/.translations/ca.json b/homeassistant/components/vera/.translations/ca.json new file mode 100644 index 00000000000..d15d12ce6c3 --- /dev/null +++ b/homeassistant/components/vera/.translations/ca.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ja hi ha un controlador configurat.", + "cannot_connect": "No s'ha pogut connectar amb el controlador amb l'URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Identificadors de dispositiu Vera a excloure de Home Assistant.", + "lights": "Identificadors de dispositiu dels commutadors Vera a tractar com a llums a Home Assistant.", + "vera_controller_url": "URL del controlador" + }, + "description": "Proporciona un URL pel controlador Vera. Hauria de quedar aix\u00ed: http://192.168.1.161:3480.", + "title": "Configuraci\u00f3 del controlador Vera" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Identificadors de dispositiu Vera a excloure de Home Assistant.", + "lights": "Identificadors de dispositiu dels commutadors Vera a tractar com a llums a Home Assistant." + }, + "description": "Consulta la documentaci\u00f3 de Vera per veure els detalls sobre els par\u00e0metres opcionals a: https://www.home-assistant.io/integrations/vera/. Nota: tots els canvis fets aqu\u00ed necessitaran un reinici de Home Assistant. Per esborrar valors, posa-hi un espai.", + "title": "Opcions del controlador Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/de.json b/homeassistant/components/vera/.translations/de.json new file mode 100644 index 00000000000..91f61c9c2bc --- /dev/null +++ b/homeassistant/components/vera/.translations/de.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Ein Controller ist bereits konfiguriert.", + "cannot_connect": "Konnte keine Verbindung zum Controller mit url {base_url} herstellen" + }, + "step": { + "user": { + "data": { + "exclude": "Vera-Ger\u00e4te-IDs, die vom Home Assistant ausgeschlossen werden sollen.", + "lights": "Vera Switch-Ger\u00e4te-IDs, die im Home Assistant als Lichter behandelt werden sollen.", + "vera_controller_url": "Controller-URL" + }, + "description": "Stellen Sie unten eine Vera-Controller-Url zur Verf\u00fcgung. Sie sollte wie folgt aussehen: http://192.168.1.161:3480.", + "title": "Richten Sie den Vera-Controller ein" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera-Ger\u00e4te-IDs, die vom Home Assistant ausgeschlossen werden sollen." + }, + "description": "Weitere Informationen zu optionalen Parametern finden Sie in der Vera-Dokumentation: https://www.home-assistant.io/integrations/vera/. Hinweis: Alle \u00c4nderungen hier erfordern einen Neustart des Home Assistant-Servers. Geben Sie ein Leerzeichen ein, um Werte zu l\u00f6schen.", + "title": "Vera Controller Optionen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/en.json b/homeassistant/components/vera/.translations/en.json new file mode 100644 index 00000000000..0578daa4c0b --- /dev/null +++ b/homeassistant/components/vera/.translations/en.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "A controller is already configured.", + "cannot_connect": "Could not connect to controller with url {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Vera device ids to exclude from Home Assistant.", + "lights": "Vera switch device ids to treat as lights in Home Assistant.", + "vera_controller_url": "Controller URL" + }, + "description": "Provide a Vera controller url below. It should look like this: http://192.168.1.161:3480.", + "title": "Setup Vera controller" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Vera device ids to exclude from Home Assistant.", + "lights": "Vera switch device ids to treat as lights in Home Assistant." + }, + "description": "See the vera documentation for details on optional parameters: https://www.home-assistant.io/integrations/vera/. Note: Any changes here will need a restart to the home assistant server. To clear values, provide a space.", + "title": "Vera controller options" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/es.json b/homeassistant/components/vera/.translations/es.json new file mode 100644 index 00000000000..672bcc9056e --- /dev/null +++ b/homeassistant/components/vera/.translations/es.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Un controlador ya est\u00e1 configurado.", + "cannot_connect": "No se pudo conectar con el controlador con url {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", + "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant", + "vera_controller_url": "URL del controlador" + }, + "description": "Introduce una URL para el controlador Vera a continuaci\u00f3n. Ser\u00eda algo como: http://192.168.1.161:3480.", + "title": "Configurar el controlador Vera" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Identificadores de dispositivos Vera a excluir de Home Assistant", + "lights": "Identificadores de interruptores Vera que deben ser tratados como luces en Home Assistant" + }, + "description": "Consulte la documentaci\u00f3n de Vera para obtener detalles sobre los par\u00e1metros opcionales: https://www.home-assistant.io/integrations/vera/. Nota: Cualquier cambio aqu\u00ed necesitar\u00e1 un reinicio del servidor de Home Assistant. Para borrar valores, introduce un espacio.", + "title": "Opciones del controlador Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/ko.json b/homeassistant/components/vera/.translations/ko.json new file mode 100644 index 00000000000..cecde6b9183 --- /dev/null +++ b/homeassistant/components/vera/.translations/ko.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\ucee8\ud2b8\ub864\ub7ec\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", + "cannot_connect": "URL {base_url} \uc5d0 \ucee8\ud2b8\ub864\ub7ec\ub97c \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4" + }, + "step": { + "user": { + "data": { + "exclude": "Home Assistant \uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", + "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4.", + "vera_controller_url": "\ucee8\ud2b8\ub864\ub7ec URL" + }, + "description": "\uc544\ub798\uc5d0 Vera \ucee8\ud2b8\ub864\ub7ec URL \uc744 \uc785\ub825\ud574\uc8fc\uc138\uc694. http://192.168.1.161:3480 \uacfc \uac19\uc740 \ud615\uc2dd\uc774\uc5b4\uc57c \ud569\ub2c8\ub2e4.", + "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc124\uc815" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Home Assistant \uc5d0\uc11c \uc81c\uc678\ud560 Vera \uae30\uae30 ID.", + "lights": "Vera \uc2a4\uc704\uce58 \uae30\uae30 ID \ub294 Home Assistant \uc5d0\uc11c \uc870\uba85\uc73c\ub85c \ucde8\uae09\ub429\ub2c8\ub2e4." + }, + "description": "\ub9e4\uac1c \ubcc0\uc218 \uc120\ud0dd\uc0ac\ud56d\uc5d0 \ub300\ud55c \uc790\uc138\ud55c \ub0b4\uc6a9\uc740 vera \uc124\uba85\uc11c\ub97c \ucc38\uc870\ud574\uc8fc\uc138\uc694: https://www.home-assistant.io/integrations/vera/. \ucc38\uace0: \uc5ec\uae30\uc5d0\uc11c \ubcc0\uacbd\ud558\uba74 Home Assistant \uc11c\ubc84\ub97c \ub2e4\uc2dc \uc2dc\uc791\ud574\uc57c \ud569\ub2c8\ub2e4. \uac12\uc744 \uc9c0\uc6b0\ub824\uba74 \uc785\ub825\ub780\uc744 \uacf5\ubc31\uc73c\ub85c \ub450\uc138\uc694.", + "title": "Vera \ucee8\ud2b8\ub864\ub7ec \uc635\uc158" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/lb.json b/homeassistant/components/vera/.translations/lb.json new file mode 100644 index 00000000000..440c576596f --- /dev/null +++ b/homeassistant/components/vera/.translations/lb.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "Ee Kontroller ass scho konfigur\u00e9iert", + "cannot_connect": "Et konnt keng Verbindung mam Kontroller mat der URL {base_url} hiergestallt ginn" + }, + "step": { + "user": { + "data": { + "exclude": "IDs vu Vera Apparater d\u00e9i vun Home Assistant ausgeschloss solle ginn.", + "lights": "IDs vun Apparater vu Vera Schalter d\u00e9i als Luuchten am Home Assistant trait\u00e9iert ginn.", + "vera_controller_url": "Kontroller URL" + }, + "description": "Vera Kontroller URL uginn: D\u00e9i sollt sou ausgesinn:\nhttp://192.168.1.161:3480.", + "title": "Vera Kontroller ariichten" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "IDs vu Vera Apparater d\u00e9i vun Home Assistant ausgeschloss solle ginn.", + "lights": "IDs vun Apparater vu Vera Schalter d\u00e9i als Luuchten am Home Assistant trait\u00e9iert ginn." + }, + "description": "Kuck Vera Dokumentatioun fir Detailer zu den optionellle Parameter: https://www.home-assistant.io/integrations/vera/. Hiweis: All \u00c4nnerunge ginn er\u00e9ischt no engem Neistart vum Home Assistant aktiv. Fir W\u00e4rter ze l\u00e4schen, einfach een \"Leerzeichen\" am Feld uginn.", + "title": "Vera Kontroller Optiounen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/ru.json b/homeassistant/components/vera/.translations/ru.json new file mode 100644 index 00000000000..de374358e84 --- /dev/null +++ b/homeassistant/components/vera/.translations/ru.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0443 \u043f\u043e \u0430\u0434\u0440\u0435\u0441\u0443 {base_url}." + }, + "step": { + "user": { + "data": { + "exclude": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant.", + "lights": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u0432 Home Assistant.", + "vera_controller_url": "URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430" + }, + "description": "\u0423\u043a\u0430\u0436\u0438\u0442\u0435 URL-\u0430\u0434\u0440\u0435\u0441 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 Vera. \u0410\u0434\u0440\u0435\u0441 \u0434\u043e\u043b\u0436\u0435\u043d \u0431\u044b\u0442\u044c \u0443\u043a\u0430\u0437\u0430\u043d \u0432 \u0444\u043e\u0440\u043c\u0430\u0442\u0435 'http://192.168.1.161:3480'.", + "title": "Vera" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant.", + "lights": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u0432 Home Assistant." + }, + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u043b\u044e\u0431\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0440\u043e\u0431\u0435\u043b.", + "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vera/.translations/zh-Hant.json b/homeassistant/components/vera/.translations/zh-Hant.json new file mode 100644 index 00000000000..6fb71a57abe --- /dev/null +++ b/homeassistant/components/vera/.translations/zh-Hant.json @@ -0,0 +1,32 @@ +{ + "config": { + "abort": { + "already_configured": "\u6b64\u63a7\u5236\u5668\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210\u3002", + "cannot_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u63a7\u5236\u5668 URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u8a2d\u5099 ID\u3002", + "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u8a2d\u5099 ID\u3002", + "vera_controller_url": "\u63a7\u5236\u5668 URL" + }, + "description": "\u65bc\u4e0b\u65b9\u63d0\u4f9b Vera \u63a7\u5236\u5668 URL\u3002\u683c\u5f0f\u61c9\u8a72\u70ba\uff1ahttp://192.168.1.161:3480\u3002", + "title": "\u8a2d\u5b9a Vera \u63a7\u5236\u5668" + } + }, + "title": "Vera" + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "\u5f9e Home Assistant \u6392\u9664\u7684 Vera \u8a2d\u5099 ID\u3002", + "lights": "\u65bc Home Assistant \u4e2d\u8996\u70ba\u71c8\u5149\u7684 Vera \u958b\u95dc\u8a2d\u5099 ID\u3002" + }, + "description": "\u8acb\u53c3\u95b1 Vera \u6587\u4ef6\u4ee5\u7372\u5f97\u8a73\u7d30\u7684\u9078\u9805\u53c3\u6578\u8cc7\u6599\uff1ahttps://www.home-assistant.io/integrations/vera/\u3002\u8acb\u6ce8\u610f\uff1a\u4efb\u4f55\u8b8a\u66f4\u90fd\u9700\u8981\u91cd\u555f Home Assistant\u3002\u6b32\u6e05\u9664\u8a2d\u5b9a\u503c\u3001\u8acb\u8f38\u5165\u7a7a\u683c\u3002", + "title": "Vera \u63a7\u5236\u5668\u9078\u9805" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/.translations/pl.json b/homeassistant/components/vilfo/.translations/pl.json index aef0c14703f..e9cd91209a4 100644 --- a/homeassistant/components/vilfo/.translations/pl.json +++ b/homeassistant/components/vilfo/.translations/pl.json @@ -15,7 +15,7 @@ "host": "Nazwa hosta lub adres IP routera" }, "description": "Skonfiguruj integracj\u0119 routera Vilfo. Potrzebujesz nazwy hosta/adresu IP routera Vilfo i tokena dost\u0119pu do interfejsu API. Aby uzyska\u0107 dodatkowe informacje na temat tej integracji i sposobu uzyskania niezb\u0119dnych danych do konfiguracji, odwied\u017a: https://www.home-assistant.io/integrations/vilfo", - "title": "Po\u0142\u0105cz si\u0119 z routerem Vilfo" + "title": "Po\u0142\u0105czenie z routerem Vilfo" } }, "title": "Router Vilfo" diff --git a/homeassistant/components/vizio/.translations/ca.json b/homeassistant/components/vizio/.translations/ca.json index c8cfa563779..6b9a3a89134 100644 --- a/homeassistant/components/vizio/.translations/ca.json +++ b/homeassistant/components/vizio/.translations/ca.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "El flux de dades de configuraci\u00f3 pel component Vizio ja est\u00e0 en curs.", "already_setup": "Aquesta entrada ja ha estat configurada.", - "already_setup_with_diff_host_and_name": "Sembla que aquesta entrada ja s'ha configurat amb un amfitri\u00f3 i nom diferents a partir del n\u00famero de s\u00e8rie. Elimina les entrades antigues de configuraction.yaml i del men\u00fa d'integracions abans de provar d'afegir el dispositiu novament.", - "host_exists": "Ja existeix un component Vizio configurat amb el host.", - "name_exists": "Ja existeix un component Vizio configurat amb el nom.", - "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", - "updated_options": "Aquesta entrada ja s'ha configurat per\u00f2 les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat.", - "updated_volume_step": "Aquesta entrada ja s'ha configurat per\u00f2 la mida de l'increment de volum definit a la configuraci\u00f3 no coincideix, en conseq\u00fc\u00e8ncia, s'ha actualitzat." + "updated_entry": "Aquesta entrada ja s'ha configurat per\u00f2 el nom i les opcions definides a la configuraci\u00f3 no coincideixen amb els valors importats anteriorment, en conseq\u00fc\u00e8ncia, s'han actualitzat." }, "error": { "cant_connect": "No s'ha pogut connectar amb el dispositiu. [Comprova la documentaci\u00f3](https://www.home-assistant.io/integrations/vizio/) i torna a verificar que: \n - El dispositiu est\u00e0 engegat \n - El dispositiu est\u00e0 connectat a la xarxa \n - Els valors que has intridu\u00eft s\u00f3n correctes\n abans d\u2019intentar tornar a presentar.", "complete_pairing failed": "No s'ha pogut completar l'emparellament. Verifica que el PIN proporcionat sigui el correcte i que el televisor segueix connectat a la xarxa abans de provar-ho de nou.", "host_exists": "Dispositiu Vizio amb aquest nom d'amfitri\u00f3 ja configurat.", - "name_exists": "Dispositiu Vizio amb aquest nom ja configurat.", - "tv_needs_token": "Si el tipus de dispositiu \u00e9s 'tv', cal un testimoni d'acc\u00e9s v\u00e0lid (token)." + "name_exists": "Dispositiu Vizio amb aquest nom ja configurat." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "El dispositiu Vizio SmartCast est\u00e0 connectat a Home Assistant.\n\nEl testimoni d'acc\u00e9s (Access Token) \u00e9s '**{access_token}**'.", "title": "Emparellament completat" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Aplicacions a incloure o excloure", - "include_or_exclude": "Incloure o excloure aplicacions?" - }, - "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista. Pots ometre aquest pas si el teu televisor no admet aplicacions.", - "title": "Configuraci\u00f3 d'Aplicacions per a Smart TV" - }, "user": { "data": { "access_token": "Testimoni d'acc\u00e9s", @@ -50,14 +35,6 @@ }, "description": "Nom\u00e9s es necessita testimoni d'acc\u00e9s per als televisors. Si est\u00e0s configurant un televisor i encara no tens un testimoni d'acc\u00e9s, deixeu-ho en blanc per poder fer el proc\u00e9s d'emparellament.", "title": "Configuraci\u00f3 del client de Vizio SmartCast" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Aplicacions a incloure o excloure", - "include_or_exclude": "Incloure o excloure aplicacions?" - }, - "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista. Pots ometre aquest pas si el teu televisor no admet aplicacions.", - "title": "Configuraci\u00f3 d'Aplicacions per a Smart TV" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "Aplicacions a incloure o excloure", "include_or_exclude": "Incloure o excloure aplicacions?", - "timeout": "Temps d'espera de les sol\u00b7licituds API (en segons)", "volume_step": "Mida del pas de volum" }, "description": "Si tens una Smart TV, pots filtrar de manera opcional la teva llista de canals escollint quines aplicacions vols incloure o excloure de la llista.", diff --git a/homeassistant/components/vizio/.translations/da.json b/homeassistant/components/vizio/.translations/da.json index 9bfd5864025..5317c1c2adb 100644 --- a/homeassistant/components/vizio/.translations/da.json +++ b/homeassistant/components/vizio/.translations/da.json @@ -1,20 +1,13 @@ { "config": { "abort": { - "already_in_progress": "Konfigurationsproces for Vizio-komponenten er allerede i gang.", "already_setup": "Denne post er allerede blevet konfigureret.", - "already_setup_with_diff_host_and_name": "Denne post ser ud til allerede at v\u00e6re konfigureret med en anden v\u00e6rt og navn baseret p\u00e5 dens serienummer. Fjern eventuelle gamle poster fra din configuration.yaml og i menuen Integrationer, f\u00f8r du fors\u00f8ger at tilf\u00f8je denne enhed igen.", - "host_exists": "Vizio-komponent med v\u00e6rt er allerede konfigureret.", - "name_exists": "Vizio-komponent med navn er allerede konfigureret.", - "updated_entry": "Denne post er allerede konfigureret, men navnet og/eller indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med den tidligere importerede konfiguration, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed.", - "updated_options": "Denne post er allerede konfigureret, men indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med de tidligere importerede indstillingsv\u00e6rdier, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed.", - "updated_volume_step": "Denne post er allerede konfigureret, men lydstyrketrinst\u00f8rrelsen i konfigurationen stemmer ikke overens med konfigurationsposten, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed." + "updated_entry": "Denne post er allerede konfigureret, men navnet og/eller indstillingerne, der er defineret i konfigurationen, stemmer ikke overens med den tidligere importerede konfiguration, s\u00e5 konfigurationsposten er blevet opdateret i overensstemmelse hermed." }, "error": { "cant_connect": "Kunne ikke oprette forbindelse til enheden. [Gennemg\u00e5 dokumentationen] (https://www.home-assistant.io/integrations/vizio/), og bekr\u00e6ft, at: \n - Enheden er t\u00e6ndt \n - Enheden er tilsluttet netv\u00e6rket \n - De angivne v\u00e6rdier er korrekte \n f\u00f8r du fors\u00f8ger at indsende igen.", "host_exists": "Vizio-enhed med den specificerede v\u00e6rt er allerede konfigureret.", - "name_exists": "Vizio-enhed med det specificerede navn er allerede konfigureret.", - "tv_needs_token": "N\u00e5r enhedstypen er 'tv', skal der bruges en gyldig adgangstoken." + "name_exists": "Vizio-enhed med det specificerede navn er allerede konfigureret." }, "step": { "user": { @@ -33,7 +26,6 @@ "step": { "init": { "data": { - "timeout": "Timeout for API-anmodning (sekunder)", "volume_step": "Lydstyrkestrinst\u00f8rrelse" }, "title": "Opdater Vizo SmartCast-indstillinger" diff --git a/homeassistant/components/vizio/.translations/de.json b/homeassistant/components/vizio/.translations/de.json index a3b69526943..2197d27de71 100644 --- a/homeassistant/components/vizio/.translations/de.json +++ b/homeassistant/components/vizio/.translations/de.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Konfigurationsablauf f\u00fcr die Vizio-Komponente wird bereits ausgef\u00fchrt.", "already_setup": "Dieser Eintrag wurde bereits eingerichtet.", - "already_setup_with_diff_host_and_name": "Dieser Eintrag scheint bereits mit einem anderen Host und Namen basierend auf seiner Seriennummer eingerichtet worden zu sein. Bitte entfernen Sie alle alten Eintr\u00e4ge aus Ihrer configuration.yaml und aus dem Men\u00fc Integrationen, bevor Sie erneut versuchen, dieses Ger\u00e4t hinzuzuf\u00fcgen.", - "host_exists": "Vizio-Komponent mit bereits konfiguriertem Host.", - "name_exists": "Vizio-Komponent mit bereits konfiguriertem Namen.", - "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde.", - "updated_options": "Dieser Eintrag wurde bereits eingerichtet, aber die in der Konfiguration definierten Optionen stimmen nicht mit den zuvor importierten Optionswerten \u00fcberein, daher wurde der Konfigurationseintrag entsprechend aktualisiert.", - "updated_volume_step": "Dieser Eintrag wurde bereits eingerichtet, aber die Lautst\u00e4rken-Schrittgr\u00f6\u00dfe in der Konfiguration stimmt nicht mit dem Konfigurationseintrag \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." + "updated_entry": "Dieser Eintrag wurde bereits eingerichtet, aber der Name, die Apps und / oder die in der Konfiguration definierten Optionen stimmen nicht mit der zuvor importierten Konfiguration \u00fcberein, sodass der Konfigurationseintrag entsprechend aktualisiert wurde." }, "error": { "cant_connect": "Es konnte keine Verbindung zum Ger\u00e4t hergestellt werden. [\u00dcberpr\u00fcfen Sie die Dokumentation] (https://www.home-assistant.io/integrations/vizio/) und \u00fcberpr\u00fcfen Sie Folgendes erneut:\n- Das Ger\u00e4t ist eingeschaltet\n- Das Ger\u00e4t ist mit dem Netzwerk verbunden\n- Die von Ihnen eingegebenen Werte sind korrekt\nbevor sie versuchen, erneut zu \u00fcbermitteln.", "complete_pairing failed": "Das Pairing kann nicht abgeschlossen werden. Stellen Sie sicher, dass die von Ihnen angegebene PIN korrekt ist und das Fernsehger\u00e4t weiterhin mit Strom versorgt und mit dem Netzwerk verbunden ist, bevor Sie es erneut versuchen.", "host_exists": "Vizio-Ger\u00e4t mit angegebenem Host bereits konfiguriert.", - "name_exists": "Vizio-Ger\u00e4t mit angegebenem Namen bereits konfiguriert.", - "tv_needs_token": "Wenn der Ger\u00e4tetyp \"TV\" ist, wird ein g\u00fcltiger Zugriffstoken ben\u00f6tigt." + "name_exists": "Vizio-Ger\u00e4t mit angegebenem Namen bereits konfiguriert." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Ihr Vizio SmartCast-Fernseher ist jetzt mit Home Assistant verbunden. \n\n Ihr Zugriffstoken ist '**{access_token}**'.", "title": "Kopplung abgeschlossen" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Apps zum Einschlie\u00dfen oder Ausschlie\u00dfen", - "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?" - }, - "description": "Wenn Sie \u00fcber ein Smart TV verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen. Sie k\u00f6nnen diesen Schritt f\u00fcr Fernsehger\u00e4te \u00fcberspringen, die keine Apps unterst\u00fctzen.", - "title": "Konfigurieren Sie Apps f\u00fcr Ihr Smart TV" - }, "user": { "data": { "access_token": "Zugangstoken", @@ -50,14 +35,6 @@ }, "description": "Ein Zugriffstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn Sie ein Fernsehger\u00e4t konfigurieren und noch kein Zugriffstoken haben, lassen Sie es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", "title": "Richten Sie das Vizio SmartCast-Ger\u00e4t ein" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Apps zum Einschlie\u00dfen oder Ausschlie\u00dfen", - "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?" - }, - "description": "Wenn Sie \u00fcber ein Smart TV verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen. Sie k\u00f6nnen diesen Schritt f\u00fcr Fernsehger\u00e4te \u00fcberspringen, die keine Apps unterst\u00fctzen.", - "title": "Konfigurieren Sie Apps f\u00fcr Ihr Smart TV" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "Apps zum Einschlie\u00dfen oder Ausschlie\u00dfen", "include_or_exclude": "Apps einschlie\u00dfen oder ausschlie\u00dfen?", - "timeout": "API Request Timeout (Sekunden)", "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", diff --git a/homeassistant/components/vizio/.translations/en.json b/homeassistant/components/vizio/.translations/en.json index ec82f41c079..f4b03e1eb82 100644 --- a/homeassistant/components/vizio/.translations/en.json +++ b/homeassistant/components/vizio/.translations/en.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Config flow for vizio component already in progress.", "already_setup": "This entry has already been setup.", - "already_setup_with_diff_host_and_name": "This entry appears to have already been setup with a different host and name based on its serial number. Please remove any old entries from your configuration.yaml and from the Integrations menu before reattempting to add this device.", - "host_exists": "Vizio component with host already configured.", - "name_exists": "Vizio component with name already configured.", - "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly.", - "updated_options": "This entry has already been setup but the options defined in the config do not match the previously imported options values so the config entry has been updated accordingly.", - "updated_volume_step": "This entry has already been setup but the volume step size in the config does not match the config entry so the config entry has been updated accordingly." + "updated_entry": "This entry has already been setup but the name, apps, and/or options defined in the configuration do not match the previously imported configuration, so the configuration entry has been updated accordingly." }, "error": { "cant_connect": "Could not connect to the device. [Review the docs](https://www.home-assistant.io/integrations/vizio/) and re-verify that:\n- The device is powered on\n- The device is connected to the network\n- The values you filled in are accurate\nbefore attempting to resubmit.", "complete_pairing failed": "Unable to complete pairing. Ensure the PIN you provided is correct and the TV is still powered and connected to the network before resubmitting.", "host_exists": "Vizio device with specified host already configured.", - "name_exists": "Vizio device with specified name already configured.", - "tv_needs_token": "When Device Type is `tv` then a valid Access Token is needed." + "name_exists": "Vizio device with specified name already configured." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Your Vizio SmartCast TV is now connected to Home Assistant.\n\nYour Access Token is '**{access_token}**'.", "title": "Pairing Complete" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Apps to Include or Exclude", - "include_or_exclude": "Include or Exclude Apps?" - }, - "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list. You can skip this step for TVs that don't support apps.", - "title": "Configure Apps for Smart TV" - }, "user": { "data": { "access_token": "Access Token", @@ -50,14 +35,6 @@ }, "description": "An Access Token is only needed for TVs. If you are configuring a TV and do not have an Access Token yet, leave it blank to go through a pairing process.", "title": "Setup Vizio SmartCast Device" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Apps to Include or Exclude", - "include_or_exclude": "Include or Exclude Apps?" - }, - "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list. You can skip this step for TVs that don't support apps.", - "title": "Configure Apps for Smart TV" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "Apps to Include or Exclude", "include_or_exclude": "Include or Exclude Apps?", - "timeout": "API Request Timeout (seconds)", "volume_step": "Volume Step Size" }, "description": "If you have a Smart TV, you can optionally filter your source list by choosing which apps to include or exclude in your source list.", diff --git a/homeassistant/components/vizio/.translations/es.json b/homeassistant/components/vizio/.translations/es.json index 68e855fa5a8..eb35fbb0b5b 100644 --- a/homeassistant/components/vizio/.translations/es.json +++ b/homeassistant/components/vizio/.translations/es.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Configurar el flujo para el componente vizio que ya est\u00e1 en marcha.", "already_setup": "Esta entrada ya ha sido configurada.", - "already_setup_with_diff_host_and_name": "Esta entrada parece haber sido ya configurada con un host y un nombre diferentes basados en su n\u00famero de serie. Elimine las entradas antiguas de su archivo configuration.yaml y del men\u00fa Integraciones antes de volver a intentar agregar este dispositivo.", - "host_exists": "Host ya configurado del componente de Vizio", - "name_exists": "Nombre ya configurado del componente de Vizio", - "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.", - "updated_options": "Esta entrada ya ha sido configurada pero las opciones definidas en la configuraci\u00f3n no coinciden con los valores de las opciones importadas previamente, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia.", - "updated_volume_step": "Esta entrada ya ha sido configurada pero el tama\u00f1o del paso de volumen en la configuraci\u00f3n no coincide con la entrada de la configuraci\u00f3n, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." + "updated_entry": "Esta entrada ya ha sido configurada pero el nombre y/o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n previamente importada, por lo que la entrada de la configuraci\u00f3n ha sido actualizada en consecuencia." }, "error": { "cant_connect": "No se pudo conectar al dispositivo. [Revise los documentos] (https://www.home-assistant.io/integrations/vizio/) y vuelva a verificar que:\n- El dispositivo est\u00e1 encendido\n- El dispositivo est\u00e1 conectado a la red\n- Los valores que ha rellenado son precisos\nantes de intentar volver a enviar.", "complete_pairing failed": "No se pudo completar el emparejamiento. Aseg\u00farate de que el PIN que has proporcionado es correcto y que el televisor sigue encendido y conectado a la red antes de volver a enviarlo.", "host_exists": "El host ya est\u00e1 configurado.", - "name_exists": "Nombre ya configurado.", - "tv_needs_token": "Cuando el tipo de dispositivo es `tv`, se necesita un token de acceso v\u00e1lido." + "name_exists": "Nombre ya configurado." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Su dispositivo Vizio SmartCast TV ahora est\u00e1 conectado a Home Assistant.\n\nEl Token de Acceso es '**{access_token}**'.", "title": "Emparejamiento Completado" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", - "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?" - }, - "description": "Si tiene un Smart TV, opcionalmente puede filtrar su lista de origen eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de origen. Puede omitir este paso para televisores que no admiten aplicaciones.", - "title": "Configurar aplicaciones para Smart TV" - }, "user": { "data": { "access_token": "Token de acceso", @@ -50,14 +35,6 @@ }, "description": "Todos los campos son obligatorios excepto el Token de Acceso. Si decides no proporcionar un Token de Acceso y tu Tipo de Dispositivo es \"tv\", se te llevar\u00e1 por un proceso de emparejamiento con tu dispositivo para que se pueda recuperar un Token de Acceso.\n\nPara pasar por el proceso de emparejamiento, antes de pulsar en Enviar, aseg\u00farese de que tu TV est\u00e9 encendida y conectada a la red. Tambi\u00e9n es necesario poder ver la pantalla.", "title": "Configurar el cliente de Vizio SmartCast" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", - "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?" - }, - "description": "Si tienes un Smart TV, puedes opcionalmente filtrar tu lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en tu lista de fuentes. Puedes omitir este paso para televisores que no admiten aplicaciones.", - "title": "Configurar aplicaciones para Smart TV" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?", - "timeout": "Tiempo de espera de solicitud de API (segundos)", "volume_step": "Tama\u00f1o del paso de volumen" }, "description": "Si tienes un Smart TV, opcionalmente puedes filtrar su lista de fuentes eligiendo qu\u00e9 aplicaciones incluir o excluir en su lista de fuentes.", diff --git a/homeassistant/components/vizio/.translations/fr.json b/homeassistant/components/vizio/.translations/fr.json index bf672e9dfb9..0c0ff56af69 100644 --- a/homeassistant/components/vizio/.translations/fr.json +++ b/homeassistant/components/vizio/.translations/fr.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Flux de configuration pour le composant Vizio d\u00e9j\u00e0 en cours.", "already_setup": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e.", - "already_setup_with_diff_host_and_name": "Cette entr\u00e9e semble avoir d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e avec un h\u00f4te et un nom diff\u00e9rents en fonction de son num\u00e9ro de s\u00e9rie. Veuillez supprimer toutes les anciennes entr\u00e9es de votre configuration.yaml et du menu Int\u00e9grations avant de r\u00e9essayer d'ajouter ce p\u00e9riph\u00e9rique.", - "host_exists": "Composant Vizio avec h\u00f4te d\u00e9j\u00e0 configur\u00e9.", - "name_exists": "Composant Vizio dont le nom est d\u00e9j\u00e0 configur\u00e9.", - "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", - "updated_options": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais les options d\u00e9finies dans la configuration ne correspondent pas aux valeurs des options pr\u00e9c\u00e9demment import\u00e9es, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence.", - "updated_volume_step": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e, mais la taille du pas du volume dans la configuration ne correspond pas \u00e0 l'entr\u00e9e de configuration, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." + "updated_entry": "Cette entr\u00e9e a d\u00e9j\u00e0 \u00e9t\u00e9 configur\u00e9e mais le nom et/ou les options d\u00e9finis dans la configuration ne correspondent pas \u00e0 la configuration pr\u00e9c\u00e9demment import\u00e9e, de sorte que l'entr\u00e9e de configuration a \u00e9t\u00e9 mise \u00e0 jour en cons\u00e9quence." }, "error": { "cant_connect": "Impossible de se connecter \u00e0 l'appareil. [V\u00e9rifier les documents](https://www.home-assistant.io/integrations/vizio/) et rev\u00e9rifier que:\n- L'appareil est sous tension\n- L'appareil est connect\u00e9 au r\u00e9seau\n- Les valeurs que vous avez saisies sont exactes\navant d'essayer de le soumettre \u00e0 nouveau.", "complete_pairing failed": "Impossible de terminer l'appariement. Assurez-vous que le code PIN que vous avez fourni est correct et que le t\u00e9l\u00e9viseur est toujours aliment\u00e9 et connect\u00e9 au r\u00e9seau avant de soumettre \u00e0 nouveau.", "host_exists": "H\u00f4te d\u00e9j\u00e0 configur\u00e9.", - "name_exists": "Nom d\u00e9j\u00e0 configur\u00e9.", - "tv_needs_token": "Lorsque le type de p\u00e9riph\u00e9rique est \" TV \", un jeton d'acc\u00e8s valide est requis." + "name_exists": "Nom d\u00e9j\u00e0 configur\u00e9." }, "step": { "pair_tv": { @@ -31,13 +24,6 @@ "pairing_complete_import": { "title": "Appairage termin\u00e9" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Applications \u00e0 inclure ou \u00e0 exclure", - "include_or_exclude": "Inclure ou exclure des applications?" - }, - "title": "Configurer les applications pour Smart TV" - }, "user": { "data": { "access_token": "Jeton d'acc\u00e8s", @@ -47,13 +33,6 @@ }, "description": "Un jeton d'acc\u00e8s n'est n\u00e9cessaire que pour les t\u00e9l\u00e9viseurs. Si vous configurez un t\u00e9l\u00e9viseur et que vous n'avez pas encore de jeton d'acc\u00e8s, laissez-le vide pour passer par un processus de couplage.", "title": "Configurer le client Vizio SmartCast" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Applications \u00e0 inclure ou \u00e0 exclure", - "include_or_exclude": "Inclure ou exclure des applications?" - }, - "title": "Configurer les applications pour Smart TV" } }, "title": "Vizio SmartCast" @@ -64,7 +43,6 @@ "data": { "apps_to_include_or_exclude": "Applications \u00e0 inclure ou \u00e0 exclure", "include_or_exclude": "Inclure ou exclure des applications?", - "timeout": "D\u00e9lai d'expiration de la demande d'API (secondes)", "volume_step": "Taille du pas de volume" }, "description": "Si vous avez une Smart TV, vous pouvez \u00e9ventuellement filtrer votre liste de sources en choisissant les applications \u00e0 inclure ou \u00e0 exclure dans votre liste de sources.", diff --git a/homeassistant/components/vizio/.translations/hu.json b/homeassistant/components/vizio/.translations/hu.json index 650d5133dbd..c8b74f33e3d 100644 --- a/homeassistant/components/vizio/.translations/hu.json +++ b/homeassistant/components/vizio/.translations/hu.json @@ -1,20 +1,13 @@ { "config": { "abort": { - "already_in_progress": "A vizio komponens konfigur\u00e1ci\u00f3s folyamata m\u00e1r folyamatban van.", "already_setup": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva.", - "already_setup_with_diff_host_and_name": "\u00dagy t\u0171nik, hogy ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva egy m\u00e1sik \u00e1llom\u00e1ssal \u00e9s n\u00e9vvel a sorozatsz\u00e1ma alapj\u00e1n. T\u00e1vol\u00edtsa el a r\u00e9gi bejegyz\u00e9seket a configuration.yaml \u00e9s az Integr\u00e1ci\u00f3k men\u00fcb\u0151l, miel\u0151tt \u00fajra megpr\u00f3b\u00e1ln\u00e1 hozz\u00e1adni ezt az eszk\u00f6zt.", - "host_exists": "Vizio-\u00f6sszetev\u0151, amelynek az kiszolg\u00e1l\u00f3neve m\u00e1r konfigur\u00e1lva van.", - "name_exists": "Vizio-\u00f6sszetev\u0151, amelynek neve m\u00e1r konfigur\u00e1lva van.", - "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.", - "updated_options": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban megadott be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt be\u00e1ll\u00edt\u00e1si \u00e9rt\u00e9kekkel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt.", - "updated_volume_step": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban l\u00e9v\u0151 henger\u0151l\u00e9p\u00e9s m\u00e9rete nem egyezik meg a konfigur\u00e1ci\u00f3s bejegyz\u00e9ssel, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." + "updated_entry": "Ez a bejegyz\u00e9s m\u00e1r be van \u00e1ll\u00edtva, de a konfigur\u00e1ci\u00f3ban defini\u00e1lt n\u00e9v, appok \u00e9s/vagy be\u00e1ll\u00edt\u00e1sok nem egyeznek meg a kor\u00e1bban import\u00e1lt konfigur\u00e1ci\u00f3val, \u00edgy a konfigur\u00e1ci\u00f3s bejegyz\u00e9s ennek megfelel\u0151en friss\u00fclt." }, "error": { "cant_connect": "Nem lehetett csatlakozni az eszk\u00f6zh\u00f6z. [Tekintsd \u00e1t a dokumentumokat] (https://www.home-assistant.io/integrations/vizio/) \u00e9s \u00fajra ellen\u0151rizd, hogy:\n- A k\u00e9sz\u00fcl\u00e9k be van kapcsolva\n- A k\u00e9sz\u00fcl\u00e9k csatlakozik a h\u00e1l\u00f3zathoz\n- A kit\u00f6lt\u00f6tt \u00e9rt\u00e9kek pontosak\nmiel\u0151tt \u00fajra elk\u00fclden\u00e9d.", "host_exists": "A megadott kiszolg\u00e1l\u00f3n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", - "name_exists": "A megadott n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van.", - "tv_needs_token": "Ha az eszk\u00f6z t\u00edpusa \"tv\", akkor \u00e9rv\u00e9nyes hozz\u00e1f\u00e9r\u00e9si tokenre van sz\u00fcks\u00e9g." + "name_exists": "A megadott n\u00e9vvel rendelkez\u0151 Vizio-eszk\u00f6z m\u00e1r konfigur\u00e1lva van." }, "step": { "user": { @@ -32,7 +25,6 @@ "step": { "init": { "data": { - "timeout": "API-k\u00e9r\u00e9s id\u0151t\u00fall\u00e9p\u00e9se (m\u00e1sodpercben)", "volume_step": "Hanger\u0151 l\u00e9p\u00e9s nagys\u00e1ga" }, "title": "Friss\u00edtse a Vizo SmartCast be\u00e1ll\u00edt\u00e1sokat" diff --git a/homeassistant/components/vizio/.translations/it.json b/homeassistant/components/vizio/.translations/it.json index eef86bf78cb..4a26a40ad56 100644 --- a/homeassistant/components/vizio/.translations/it.json +++ b/homeassistant/components/vizio/.translations/it.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Il flusso di configurazione per vizio \u00e8 gi\u00e0 in corso.", "already_setup": "Questa voce \u00e8 gi\u00e0 stata configurata.", - "already_setup_with_diff_host_and_name": "Sembra che questa voce sia gi\u00e0 stata configurata con un host e un nome diversi in base al suo numero seriale. Rimuovere eventuali voci precedenti da configuration.yaml e dal menu Integrazioni prima di tentare nuovamente di aggiungere questo dispositivo.", - "host_exists": "Componente Vizio con host gi\u00e0 configurato.", - "name_exists": "Componente Vizio con nome gi\u00e0 configurato.", - "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome, le app e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza.", - "updated_options": "Questa voce \u00e8 gi\u00e0 stata impostata, ma le opzioni definite nella configurazione non corrispondono ai valori delle opzioni importate in precedenza, quindi la voce di configurazione \u00e8 stata aggiornata di conseguenza.", - "updated_volume_step": "Questa voce \u00e8 gi\u00e0 stata impostata, ma la dimensione del passo del volume nella configurazione non corrisponde alla voce di configurazione, quindi \u00e8 stata aggiornata di conseguenza." + "updated_entry": "Questa voce \u00e8 gi\u00e0 stata configurata, ma il nome, le app e/o le opzioni definite nella configurazione non corrispondono alla configurazione importata in precedenza, pertanto la voce di configurazione \u00e8 stata aggiornata di conseguenza." }, "error": { "cant_connect": "Impossibile connettersi al dispositivo. [Esamina i documenti] (https://www.home-assistant.io/integrations/vizio/) e verifica nuovamente che: \n - Il dispositivo sia acceso \n - Il dispositivo sia collegato alla rete \n - I valori inseriti siano corretti \n prima di ritentare.", "complete_pairing failed": "Impossibile completare l'associazione. Assicurarsi che il PIN fornito sia corretto e che il televisore sia ancora alimentato e connesso alla rete prima di inviarlo di nuovo.", "host_exists": "Dispositivo Vizio con host specificato gi\u00e0 configurato.", - "name_exists": "Dispositivo Vizio con il nome specificato gi\u00e0 configurato.", - "tv_needs_token": "Quando Device Type \u00e8 `tv`, \u00e8 necessario un token di accesso valido." + "name_exists": "Dispositivo Vizio con il nome specificato gi\u00e0 configurato." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Il dispositivo Vizio SmartCast TV \u00e8 ora connesso a Home Assistant. \n\nIl tuo Token di Accesso \u00e8 '**{access_token}**'.", "title": "Associazione completata" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "App da includere o escludere", - "include_or_exclude": "Includere o Escludere le App?" - }, - "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare facoltativamente l'elenco di origine scegliendo le app da includere o escludere in esso. \u00c8 possibile saltare questo passaggio per i televisori che non supportano le app.", - "title": "Configura le app per Smart TV" - }, "user": { "data": { "access_token": "Token di accesso", @@ -50,14 +35,6 @@ }, "description": "Un Token di Accesso \u00e8 necessario solo per i televisori. Se si sta configurando un televisore e non si dispone ancora di un Token di Accesso, lasciarlo vuoto per passare attraverso un processo di associazione.", "title": "Configurazione del dispositivo SmartCast Vizio" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "App da Includere o Escludere", - "include_or_exclude": "Includere o Escludere le App?" - }, - "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare facoltativamente l'elenco di origine scegliendo le app da includere o escludere in esso. \u00c8 possibile saltare questo passaggio per i televisori che non supportano le app.", - "title": "Configura le app per Smart TV" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "App da includere o escludere", "include_or_exclude": "Includere o escludere app?", - "timeout": "Timeout richiesta API (secondi)", "volume_step": "Dimensione del passo del volume" }, "description": "Se si dispone di una Smart TV, \u00e8 possibile filtrare l'elenco di origine scegliendo le app da includere o escludere in esso.", diff --git a/homeassistant/components/vizio/.translations/ko.json b/homeassistant/components/vizio/.translations/ko.json index 33edb72733a..df2fb243f88 100644 --- a/homeassistant/components/vizio/.translations/ko.json +++ b/homeassistant/components/vizio/.translations/ko.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "vizio \uad6c\uc131 \uc694\uc18c\uc5d0 \ub300\ud55c \uad6c\uc131 \ud50c\ub85c\uc6b0\uac00 \uc774\ubbf8 \uc9c4\ud589 \uc911\uc785\ub2c8\ub2e4.", "already_setup": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "already_setup_with_diff_host_and_name": "\uc774 \ud56d\ubaa9\uc740 \uc2dc\ub9ac\uc5bc \ubc88\ud638\ub85c \ub2e4\ub978 \ud638\uc2a4\ud2b8 \ubc0f \uc774\ub984\uc73c\ub85c \uc774\ubbf8 \uc124\uc815\ub418\uc5b4\uc788\ub294 \uac83\uc73c\ub85c \ubcf4\uc785\ub2c8\ub2e4. \uc774 \uae30\uae30\ub97c \ucd94\uac00\ud558\uae30 \uc804\uc5d0 configuration.yaml \ubc0f \ud1b5\ud569 \uad6c\uc131\uc694\uc18c \uba54\ub274\uc5d0\uc11c \uc774\uc804 \ud56d\ubaa9\uc744 \uc81c\uac70\ud574\uc8fc\uc138\uc694.", - "host_exists": "\ud574\ub2f9 \ud638\uc2a4\ud2b8\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "name_exists": "\ud574\ub2f9 \uc774\ub984\uc758 Vizio \uad6c\uc131 \uc694\uc18c\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "updated_options": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uc635\uc158 \uac12\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "updated_volume_step": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc758 \ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30\uac00 \uad6c\uc131 \ud56d\ubaa9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." + "updated_entry": "\uc774 \ud56d\ubaa9\uc740 \uc774\ubbf8 \uc124\uc815\ub418\uc5c8\uc9c0\ub9cc \uad6c\uc131\uc5d0 \uc815\uc758\ub41c \uc774\ub984, \uc571 \ud639\uc740 \uc635\uc158\uc774 \uc774\uc804\uc5d0 \uac00\uc838\uc628 \uad6c\uc131 \ub0b4\uc6a9\uacfc \uc77c\uce58\ud558\uc9c0 \uc54a\uc73c\ubbc0\ub85c \uad6c\uc131 \ud56d\ubaa9\uc774 \uadf8\uc5d0 \ub530\ub77c \uc5c5\ub370\uc774\ud2b8\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "error": { "cant_connect": "\uae30\uae30\uc5d0 \uc5f0\uacb0\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. [\uc548\ub0b4\ub97c \ucc38\uace0] (https://www.home-assistant.io/integrations/vizio/)\ud558\uace0 \uc591\uc2dd\uc744 \ub2e4\uc2dc \uc81c\ucd9c\ud558\uae30 \uc804\uc5d0 \ub2e4\uc74c\uc744 \ub2e4\uc2dc \ud655\uc778\ud574\uc8fc\uc138\uc694.\n- \uae30\uae30 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uc2b5\ub2c8\uae4c?\n- \uae30\uae30\uac00 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\uc2b5\ub2c8\uae4c?\n- \uc785\ub825\ud55c \ub0b4\uc6a9\uc774 \uc62c\ubc14\ub985\ub2c8\uae4c?", "complete_pairing failed": "\ud398\uc5b4\ub9c1\uc744 \uc644\ub8cc\ud560 \uc218 \uc5c6\uc2b5\ub2c8\ub2e4. \uc785\ub825\ud55c PIN \uc774 \uc62c\ubc14\ub978\uc9c0 \ud655\uc778\ud558\uace0 \ub2e4\uc74c \uacfc\uc815\uc744 \uc9c4\ud589\ud558\uae30 \uc804\uc5d0 TV \uc758 \uc804\uc6d0\uc774 \ucf1c\uc838 \uc788\uace0 \ub124\ud2b8\uc6cc\ud06c\uc5d0 \uc5f0\uacb0\ub418\uc5b4 \uc788\ub294\uc9c0 \ud655\uc778\ud574\uc8fc\uc138\uc694.", "host_exists": "\uc124\uc815\ub41c \ud638\uc2a4\ud2b8\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "tv_needs_token": "\uae30\uae30 \uc720\ud615\uc774 'tv' \uc778 \uacbd\uc6b0 \uc720\ud6a8\ud55c \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \ud544\uc694\ud569\ub2c8\ub2e4." + "name_exists": "\uc124\uc815\ub41c \uc774\ub984\uc758 Vizio \uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Vizio SmartCast TV \uac00 Home Assistant \uc5d0 \uc5f0\uacb0\ub418\uc5c8\uc2b5\ub2c8\ub2e4. \n\n\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 '**{access_token}**' \uc785\ub2c8\ub2e4.", "title": "\ud398\uc5b4\ub9c1 \uc644\ub8cc" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", - "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - }, - "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc571\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 TV\uc758 \uacbd\uc6b0 \uc774 \ub2e8\uacc4\ub97c \uac74\ub108\ub6f8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "\uc2a4\ub9c8\ud2b8 TV \uc6a9 \uc571 \uad6c\uc131" - }, "user": { "data": { "access_token": "\uc561\uc138\uc2a4 \ud1a0\ud070", @@ -50,14 +35,6 @@ }, "description": "\uc561\uc138\uc2a4 \ud1a0\ud070\uc740 TV \uc5d0\ub9cc \ud544\uc694\ud569\ub2c8\ub2e4. TV \ub97c \uad6c\uc131\ud558\uace0 \uc788\uace0 \uc544\uc9c1 \uc561\uc138\uc2a4 \ud1a0\ud070\uc774 \uc5c6\ub294 \uacbd\uc6b0 \ud398\uc5b4\ub9c1 \uacfc\uc815\uc744 \uc9c4\ud589\ud558\ub824\uba74 \ube44\uc6cc\ub450\uc138\uc694.", "title": "Vizio SmartCast \uae30\uae30 \uc124\uc815" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", - "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?" - }, - "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4. \uc571\uc744 \uc9c0\uc6d0\ud558\uc9c0 \uc54a\ub294 TV\uc758 \uacbd\uc6b0 \uc774 \ub2e8\uacc4\ub97c \uac74\ub108\ub6f8 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", - "title": "\uc2a4\ub9c8\ud2b8 TV \uc6a9 \uc571 \uad6c\uc131" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "\ud3ec\ud568 \ub610\ub294 \uc81c\uc678 \ud560 \uc571", "include_or_exclude": "\uc571\uc744 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud558\uc2dc\uaca0\uc2b5\ub2c8\uae4c?", - "timeout": "API \uc694\uccad \uc2dc\uac04 \ucd08\uacfc (\ucd08)", "volume_step": "\ubcfc\ub968 \ub2e8\uacc4 \ud06c\uae30" }, "description": "\uc2a4\ub9c8\ud2b8 TV \uac00 \uc788\ub294 \uacbd\uc6b0 \uc120\ud0dd\uc0ac\ud56d\uc73c\ub85c \uc18c\uc2a4 \ubaa9\ub85d\uc5d0 \ud3ec\ud568 \ub610\ub294 \uc81c\uc678\ud560 \uc571\uc744 \uc120\ud0dd\ud558\uc5ec \uc18c\uc2a4 \ubaa9\ub85d\uc744 \ud544\ud130\ub9c1\ud560 \uc218 \uc788\uc2b5\ub2c8\ub2e4.", diff --git a/homeassistant/components/vizio/.translations/lb.json b/homeassistant/components/vizio/.translations/lb.json index 79dfa120db2..3146c8756a8 100644 --- a/homeassistant/components/vizio/.translations/lb.json +++ b/homeassistant/components/vizio/.translations/lb.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Konfiguratioun's Oflaf fir Vizio Komponent ass schonn am gaangen.", "already_setup": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert.", - "already_setup_with_diff_host_and_name": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mat engem aneren Host an Numm bas\u00e9ierend unhand vu\u00a0senger Seriennummer. L\u00e4scht w.e.g. al Entr\u00e9e vun \u00e4rer configuration.yaml a\u00a0vum Integratioun's Men\u00fc ier dir prob\u00e9iert d\u00ebsen Apparate r\u00ebm b\u00e4i ze setzen.", - "host_exists": "Vizio Komponent mam Host ass schon konfigur\u00e9iert.", - "name_exists": "Vizio Komponent mam Numm ass scho konfigur\u00e9iert.", - "updated_entry": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9ierten Numm an/oder Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert.", - "updated_options": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9iert Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert.", - "updated_volume_step": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9iert Lautst\u00e4erkt Schr\u00ebtt Gr\u00e9isst an der Konfiguratioun st\u00ebmmt net mat der Konfiguratioun iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert." + "updated_entry": "D\u00ebs Entr\u00e9e ass scho konfigur\u00e9iert mee d\u00e9i defin\u00e9ierten Numm an/oder Optiounen an der Konfiguratioun st\u00ebmmen net mat deene virdrun import\u00e9ierten Optiounen iwwereneen, esou gouf d'Entr\u00e9e deementspriechend aktualis\u00e9iert." }, "error": { "cant_connect": "Konnt sech net mam Apparat verbannen. [Iwwerpr\u00e9ift Dokumentatioun] (https://www.home-assistant.io/integrations/vizio/) a stellt s\u00e9cher dass:\n- Den Apparat ass un\n- Den Apparat ass mam Netzwierk verbonnen\n- D'Optiounen d\u00e9i dir aginn hutt si korrekt\nier dir d'Verbindung nees prob\u00e9iert", "complete_pairing failed": "Feeler beim ofschl\u00e9isse vun der Kopplung. Iwwerpr\u00e9if dass de PIN korrekt an da de Fernsee nach \u00ebmmer ugeschalt a mam Netzwierk verbonnen ass ier de n\u00e4chste Versuch gestart g\u00ebtt.", "host_exists": "Vizio Apparat mat d\u00ebsem Host ass scho konfigur\u00e9iert.", - "name_exists": "Vizio Apparat mat d\u00ebsen Numm ass scho konfigur\u00e9iert.", - "tv_needs_token": "Wann den Typ vum Apparat `tv`ass da g\u00ebtt ee g\u00ebltegen Acc\u00e8s Jeton ben\u00e9idegt." + "name_exists": "Vizio Apparat mat d\u00ebsen Numm ass scho konfigur\u00e9iert." }, "step": { "pair_tv": { @@ -33,13 +26,6 @@ "description": "D\u00e4in Visio SmartCast Apparat ass elo mam Home Assistant verbonnen.\n\nD\u00e4in Acc\u00e8s Jeton ass '**{access_token}**'.", "title": "Kopplung ofgeschloss" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", - "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" - }, - "title": "Apps fir Smart TV konfigur\u00e9ieren" - }, "user": { "data": { "access_token": "Acc\u00e8ss Jeton", @@ -47,15 +33,8 @@ "host": ":", "name": "Numm" }, - "description": "All Felder sinn noutwendeg ausser Acc\u00e8s Jeton. Wann keen Acc\u00e8s Jeton uginn ass, an den Typ vun Apparat ass 'TV', da g\u00ebtt e Kopplungs Prozess mam Apparat gestart fir een Acc\u00e8s Jeton z'erstellen.\n\nFir de Kopplung Prozess ofzesch\u00e9issen,ier op \"ofsch\u00e9cken\" klickt, pr\u00e9ift datt de Fernsee ugeschalt a mam Netzwierk verbonnen ass. Du muss och k\u00ebnnen op de Bildschierm gesinn.", - "title": "Vizo Smartcast ariichten" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", - "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?" - }, - "title": "Apps fir Smart TV konfigur\u00e9ieren" + "description": "Een Access Jeton g\u00ebtt nn\u00ebmme fir Fernseher gebraucht. Wann Dir e Fernseh konfigur\u00e9iert a keen Access Jeton hutt, da loosst et eidel fir duerch dee Pairing Prozess ze goen.", + "title": "Vizo Smartcast Apparat ariichten" } }, "title": "Vizio SmartCast" @@ -66,9 +45,9 @@ "data": { "apps_to_include_or_exclude": "Apps fir mat abegr\u00e4ifen oder auszeschl\u00e9issen", "include_or_exclude": "Apps mat abez\u00e9ien oder auschl\u00e9issen?", - "timeout": "Z\u00e4itiwwerscheidung bei der Ufro vun der API (sekonnen)", "volume_step": "Lautst\u00e4erkt Schr\u00ebtt Gr\u00e9isst" }, + "description": "Falls du ee Smart TV hues kanns du d'Quelle L\u00ebscht optionell filteren andeems du d'Apps auswiels d\u00e9i soll mat abegraff oder ausgeschloss ginn.", "title": "Vizo Smartcast Optiounen aktualis\u00e9ieren" } }, diff --git a/homeassistant/components/vizio/.translations/nl.json b/homeassistant/components/vizio/.translations/nl.json index bbc95d73bbc..797836e0145 100644 --- a/homeassistant/components/vizio/.translations/nl.json +++ b/homeassistant/components/vizio/.translations/nl.json @@ -1,20 +1,13 @@ { "config": { "abort": { - "already_in_progress": "Configuratie stroom voor vizio component al in uitvoering.", "already_setup": "Dit item is al ingesteld.", - "already_setup_with_diff_host_and_name": "Dit item lijkt al te zijn ingesteld met een andere host en naam op basis van het serienummer. Verwijder alle oude vermeldingen uit uw configuratie.yaml en uit het menu Integraties voordat u opnieuw probeert dit apparaat toe te voegen.", - "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", - "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.", - "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt.", - "updated_options": "Dit item is al ingesteld, maar de opties die in de configuratie zijn gedefinieerd komen niet overeen met de eerder ge\u00efmporteerde optiewaarden, dus de configuratie-invoer is dienovereenkomstig bijgewerkt.", - "updated_volume_step": "Dit item is al ingesteld, maar de volumestapgrootte in de configuratie komt niet overeen met het configuratie-item, dus het configuratie-item is dienovereenkomstig bijgewerkt." + "updated_entry": "Dit item is al ingesteld, maar de naam en/of opties die zijn gedefinieerd in de configuratie komen niet overeen met de eerder ge\u00efmporteerde configuratie, dus het configuratie-item is dienovereenkomstig bijgewerkt." }, "error": { "cant_connect": "Kan geen verbinding maken met het apparaat. [Bekijk de documenten] (https://www.home-assistant.io/integrations/vizio/) en controleer of:\n- Het apparaat is ingeschakeld\n- Het apparaat is aangesloten op het netwerk\n- De waarden die u ingevuld correct zijn\nvoordat u weer probeert om opnieuw in te dienen.", "host_exists": "Vizio apparaat met opgegeven host al geconfigureerd.", - "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd.", - "tv_needs_token": "Wanneer het apparaattype `tv` is, dan is er een geldig toegangstoken nodig." + "name_exists": "Vizio apparaat met opgegeven naam al geconfigureerd." }, "step": { "user": { @@ -33,7 +26,6 @@ "step": { "init": { "data": { - "timeout": "Time-out van API-aanvragen (seconden)", "volume_step": "Volume Stapgrootte" }, "title": "Update Vizo SmartCast Opties" diff --git a/homeassistant/components/vizio/.translations/no.json b/homeassistant/components/vizio/.translations/no.json index 6391ac20aa7..65e96945e46 100644 --- a/homeassistant/components/vizio/.translations/no.json +++ b/homeassistant/components/vizio/.translations/no.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Konfigurasjons flyt for Vizio komponent er allerede i gang.", "already_setup": "Denne oppf\u00f8ringen er allerede konfigurert.", - "already_setup_with_diff_host_and_name": "Denne oppf\u00f8ringen ser ut til \u00e5 allerede v\u00e6re konfigurert med en annen vert og navn basert p\u00e5 serienummeret. Fjern den gamle oppf\u00f8ringer fra konfigurasjonen.yaml og fra integrasjonsmenyen f\u00f8r du pr\u00f8ver ut \u00e5 legge til denne enheten p\u00e5 nytt.", - "host_exists": "Vizio komponent med vert allerede konfigurert.", - "name_exists": "Vizio-komponent med navn som allerede er konfigurert.", - "updated_entry": "Dette innlegget har allerede v\u00e6rt oppsett, men navnet, apps, og/eller alternativer som er definert i konfigurasjon som ikke stemmer med det som tidligere er importert konfigurasjon, s\u00e5 konfigurasjonen innlegget har blitt oppdatert i henhold til dette.", - "updated_options": "Denne oppf\u00f8ringen er allerede konfigurert, men alternativene som er definert i konfigurasjonen samsvarer ikke med de tidligere importerte alternativverdiene, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter.", - "updated_volume_step": "Denne oppf\u00f8ringen er allerede konfigurert, men volumstrinnst\u00f8rrelsen i konfigurasjonen samsvarer ikke med konfigurasjonsoppf\u00f8ringen, s\u00e5 konfigurasjonsoppf\u00f8ringen er oppdatert deretter." + "updated_entry": "Dette innlegget har allerede v\u00e6rt oppsett, men navnet, apps, og/eller alternativer som er definert i konfigurasjon som ikke stemmer med det som tidligere er importert konfigurasjon, s\u00e5 konfigurasjonen innlegget har blitt oppdatert i henhold til dette." }, "error": { "cant_connect": "Kunne ikke koble til enheten. [Se gjennom dokumentene] (https://www.home-assistant.io/integrations/vizio/) og bekreft at: \n - Enheten er sl\u00e5tt p\u00e5 \n - Enheten er koblet til nettverket \n - Verdiene du fylte ut er n\u00f8yaktige \n f\u00f8r du pr\u00f8ver \u00e5 sende inn p\u00e5 nytt.", "complete_pairing failed": "Kan ikke fullf\u00f8re sammenkoblingen. Forsikre deg om at PIN-koden du oppga er riktig, og at TV-en fortsatt er p\u00e5 og tilkoblet nettverket f\u00f8r du sender inn p\u00e5 nytt.", "host_exists": "Vizio-enhet med spesifisert vert allerede konfigurert.", - "name_exists": "Vizio-enhet med spesifisert navn allerede konfigurert.", - "tv_needs_token": "N\u00e5r enhetstype er `tv`, er det n\u00f8dvendig med en gyldig tilgangstoken." + "name_exists": "Vizio-enhet med spesifisert navn allerede konfigurert." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Din Vizio SmartCast TV er n\u00e5 koblet til Home Assistant.\n\nTilgangstokenet er **{access_token}**.", "title": "Sammenkoblingen Er Fullf\u00f8rt" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Apper \u00e5 inkludere eller ekskludere", - "include_or_exclude": "Inkluder eller ekskludere apper?" - }, - "description": "Hvis du har en Smart TV, kan du eventuelt filtrere kildelisten din ved \u00e5 velge hvilke apper du vil inkludere eller ekskludere i kildelisten. Du kan hoppe over dette trinnet for TV-er som ikke st\u00f8tter apper.", - "title": "Konfigurere Apper for Smart TV" - }, "user": { "data": { "access_token": "Tilgangstoken", @@ -50,14 +35,6 @@ }, "description": "En tilgangstoken er bare n\u00f8dvendig for TV-er. Hvis du konfigurerer en TV og ikke har tilgangstoken enn\u00e5, m\u00e5 du la den st\u00e5 tom for \u00e5 g\u00e5 gjennom en sammenkoblingsprosess.", "title": "Sett opp Vizio SmartCast-enhet" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Apper \u00e5 inkludere eller ekskludere", - "include_or_exclude": "Inkluder eller ekskludere apper?" - }, - "description": "Hvis du har en Smart TV, kan du eventuelt filtrere kildelisten din ved \u00e5 velge hvilke apper du vil inkludere eller ekskludere i kildelisten. Du kan hoppe over dette trinnet for TV-er som ikke st\u00f8tter apper.", - "title": "Konfigurere Apper for Smart TV" } }, "title": "" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "Apper \u00e5 inkludere eller ekskludere", "include_or_exclude": "Inkluder eller ekskludere apper?", - "timeout": "Tidsavbrudd for API-foresp\u00f8rsel (sekunder)", "volume_step": "St\u00f8rrelse p\u00e5 volum trinn" }, "description": "Hvis du har en Smart-TV, kan du eventuelt filtrere kildelisten ved \u00e5 velge hvilke apper som skal inkluderes eller utelates i kildelisten.", diff --git a/homeassistant/components/vizio/.translations/pl.json b/homeassistant/components/vizio/.translations/pl.json index 91ed0b2dd53..2537279d998 100644 --- a/homeassistant/components/vizio/.translations/pl.json +++ b/homeassistant/components/vizio/.translations/pl.json @@ -1,20 +1,13 @@ { "config": { "abort": { - "already_in_progress": "Konfiguracja komponentu Vizio jest ju\u017c w trakcie.", "already_setup": "Ten komponent jest ju\u017c skonfigurowany.", - "already_setup_with_diff_host_and_name": "Wygl\u0105da na to, \u017ce ten wpis zosta\u0142 ju\u017c skonfigurowany z innym hostem i nazw\u0105 na podstawie jego numeru seryjnego. Usu\u0144 wszystkie stare wpisy z pliku configuration.yaml i z menu Integracje przed ponown\u0105 pr\u00f3b\u0105 dodania tego urz\u0105dzenia.", - "host_exists": "Komponent Vizio dla tego hosta jest ju\u017c skonfigurowany.", - "name_exists": "Komponent Vizio dla tej nazwy jest ju\u017c skonfigurowany.", - "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", - "updated_options": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany.", - "updated_volume_step": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale rozmiar skoku g\u0142o\u015bno\u015bci w konfiguracji nie pasuje do wpisu konfiguracji, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." + "updated_entry": "Ten wpis zosta\u0142 ju\u017c skonfigurowany, ale nazwa i/lub opcje zdefiniowane w konfiguracji nie pasuj\u0105 do wcze\u015bniej zaimportowanych warto\u015bci, wi\u0119c wpis konfiguracji zosta\u0142 odpowiednio zaktualizowany." }, "error": { "cant_connect": "Nie mo\u017cna po\u0142\u0105czy\u0107 si\u0119 z urz\u0105dzeniem. [Przejrzyj dokumentacj\u0119] (https://www.home-assistant.io/integrations/vizio/) i ponownie sprawd\u017a, czy: \n - urz\u0105dzenie jest w\u0142\u0105czone,\n - urz\u0105dzenie jest pod\u0142\u0105czone do sieci,\n - wprowadzone warto\u015bci s\u0105 prawid\u0142owe,\n przed pr\u00f3b\u0105 ponownego przes\u0142ania.", "host_exists": "Urz\u0105dzenie Vizio z okre\u015blonym hostem jest ju\u017c skonfigurowane.", - "name_exists": "Urz\u0105dzenie Vizio o okre\u015blonej nazwie jest ju\u017c skonfigurowane.", - "tv_needs_token": "Gdy typem urz\u0105dzenia jest `tv` potrzebny jest prawid\u0142owy token dost\u0119pu." + "name_exists": "Urz\u0105dzenie Vizio o okre\u015blonej nazwie jest ju\u017c skonfigurowane." }, "step": { "pair_tv": { @@ -28,14 +21,6 @@ "pairing_complete_import": { "title": "Parowanie zako\u0144czone" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", - "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji" - }, - "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", - "title": "Konfigurowanie aplikacji dla smart TV" - }, "user": { "data": { "access_token": "Token dost\u0119pu", @@ -44,14 +29,6 @@ "name": "Nazwa" }, "title": "Konfiguracja klienta Vizio SmartCast" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", - "include_or_exclude": "Do\u0142\u0105czy\u0107 czy wykluczy\u0107 aplikacje?" - }, - "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", - "title": "Skonfiguruj aplikacje dla Smart TV" } }, "title": "Vizio SmartCast" @@ -62,7 +39,6 @@ "data": { "apps_to_include_or_exclude": "Aplikacje do do\u0142\u0105czenia lub wykluczenia", "include_or_exclude": "Do\u0142\u0105czanie lub wykluczanie aplikacji", - "timeout": "Limit czasu \u017c\u0105dania API (sekundy)", "volume_step": "Skok g\u0142o\u015bno\u015bci" }, "description": "Je\u015bli telewizor obs\u0142uguje aplikacje, mo\u017cesz opcjonalnie filtrowa\u0107 aplikacje, kt\u00f3re maj\u0105 zosta\u0107 uwzgl\u0119dnione lub wykluczone z listy \u017ar\u00f3de\u0142. Mo\u017cesz pomin\u0105\u0107 ten krok dla telewizor\u00f3w, kt\u00f3re nie obs\u0142uguj\u0105 aplikacji.", diff --git a/homeassistant/components/vizio/.translations/ru.json b/homeassistant/components/vizio/.translations/ru.json index 9162b2b0fe6..e1e6ac73b9d 100644 --- a/homeassistant/components/vizio/.translations/ru.json +++ b/homeassistant/components/vizio/.translations/ru.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u044f\u0435\u0442\u0441\u044f.", "already_setup": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0431\u044b\u043b\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430.", - "already_setup_with_diff_host_and_name": "\u041f\u043e\u0445\u043e\u0436\u0435, \u0447\u0442\u043e \u044d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0431\u044b\u043b\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430 \u0441 \u0434\u0440\u0443\u0433\u0438\u043c \u0445\u043e\u0441\u0442\u043e\u043c \u0438 \u0438\u043c\u0435\u043d\u0435\u043c \u043d\u0430 \u043e\u0441\u043d\u043e\u0432\u0435 \u0435\u0433\u043e \u0441\u0435\u0440\u0438\u0439\u043d\u043e\u0433\u043e \u043d\u043e\u043c\u0435\u0440\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u0443\u0434\u0430\u043b\u0438\u0442\u0435 \u0432\u0441\u0435 \u0441\u0442\u0430\u0440\u044b\u0435 \u0437\u0430\u043f\u0438\u0441\u0438 \u0438\u0437 \u0412\u0430\u0448\u0435\u0433\u043e configuration.yaml \u0438 \u0438\u0437 \u0440\u0430\u0437\u0434\u0435\u043b\u0430 \"\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u0438\" \u0438 \u0437\u0430\u0442\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u0435 \u043f\u043e\u043f\u044b\u0442\u043a\u0443.", - "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "updated_entry": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", - "updated_options": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430.", - "updated_volume_step": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u0448\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0439 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u0435\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." + "updated_entry": "\u042d\u0442\u0430 \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430, \u043d\u043e \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u044b, \u043e\u043f\u0440\u0435\u0434\u0435\u043b\u0435\u043d\u043d\u044b\u0435 \u0432 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438, \u043d\u0435 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0442 \u0440\u0430\u043d\u0435\u0435 \u0438\u043c\u043f\u043e\u0440\u0442\u0438\u0440\u043e\u0432\u0430\u043d\u043d\u044b\u043c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f\u043c, \u043f\u043e\u044d\u0442\u043e\u043c\u0443 \u0437\u0430\u043f\u0438\u0441\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u0438 \u0431\u044b\u043b\u0430 \u0441\u043e\u043e\u0442\u0432\u0435\u0442\u0441\u0442\u0432\u0443\u044e\u0449\u0438\u043c \u043e\u0431\u0440\u0430\u0437\u043e\u043c \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430." }, "error": { "cant_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f \u043a \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c \u0432 \u0442\u043e\u043c, \u0447\u0442\u043e:\n- \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432\u043a\u043b\u044e\u0447\u0435\u043d\u043e;\n- \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a \u0441\u0435\u0442\u0438;\n- \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u0432\u0432\u0435\u043b\u0438 \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/integrations/vizio/) \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.", "complete_pairing failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435. \u041f\u0440\u0435\u0436\u0434\u0435 \u0447\u0435\u043c \u043f\u043e\u0432\u0442\u043e\u0440\u0438\u0442\u044c \u043f\u043e\u043f\u044b\u0442\u043a\u0443, \u0443\u0431\u0435\u0434\u0438\u0442\u0435\u0441\u044c, \u0447\u0442\u043e \u0432\u0432\u0435\u0434\u0435\u043d\u043d\u044b\u0439 \u0412\u0430\u043c\u0438 PIN-\u043a\u043e\u0434 \u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u044b\u0439, \u0430 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0432\u043a\u043b\u044e\u0447\u0435\u043d \u0438 \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d \u043a \u0441\u0435\u0442\u0438.", "host_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u044d\u0442\u043e\u0433\u043e \u0445\u043e\u0441\u0442\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430.", - "tv_needs_token": "\u0414\u043b\u044f \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0438\u044f `tv` \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430." + "name_exists": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0441 \u0442\u0430\u043a\u0438\u043c \u0436\u0435 \u043d\u0430\u0437\u0432\u0430\u043d\u0438\u0435\u043c \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e Vizio SmartCast \u0442\u0435\u043f\u0435\u0440\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0435\u043d\u043e \u043a Home Assistant. \n\n\u0412\u0430\u0448 \u0442\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 - '**{access_token}**'.", "title": "\u0421\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u043e" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", - "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f?" - }, - "description": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c Smart TV, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432, \u0432\u043a\u043b\u044e\u0447\u0438\u0432 \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u0448\u0430\u0433 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0434\u043b\u044f Smart TV" - }, "user": { "data": { "access_token": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430", @@ -50,14 +35,6 @@ }, "description": "\u0422\u043e\u043a\u0435\u043d \u0434\u043e\u0441\u0442\u0443\u043f\u0430 \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c \u0442\u043e\u043b\u044c\u043a\u043e \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432. \u0415\u0441\u043b\u0438 \u0412\u044b \u043d\u0430\u0441\u0442\u0440\u0430\u0438\u0432\u0430\u0435\u0442\u0435 \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440 \u0438 \u0443 \u0412\u0430\u0441 \u0435\u0449\u0435 \u043d\u0435\u0442 \u0442\u043e\u043a\u0435\u043d\u0430 \u0434\u043e\u0441\u0442\u0443\u043f\u0430, \u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u044d\u0442\u043e \u043f\u043e\u043b\u0435 \u043f\u0443\u0441\u0442\u044b\u043c, \u0447\u0442\u043e\u0431\u044b \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043f\u0440\u043e\u0446\u0435\u0441\u0441 \u0441\u043e\u043f\u0440\u044f\u0436\u0435\u043d\u0438\u044f.", "title": "Vizio SmartCast" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", - "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f?" - }, - "description": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c Smart TV, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432, \u0432\u043a\u043b\u044e\u0447\u0438\u0432 \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430. \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u043e\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u044d\u0442\u043e\u0442 \u0448\u0430\u0433 \u0434\u043b\u044f \u0442\u0435\u043b\u0435\u0432\u0438\u0437\u043e\u0440\u043e\u0432, \u043a\u043e\u0442\u043e\u0440\u044b\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u044e\u0442 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f.", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439 \u0434\u043b\u044f Smart TV" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "\u0421\u043f\u0438\u0441\u043e\u043a \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u0439", "include_or_exclude": "\u0412\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0442\u044c \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f?", - "timeout": "\u0422\u0430\u0439\u043c-\u0430\u0443\u0442 \u0437\u0430\u043f\u0440\u043e\u0441\u0430 API (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", "volume_step": "\u0428\u0430\u0433 \u0433\u0440\u043e\u043c\u043a\u043e\u0441\u0442\u0438" }, "description": "\u0415\u0441\u043b\u0438 \u0443 \u0432\u0430\u0441 \u0435\u0441\u0442\u044c Smart TV, \u0412\u044b \u043c\u043e\u0436\u0435\u0442\u0435 \u043f\u0440\u0438 \u0436\u0435\u043b\u0430\u043d\u0438\u0438 \u043e\u0442\u0444\u0438\u043b\u044c\u0442\u0440\u043e\u0432\u0430\u0442\u044c \u0441\u043f\u0438\u0441\u043e\u043a \u0438\u0441\u0442\u043e\u0447\u043d\u0438\u043a\u043e\u0432, \u0432\u043a\u043b\u044e\u0447\u0438\u0432 \u0438\u043b\u0438 \u0438\u0441\u043a\u043b\u044e\u0447\u0438\u0432 \u043f\u0440\u0438\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0438\u0437 \u0441\u043f\u0438\u0441\u043a\u0430.", diff --git a/homeassistant/components/vizio/.translations/sl.json b/homeassistant/components/vizio/.translations/sl.json index 8163846aec0..ed325acd868 100644 --- a/homeassistant/components/vizio/.translations/sl.json +++ b/homeassistant/components/vizio/.translations/sl.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Konfiguracijski tok za komponento vizio je \u017ee v teku.", "already_setup": "Ta vnos je \u017ee nastavljen.", - "already_setup_with_diff_host_and_name": "Zdi se, da je bil ta vnos \u017ee nastavljen z drugim gostiteljem in imenom glede na njegovo serijsko \u0161tevilko. Pred ponovnim poskusom dodajanja te naprave, odstranite vse stare vnose iz config.yaml in iz menija Integrations.", - "host_exists": "VIZIO komponenta z gostiteljem \u017ee nastavljen.", - "name_exists": "Vizio komponenta z imenom je \u017ee konfigurirana.", - "updated_entry": "Ta vnos je bil \u017ee nastavljen, vendar se ime, aplikacije in/ali mo\u017enosti, dolo\u010dene v konfiguraciji, ne ujemajo s predhodno uvo\u017eeno konfiguracijo, zato je bil konfiguracijski vnos ustrezno posodobljen.", - "updated_options": "Ta vnos je \u017ee nastavljen, vendar se mo\u017enosti, definirane v config-u, ne ujemajo s predhodno uvo\u017eenimi vrednostmi, zato je bil vnos konfiguracije ustrezno posodobljen.", - "updated_volume_step": "Ta vnos je \u017ee nastavljen, vendar velikost koraka glasnosti v config-u ne ustreza vnosu konfiguracije, zato je bil vnos konfiguracije ustrezno posodobljen." + "updated_entry": "Ta vnos je bil \u017ee nastavljen, vendar se ime, aplikacije in/ali mo\u017enosti, dolo\u010dene v konfiguraciji, ne ujemajo s predhodno uvo\u017eeno konfiguracijo, zato je bil konfiguracijski vnos ustrezno posodobljen." }, "error": { "cant_connect": "Ni bilo mogo\u010de povezati z napravo. [Preglejte dokumente] (https://www.home-assistant.io/integrations/vizio/) in ponovno preverite, ali: \n \u2013 Naprava je vklopljena \n \u2013 Naprava je povezana z omre\u017ejem \n \u2013 Vrednosti, ki ste jih izpolnili, so to\u010dne \nnato poskusite ponovno.", "complete_pairing failed": "Seznanjanja ni mogo\u010de dokon\u010dati. Zagotovite, da je PIN, ki ste ga vnesli, pravilen in da je televizor \u0161e vedno vklopljen in priklju\u010den na omre\u017eje, preden ponovno poizkusite.", "host_exists": "Naprava Vizio z dolo\u010denim gostiteljem je \u017ee konfigurirana.", - "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana.", - "tv_needs_token": "Ko je vrsta naprave\u00bb TV \u00ab, je potreben veljaven \u017eeton za dostop." + "name_exists": "Naprava Vizio z navedenim imenom je \u017ee konfigurirana." }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Va\u0161 VIZIO SmartCast TV je zdaj priklju\u010den na Home Assistant.\n\n\u017deton za dostop je '**{access_token}**'.", "title": "Seznanjanje je kon\u010dano" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "Aplikacije za vklju\u010ditev ali izklju\u010ditev", - "include_or_exclude": "Vklju\u010di ali Izklju\u010di Aplikacije?" - }, - "description": "\u010ce imate pametni TV, lahko po izbiri filtrirate seznam virov tako, da izberete, katere aplikacije \u017eelite vklju\u010diti ali izklju\u010diti na seznamu virov. Ta korak lahko presko\u010dite za televizorje, ki ne podpirajo aplikacij.", - "title": "Konfigurirajte aplikacije za pametno televizijo" - }, "user": { "data": { "access_token": "\u017deton za dostop", @@ -50,14 +35,6 @@ }, "description": "Dostopni \u017eeton je potreben samo za televizorje. \u010ce konfigurirate televizor in \u0161e nimate \u017eetona za dostop, ga pustite prazno in boste \u0161li, da bo \u0161el skozi postopek seznanjanja.", "title": "Namestite Vizio SmartCast napravo" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "Aplikacije za vklju\u010ditev ali izklju\u010ditev", - "include_or_exclude": "Vklju\u010di ali Izklju\u010di Aplikacije?" - }, - "description": "\u010ce imate pametni TV, lahko po izbiri filtrirate seznam virov tako, da izberete, katere aplikacije \u017eelite vklju\u010diti ali izklju\u010diti na seznamu virov. Ta korak lahko presko\u010dite za televizorje, ki ne podpirajo aplikacij.", - "title": "Konfigurirajte aplikacije za pametno televizijo" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "Aplikacije za vklju\u010ditev ali izklju\u010ditev", "include_or_exclude": "Vklju\u010di ali Izklju\u010di Aplikacije?", - "timeout": "\u010casovna omejitev zahteve za API (sekunde)", "volume_step": "Velikost koraka glasnosti" }, "description": "\u010ce imate pametni TV, lahko po izbiri filtrirate seznam virov tako, da izberete, katere aplikacije \u017eelite vklju\u010diti ali izklju\u010diti na seznamu virov.", diff --git a/homeassistant/components/vizio/.translations/sv.json b/homeassistant/components/vizio/.translations/sv.json index 072b441a071..bafd7d1bd2f 100644 --- a/homeassistant/components/vizio/.translations/sv.json +++ b/homeassistant/components/vizio/.translations/sv.json @@ -1,20 +1,13 @@ { "config": { "abort": { - "already_in_progress": "Konfigurationsfl\u00f6de f\u00f6r vizio-komponenten p\u00e5g\u00e5r\nredan.", "already_setup": "Den h\u00e4r posten har redan st\u00e4llts in.", - "already_setup_with_diff_host_and_name": "Den h\u00e4r posten verkar redan ha st\u00e4llts in med en annan v\u00e4rd och ett annat namn baserat p\u00e5 dess serienummer. Ta bort alla gamla poster fr\u00e5n configuration.yaml och fr\u00e5n menyn Integrationer innan du f\u00f6rs\u00f6ker l\u00e4gga till den h\u00e4r enheten igen.", - "host_exists": "Vizio-komponenten med v\u00e4rdnamnet \u00e4r redan konfigurerad.", - "name_exists": "Vizio-komponent med namn redan konfigurerad.", - "updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta.", - "updated_options": "Den h\u00e4r posten har redan st\u00e4llts in men de alternativ som definierats i konfigurationen matchar inte de tidigare importerade alternativv\u00e4rdena s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta.", - "updated_volume_step": "Den h\u00e4r posten har redan st\u00e4llts in men volymstegstorleken i konfigurationen matchar inte konfigurationsposten s\u00e5 konfigurationsposten har uppdaterats i enlighet med detta." + "updated_entry": "Den h\u00e4r posten har redan konfigurerats, men namnet och/eller alternativen som definierats i konfigurationen matchar inte den tidigare importerade konfigurationen och d\u00e4rf\u00f6r har konfigureringsposten uppdaterats i enlighet med detta." }, "error": { "cant_connect": "Det gick inte att ansluta till enheten. [Granska dokumentationen] (https://www.home-assistant.io/integrations/vizio/) och p\u00e5 nytt kontrollera att\n- Enheten \u00e4r p\u00e5slagen\n- Enheten \u00e4r ansluten till n\u00e4tverket\n- De v\u00e4rden du fyllt i \u00e4r korrekta\ninnan du f\u00f6rs\u00f6ker skicka in igen.", "host_exists": "Vizio-enheten med angivet v\u00e4rdnamn \u00e4r redan konfigurerad.", - "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad.", - "tv_needs_token": "N\u00e4r Enhetstyp \u00e4r 'tv' beh\u00f6vs en giltig \u00e5tkomsttoken." + "name_exists": "Vizio-enheten med angivet namn \u00e4r redan konfigurerad." }, "step": { "user": { @@ -33,7 +26,6 @@ "step": { "init": { "data": { - "timeout": "Timeout f\u00f6r API-anrop (sekunder)", "volume_step": "Storlek p\u00e5 volymsteg" }, "title": "Uppdatera Vizo SmartCast-alternativ" diff --git a/homeassistant/components/vizio/.translations/zh-Hant.json b/homeassistant/components/vizio/.translations/zh-Hant.json index 4d826a287f6..eb396428e68 100644 --- a/homeassistant/components/vizio/.translations/zh-Hant.json +++ b/homeassistant/components/vizio/.translations/zh-Hant.json @@ -1,21 +1,14 @@ { "config": { "abort": { - "already_in_progress": "Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", "already_setup": "\u6b64\u7269\u4ef6\u5df2\u8a2d\u5b9a\u904e\u3002", - "already_setup_with_diff_host_and_name": "\u6839\u64da\u6240\u63d0\u4f9b\u7684\u5e8f\u865f\uff0c\u6b64\u7269\u4ef6\u4f3c\u4e4e\u5df2\u7d93\u4f7f\u7528\u4e0d\u540c\u7684\u4e3b\u6a5f\u7aef\u8207\u540d\u7a31\u9032\u884c\u8a2d\u5b9a\u3002\u8acb\u5f9e\u6574\u5408\u9078\u55ae Config.yaml \u4e2d\u79fb\u9664\u820a\u7269\u4ef6\uff0c\u7136\u5f8c\u518d\u65b0\u589e\u6b64\u8a2d\u5099\u3002", - "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u7d93\u9032\u884c\u4e2d\u3002", - "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", - "updated_options": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u9078\u9805\u5b9a\u7fa9\u8207\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002", - "updated_volume_step": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u97f3\u91cf\u5927\u5c0f\u8207\u7269\u4ef6\u8a2d\u5b9a\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" + "updated_entry": "\u6b64\u7269\u4ef6\u5df2\u7d93\u8a2d\u5b9a\uff0c\u4f46\u8a2d\u5b9a\u4e4b\u540d\u7a31\u3001App \u53ca/\u6216\u9078\u9805\u8207\u5148\u524d\u532f\u5165\u7684\u7269\u4ef6\u9078\u9805\u503c\u4e0d\u5408\uff0c\u56e0\u6b64\u8a2d\u5b9a\u5c07\u6703\u8ddf\u8457\u66f4\u65b0\u3002" }, "error": { "cant_connect": "\u7121\u6cd5\u9023\u7dda\u81f3\u8a2d\u5099\u3002[\u8acb\u53c3\u8003\u8aaa\u660e\u6587\u4ef6](https://www.home-assistant.io/integrations/vizio/) \u4e26\u78ba\u8a8d\u4ee5\u4e0b\u9805\u76ee\uff1a\n- \u8a2d\u5099\u5df2\u958b\u6a5f\n- \u8a2d\u5099\u5df2\u9023\u7dda\u81f3\u7db2\u8def\n- \u586b\u5beb\u8cc7\u6599\u6b63\u78ba\n\u7136\u5f8c\u518d\u91cd\u65b0\u50b3\u9001\u3002", "complete_pairing failed": "\u7121\u6cd5\u5b8c\u6210\u914d\u5c0d\uff0c\u50b3\u9001\u524d\u3001\u8acb\u78ba\u5b9a\u6240\u8f38\u5165\u7684 PIN \u78bc\u3001\u540c\u6642\u96fb\u8996\u5df2\u7d93\u958b\u555f\u4e26\u9023\u7dda\u81f3\u7db2\u8def\u3002", "host_exists": "\u4f9d\u4e3b\u6a5f\u7aef\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", - "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002", - "tv_needs_token": "\u7576\u8a2d\u5099\u985e\u5225\u70ba\u300cTV\u300d\u6642\uff0c\u9700\u8981\u5b58\u53d6\u5bc6\u9470\u3002" + "name_exists": "\u4f9d\u540d\u7a31\u4e4b Vizio \u5143\u4ef6\u8a2d\u5b9a\u5df2\u8a2d\u5b9a\u5b8c\u6210\u3002" }, "step": { "pair_tv": { @@ -33,14 +26,6 @@ "description": "Vizio SmartCast TV \u8a2d\u5099\u5df2\u9023\u7dda\u81f3 Home Assistant\u3002\n\n\u5b58\u53d6\u5bc6\u9470\u70ba\u300c**{access_token}**\u300d\u3002", "title": "\u914d\u5c0d\u5b8c\u6210" }, - "tv_apps": { - "data": { - "apps_to_include_or_exclude": "\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684 App", - "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f" - }, - "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u4ee5\u65bc\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6216\u6392\u9664\u904e\u6ffe App\u3002\u5047\u5982\u96fb\u8996\u4e0d\u652f\u63f4 App\u3001\u5247\u53ef\u8df3\u904e\u6b64\u6b65\u9a5f\u3002", - "title": "Smart TV \u8a2d\u5b9a App" - }, "user": { "data": { "access_token": "\u5b58\u53d6\u5bc6\u9470", @@ -50,14 +35,6 @@ }, "description": "\u6b64\u96fb\u8996\u50c5\u9700\u5b58\u53d6\u5bc6\u9470\u3002\u5047\u5982\u60a8\u6b63\u5728\u8a2d\u5b9a\u96fb\u8996\u3001\u5c1a\u672a\u53d6\u5f97\u5bc6\u9470\uff0c\u4fdd\u6301\u7a7a\u767d\u4ee5\u9032\u884c\u914d\u5c0d\u904e\u7a0b\u3002", "title": "\u8a2d\u5b9a Vizio SmartCast \u8a2d\u5099" - }, - "user_tv": { - "data": { - "apps_to_include_or_exclude": "\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684 App", - "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f" - }, - "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u4ee5\u65bc\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6216\u6392\u9664\u904e\u6ffe App\u3002\u5047\u5982\u96fb\u8996\u4e0d\u652f\u63f4 App\u3001\u5247\u53ef\u8df3\u904e\u6b64\u6b65\u9a5f\u3002", - "title": "Smart TV \u8a2d\u5b9a App" } }, "title": "Vizio SmartCast" @@ -68,7 +45,6 @@ "data": { "apps_to_include_or_exclude": "\u6240\u8981\u5305\u542b\u6216\u6392\u9664\u7684 App", "include_or_exclude": "\u5305\u542b\u6216\u6392\u9664 App\uff1f", - "timeout": "API \u8acb\u6c42\u903e\u6642\uff08\u79d2\uff09", "volume_step": "\u97f3\u91cf\u5927\u5c0f" }, "description": "\u5047\u5982\u60a8\u64c1\u6709 Smart TV\u3001\u53ef\u7531\u4f86\u6e90\u5217\u8868\u4e2d\u9078\u64c7\u6240\u8981\u904e\u6ffe\u5305\u542b\u6216\u6392\u9664\u7684 App\u3002\u3002", diff --git a/homeassistant/components/withings/.translations/bg.json b/homeassistant/components/withings/.translations/bg.json index 4064b21ca6b..30e384e0bc0 100644 --- a/homeassistant/components/withings/.translations/bg.json +++ b/homeassistant/components/withings/.translations/bg.json @@ -1,8 +1,5 @@ { "config": { - "abort": { - "no_flows": "\u0422\u0440\u044f\u0431\u0432\u0430 \u0434\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0438\u0440\u0430\u0442\u0435 Withings, \u043f\u0440\u0435\u0434\u0438 \u0434\u0430 \u043c\u043e\u0436\u0435\u0442\u0435 \u0434\u0430 \u0441\u0435 \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u0446\u0438\u0440\u0430\u0442\u0435. \u041c\u043e\u043b\u044f, \u043f\u0440\u043e\u0447\u0435\u0442\u0435\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." }, @@ -13,13 +10,6 @@ }, "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." - }, - "user": { - "data": { - "profile": "\u041f\u0440\u043e\u0444\u0438\u043b" - }, - "description": "\u0418\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b, \u043a\u044a\u043c \u043a\u043e\u0439\u0442\u043e \u0438\u0441\u043a\u0430\u0442\u0435 \u0434\u0430 \u0441\u0432\u044a\u0440\u0436\u0435\u0442\u0435 Home Assistant \u0441 Withings. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0430\u0442\u0430 \u043d\u0430 Withings \u043d\u0435 \u0437\u0430\u0431\u0440\u0430\u0432\u044f\u0439\u0442\u0435 \u0434\u0430 \u0438\u0437\u0431\u0435\u0440\u0435\u0442\u0435 \u0435\u0434\u0438\u043d \u0438 \u0441\u044a\u0449 \u043f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b \u0438\u043b\u0438 \u0434\u0430\u043d\u043d\u0438\u0442\u0435 \u043d\u044f\u043c\u0430 \u0434\u0430 \u0431\u044a\u0434\u0430\u0442 \u0441\u0432\u044a\u0440\u0437\u0430\u043d\u0438 \u043f\u0440\u0430\u0432\u0438\u043b\u043d\u043e.", - "title": "\u041f\u043e\u0442\u0440\u0435\u0431\u0438\u0442\u0435\u043b\u0441\u043a\u0438 \u043f\u0440\u043e\u0444\u0438\u043b." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/ca.json b/homeassistant/components/withings/.translations/ca.json index edb95a946aa..6363ddf1983 100644 --- a/homeassistant/components/withings/.translations/ca.json +++ b/homeassistant/components/withings/.translations/ca.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "S'ha acabat el temps d'espera durant la generaci\u00f3 de l'URL d'autoritzaci\u00f3.", - "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3.", - "no_flows": "Necessites configurar Withings abans de poder autenticar't-hi. Llegeix la documentaci\u00f3." + "missing_configuration": "La integraci\u00f3 Withings no est\u00e0 configurada. Mira'n la documentaci\u00f3." }, "create_entry": { "default": "Autenticaci\u00f3 exitosa amb Withings per al perfil seleccionat." @@ -18,13 +17,6 @@ }, "description": "Quin perfil has seleccionat al lloc web de Withings? \u00c9s important que els perfils coincideixin sin\u00f3, les dades no s\u2019etiquetaran correctament.", "title": "Perfil d'usuari." - }, - "user": { - "data": { - "profile": "Perfil" - }, - "description": "Selecciona un perfil d'usuari amb el qual vols que Home Assistant s'uneixi amb un perfil de Withings. A la p\u00e0gina de Withings, assegura't de seleccionar el mateix usuari o, les dades no seran les correctes.", - "title": "Perfil d'usuari." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/da.json b/homeassistant/components/withings/.translations/da.json index 72d851ad873..09e73e4ea8e 100644 --- a/homeassistant/components/withings/.translations/da.json +++ b/homeassistant/components/withings/.translations/da.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Timeout ved generering af godkendelses-url.", - "missing_configuration": "Withings-integrationen er ikke konfigureret. F\u00f8lg venligst dokumentationen.", - "no_flows": "Du skal konfigurere Withings, f\u00f8r du kan godkende med den. L\u00e6s venligst dokumentationen." + "missing_configuration": "Withings-integrationen er ikke konfigureret. F\u00f8lg venligst dokumentationen." }, "create_entry": { "default": "Godkendt med Withings." @@ -18,13 +17,6 @@ }, "description": "Hvilken profil har du valgt p\u00e5 Withings hjemmeside? Det er vigtigt, at profilerne matcher, ellers vil data blive m\u00e6rket forkert.", "title": "Brugerprofil." - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "V\u00e6lg en brugerprofil, som du vil have Home Assistant til at tilknytte med en Withings-profil. P\u00e5 siden Withings skal du s\u00f8rge for at v\u00e6lge den samme bruger eller data vil ikke blive m\u00e6rket korrekt.", - "title": "Brugerprofil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/de.json b/homeassistant/components/withings/.translations/de.json index ae8ab679593..6295d918848 100644 --- a/homeassistant/components/withings/.translations/de.json +++ b/homeassistant/components/withings/.translations/de.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Zeit\u00fcberschreitung beim Erstellen der Autorisierungs-URL.", - "missing_configuration": "Die Withings-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation.", - "no_flows": "Withings muss konfiguriert werden, bevor die Integration authentifiziert werden kann. Bitte lies die Dokumentation." + "missing_configuration": "Die Withings-Integration ist nicht konfiguriert. Bitte folgen Sie der Dokumentation." }, "create_entry": { "default": "Erfolgreiche Authentifizierung mit Withings." @@ -18,13 +17,6 @@ }, "description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", "title": "Benutzerprofil" - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "W\u00e4hle ein Benutzerprofil aus, dem Home Assistant ein Withings-Profil zuordnen soll. Stelle sicher, dass du auf der Withings-Seite denselben Benutzer ausw\u00e4hlst, da sonst die Daten nicht korrekt gekennzeichnet werden.", - "title": "Benutzerprofil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/en.json b/homeassistant/components/withings/.translations/en.json index c39ac530ae6..eefa54b9490 100644 --- a/homeassistant/components/withings/.translations/en.json +++ b/homeassistant/components/withings/.translations/en.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Withings integration is not configured. Please follow the documentation.", - "no_flows": "You need to configure Withings before being able to authenticate with it. Please read the documentation." + "missing_configuration": "The Withings integration is not configured. Please follow the documentation." }, "create_entry": { "default": "Successfully authenticated with Withings." @@ -18,13 +17,6 @@ }, "description": "Which profile did you select on the Withings website? It's important the profiles match, otherwise data will be mis-labeled.", "title": "User Profile." - }, - "user": { - "data": { - "profile": "Profile" - }, - "description": "Select a user profile to which you want Home Assistant to map with a Withings profile. On the withings page, be sure to select the same user or data will not be labeled correctly.", - "title": "User Profile." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/es-419.json b/homeassistant/components/withings/.translations/es-419.json index 485150d2928..f0490e5724b 100644 --- a/homeassistant/components/withings/.translations/es-419.json +++ b/homeassistant/components/withings/.translations/es-419.json @@ -1,19 +1,8 @@ { "config": { - "abort": { - "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor lea la documentaci\u00f3n." - }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." }, - "step": { - "user": { - "data": { - "profile": "Perfil" - }, - "title": "Perfil del usuario." - } - }, "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/es.json b/homeassistant/components/withings/.translations/es.json index c239d7d8db9..f3e2c36ae72 100644 --- a/homeassistant/components/withings/.translations/es.json +++ b/homeassistant/components/withings/.translations/es.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tiempo de espera agotado para la autorizaci\u00f3n de la url.", - "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n.", - "no_flows": "Debe configurar Withings antes de poder autenticarse con \u00e9l. Por favor, lea la documentaci\u00f3n." + "missing_configuration": "La integraci\u00f3n de Withings no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." }, "create_entry": { "default": "Autenticado correctamente con Withings para el perfil seleccionado." @@ -18,13 +17,6 @@ }, "description": "\u00bfQu\u00e9 perfil seleccion\u00f3 en el sitio web de Withings? Es importante que los perfiles coincidan, de lo contrario los datos se etiquetar\u00e1n incorrectamente.", "title": "Perfil de usuario." - }, - "user": { - "data": { - "profile": "Perfil" - }, - "description": "Seleccione un perfil de usuario para el cual desea que Home Assistant se conecte con el perfil de Withings. En la p\u00e1gina de Withings, aseg\u00farese de seleccionar el mismo usuario o los datos no se identificar\u00e1n correctamente.", - "title": "Perfil de usuario." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/fr.json b/homeassistant/components/withings/.translations/fr.json index a9a0db55005..d178ef6c889 100644 --- a/homeassistant/components/withings/.translations/fr.json +++ b/homeassistant/components/withings/.translations/fr.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "D\u00e9lai d'expiration g\u00e9n\u00e9rant une URL d'autorisation.", - "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation.", - "no_flows": "Vous devez configurer Withings avant de pouvoir vous authentifier avec celui-ci. Veuillez lire la documentation." + "missing_configuration": "L'int\u00e9gration Withings n'est pas configur\u00e9e. Veuillez suivre la documentation." }, "create_entry": { "default": "Authentifi\u00e9 avec succ\u00e8s \u00e0 Withings pour le profil s\u00e9lectionn\u00e9." @@ -18,13 +17,6 @@ }, "description": "Quel profil avez-vous s\u00e9lectionn\u00e9 sur le site Withings? Il est important que les profils correspondent, sinon les donn\u00e9es seront mal \u00e9tiquet\u00e9es.", "title": "Profil utilisateur" - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "S\u00e9lectionnez l'utilisateur que vous souhaitez associer \u00e0 Withings. Sur la page withings, veillez \u00e0 s\u00e9lectionner le m\u00eame utilisateur, sinon les donn\u00e9es ne seront pas \u00e9tiquet\u00e9es correctement.", - "title": "Profil utilisateur" } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/hu.json b/homeassistant/components/withings/.translations/hu.json index 503013e402f..b13cf9ec524 100644 --- a/homeassistant/components/withings/.translations/hu.json +++ b/homeassistant/components/withings/.translations/hu.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Id\u0151t\u00fall\u00e9p\u00e9s az \u00e9rv\u00e9nyes\u00edt\u00e9si url gener\u00e1l\u00e1sa sor\u00e1n.", - "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t.", - "no_flows": "Konfigur\u00e1lnia kell a Withings-et, miel\u0151tt hiteles\u00edtheti mag\u00e1t vele. K\u00e9rj\u00fck, olvassa el a dokument\u00e1ci\u00f3t." + "missing_configuration": "A Withings integr\u00e1ci\u00f3 nincs konfigur\u00e1lva. K\u00e9rj\u00fck, k\u00f6vesse a dokument\u00e1ci\u00f3t." }, "create_entry": { "default": "A Withings sikeresen hiteles\u00edtett." @@ -18,13 +17,6 @@ }, "description": "Melyik profilt v\u00e1lasztottad ki a Withings weboldalon? Fontos, hogy a profilok egyeznek, k\u00fcl\u00f6nben az adatok helytelen c\u00edmk\u00e9vel lesznek ell\u00e1tva.", "title": "Felhaszn\u00e1l\u00f3i profil." - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "V\u00e1lasszon egy felhaszn\u00e1l\u00f3i profilt, amelyet szeretn\u00e9, hogy a Home Assistant hozz\u00e1rendeljen a Withings profilhoz. \u00dcgyeljen arra, hogy ugyanazt a felhaszn\u00e1l\u00f3t v\u00e1lassza a Withings oldalon, k\u00fcl\u00f6nben az adatok nem lesznek megfelel\u0151en felcimk\u00e9zve.", - "title": "Felhaszn\u00e1l\u00f3i profil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/it.json b/homeassistant/components/withings/.translations/it.json index 4a6f5e67965..6deeff07489 100644 --- a/homeassistant/components/withings/.translations/it.json +++ b/homeassistant/components/withings/.translations/it.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Timeout durante la generazione dell'URL di autorizzazione.", - "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione.", - "no_flows": "\u00c8 necessario configurare Withings prima di potersi autenticare con esso. Si prega di leggere la documentazione." + "missing_configuration": "Il componente Withings non \u00e8 configurato. Si prega di seguire la documentazione." }, "create_entry": { "default": "Autenticazione riuscita con Withings." @@ -18,13 +17,6 @@ }, "description": "Quale profilo hai selezionato sul sito web di Withings? \u00c8 importante che i profili corrispondano, altrimenti i dati avranno con un'errata etichettatura.", "title": "Profilo utente." - }, - "user": { - "data": { - "profile": "Profilo" - }, - "description": "Seleziona un profilo utente a cui desideri associare Home Assistant con un profilo Withings. Nella pagina Withings, assicurati di selezionare lo stesso utente o i dati non saranno etichettati correttamente.", - "title": "Profilo utente." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/ko.json b/homeassistant/components/withings/.translations/ko.json index 4ff2a80434a..8cdd8511919 100644 --- a/homeassistant/components/withings/.translations/ko.json +++ b/homeassistant/components/withings/.translations/ko.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\uc778\uc99d url \uc0dd\uc131 \uc2dc\uac04\uc774 \ucd08\uacfc\ub418\uc5c8\uc2b5\ub2c8\ub2e4.", - "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694.", - "no_flows": "Withings \ub97c \uc778\uc99d\ud558\ub824\uba74 \uba3c\uc800 Withings \ub97c \uad6c\uc131\ud574\uc57c \ud569\ub2c8\ub2e4. [\uc548\ub0b4](https://www.home-assistant.io/components/withings/) \ub97c \uc77d\uc5b4\ubcf4\uc138\uc694." + "missing_configuration": "Withings \uad6c\uc131\uc694\uc18c\uac00 \uad6c\uc131\ub418\uc9c0 \uc54a\uc558\uc2b5\ub2c8\ub2e4. \uc124\uba85\uc11c\ub97c \ucc38\uace0\ud574\uc8fc\uc138\uc694." }, "create_entry": { "default": "Withings \ub85c \uc131\uacf5\uc801\uc73c\ub85c \uc778\uc99d\ub418\uc5c8\uc2b5\ub2c8\ub2e4." @@ -18,13 +17,6 @@ }, "description": "Withings \uc6f9 \uc0ac\uc774\ud2b8\uc5d0\uc11c \uc5b4\ub5a4 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud558\uc168\ub098\uc694? \ud504\ub85c\ud544\uc774 \uc77c\uce58\ud574\uc57c \ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74, \ub370\uc774\ud130\uc5d0 \ub808\uc774\ube14\uc774 \uc798\ubabb \uc9c0\uc815\ub429\ub2c8\ub2e4.", "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." - }, - "user": { - "data": { - "profile": "\ud504\ub85c\ud544" - }, - "description": "Home Assistant \uac00 Withings \ud504\ub85c\ud544\uacfc \ub9f5\ud551\ud560 \uc0ac\uc6a9\uc790 \ud504\ub85c\ud544\uc744 \uc120\ud0dd\ud574\uc8fc\uc138\uc694. Withings \ud398\uc774\uc9c0\uc5d0\uc11c \ub3d9\uc77c\ud55c \uc0ac\uc6a9\uc790\ub97c \uc120\ud0dd\ud574\uc57c\ud569\ub2c8\ub2e4. \uadf8\ub807\uc9c0 \uc54a\uc73c\uba74 \ub370\uc774\ud130\uc5d0 \uc62c\ubc14\ub978 \ub808\uc774\ube14\uc774 \uc9c0\uc815\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4.", - "title": "\uc0ac\uc6a9\uc790 \ud504\ub85c\ud544." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/lb.json b/homeassistant/components/withings/.translations/lb.json index 4f3fb27e7b2..1984ef6f586 100644 --- a/homeassistant/components/withings/.translations/lb.json +++ b/homeassistant/components/withings/.translations/lb.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Z\u00e4it Iwwerschreidung beim gener\u00e9ieren vun der Autorisatiouns URL.", - "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun.", - "no_flows": "Dir musst Withingss konfigur\u00e9ieren, ier Dir d\u00ebs Authentifiz\u00e9ierung k\u00ebnnt benotzen. Liest w.e.g. d'Instruktioune." + "missing_configuration": "Withings Integratioun ass nach net konfigur\u00e9iert. Follegt w.e.g der Dokumentatioun." }, "create_entry": { "default": "Erfollegr\u00e4ich mam ausgewielte Profile mat Withings authentifiz\u00e9iert." @@ -18,13 +17,6 @@ }, "description": "W\u00e9ie Profil hutt dir op der Withings Webs\u00e4it ausgewielt? Et ass wichteg dass Profiller passen, soss ginn Donn\u00e9e\u00eb falsch gekennzeechent.", "title": "Benotzer Profil." - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "Wielt ee Benotzer Profile aus dee mam Withings Profile soll verbonne ginn. Stellt s\u00e9cher dass dir op der Withings S\u00e4it deeselwechte Benotzer auswielt, soss ginn d'Donn\u00e9e net richteg ugewisen.", - "title": "Benotzer Profil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/lv.json b/homeassistant/components/withings/.translations/lv.json index 3f7cf20fdb4..7d8b268367c 100644 --- a/homeassistant/components/withings/.translations/lv.json +++ b/homeassistant/components/withings/.translations/lv.json @@ -1,13 +1,5 @@ { "config": { - "step": { - "user": { - "data": { - "profile": "Profils" - }, - "title": "Lietot\u0101ja profils." - } - }, "title": "Withings" } } \ No newline at end of file diff --git a/homeassistant/components/withings/.translations/nl.json b/homeassistant/components/withings/.translations/nl.json index 0b01fc8c16a..d534acc5c09 100644 --- a/homeassistant/components/withings/.translations/nl.json +++ b/homeassistant/components/withings/.translations/nl.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Time-out tijdens genereren autorisatie url.", - "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen.", - "no_flows": "U moet Withings configureren voordat u zich ermee kunt verifi\u00ebren. [Gelieve de documentatie te lezen]" + "missing_configuration": "De Withings integratie is niet geconfigureerd. Gelieve de documentatie te volgen." }, "create_entry": { "default": "Succesvol geverifieerd met Withings voor het geselecteerde profiel." @@ -18,13 +17,6 @@ }, "description": "Welk profiel hebt u op de website van Withings selecteren? Het is belangrijk dat de profielen overeenkomen, anders worden gegevens verkeerd gelabeld.", "title": "Gebruikersprofiel." - }, - "user": { - "data": { - "profile": "Profiel" - }, - "description": "Selecteer een gebruikersprofiel waaraan u Home Assistant wilt toewijzen met een Withings-profiel. Zorg ervoor dat u op de pagina Withings dezelfde gebruiker selecteert, anders worden de gegevens niet correct gelabeld.", - "title": "Gebruikersprofiel." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/no.json b/homeassistant/components/withings/.translations/no.json index 1c4a8c0fb71..fac2fa3a8fc 100644 --- a/homeassistant/components/withings/.translations/no.json +++ b/homeassistant/components/withings/.translations/no.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Tidsavbrudd ved generering av autoriseringsadresse.", - "missing_configuration": "Withings-integreringen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen.", - "no_flows": "Du m\u00e5 konfigurere Withings f\u00f8r du kan godkjenne med den. Vennligst les dokumentasjonen." + "missing_configuration": "Withings-integreringen er ikke konfigurert. Vennligst f\u00f8lg dokumentasjonen." }, "create_entry": { "default": "Vellykket godkjent med Withings." @@ -18,13 +17,6 @@ }, "description": "Hvilken profil valgte du p\u00e5 Withings nettsted? Det er viktig at profilene samsvarer, ellers blir data feilmerket.", "title": "Brukerprofil." - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "Velg en brukerprofil som du vil at Home Assistant skal kartlegge med en Withings-profil. P\u00e5 Withings-siden m\u00e5 du passe p\u00e5 at du velger samme bruker ellers vil ikke dataen bli merket riktig.", - "title": "Brukerprofil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/pl.json b/homeassistant/components/withings/.translations/pl.json index afe35bd06cf..c20f7a9ba53 100644 --- a/homeassistant/components/withings/.translations/pl.json +++ b/homeassistant/components/withings/.translations/pl.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Min\u0105\u0142 limit czasu generowania url autoryzacji.", - "missing_configuration": "Integracja z Withings nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105.", - "no_flows": "Musisz skonfigurowa\u0107 Withings, aby m\u00f3c si\u0119 z nim uwierzytelni\u0107. Zapoznaj si\u0119 z dokumentacj\u0105." + "missing_configuration": "Integracja z Withings nie jest skonfigurowana. Post\u0119puj zgodnie z dokumentacj\u0105." }, "create_entry": { "default": "Pomy\u015blnie uwierzytelniono z Withings dla wybranego profilu" @@ -18,13 +17,6 @@ }, "description": "Kt\u00f3ry profil wybra\u0142e\u015b na stronie Withings? Wa\u017cne jest, aby profile si\u0119 zgadza\u0142y, w przeciwnym razie dane zostan\u0105 b\u0142\u0119dnie oznaczone.", "title": "Profil u\u017cytkownika" - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "Wybierz profil u\u017cytkownika Withings, na kt\u00f3ry chcesz po\u0142\u0105czy\u0107 z Home Assistant'em. Na stronie Withings wybierz ten sam profil u\u017cytkownika, by dane by\u0142y poprawnie oznaczone.", - "title": "Profil u\u017cytkownika" } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/ru.json b/homeassistant/components/withings/.translations/ru.json index 407bcf48c1a..eba16290453 100644 --- a/homeassistant/components/withings/.translations/ru.json +++ b/homeassistant/components/withings/.translations/ru.json @@ -2,8 +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.", - "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Withings \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \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.", - "no_flows": "\u041d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u043e \u0432\u044b\u043f\u043e\u043b\u043d\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443 Withings \u043f\u0435\u0440\u0435\u0434 \u043f\u0440\u043e\u0445\u043e\u0436\u0434\u0435\u043d\u0438\u0435\u043c \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \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." + "missing_configuration": "\u0418\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f Withings \u043d\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0435\u043d\u0430. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \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." }, "create_entry": { "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." @@ -18,13 +17,6 @@ }, "description": "\u041a\u0430\u043a\u043e\u0439 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u0412\u044b \u0432\u044b\u0431\u0440\u0430\u043b\u0438 \u043d\u0430 \u0441\u0430\u0439\u0442\u0435 Withings? \u0412\u0430\u0436\u043d\u043e, \u0447\u0442\u043e\u0431\u044b \u043f\u0440\u043e\u0444\u0438\u043b\u0438 \u0441\u043e\u0432\u043f\u0430\u0434\u0430\u043b\u0438, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b.", "title": "Withings" - }, - "user": { - "data": { - "profile": "\u041f\u0440\u043e\u0444\u0438\u043b\u044c" - }, - "description": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043f\u0440\u043e\u0444\u0438\u043b\u044c \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f. \u041d\u0430 \u0441\u0442\u0440\u0430\u043d\u0438\u0446\u0435 Withings \u043e\u0431\u044f\u0437\u0430\u0442\u0435\u043b\u044c\u043d\u043e \u0432\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0442\u043e\u0433\u043e \u0436\u0435 \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f, \u0438\u043d\u0430\u0447\u0435 \u0434\u0430\u043d\u043d\u044b\u0435 \u0431\u0443\u0434\u0443\u0442 \u043f\u043e\u043c\u0435\u0447\u0435\u043d\u044b \u043d\u0435\u043f\u0440\u0430\u0432\u0438\u043b\u044c\u043d\u043e.", - "title": "Withings" } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/sl.json b/homeassistant/components/withings/.translations/sl.json index faa76ac9333..1de0a0d6ce7 100644 --- a/homeassistant/components/withings/.translations/sl.json +++ b/homeassistant/components/withings/.translations/sl.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u010casovna omejitev za generiranje potrditvenega URL-ja je potekla.", - "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo.", - "no_flows": "Withings morate prvo konfigurirati, preden ga boste lahko uporabili za overitev. Prosimo, preberite dokumentacijo." + "missing_configuration": "Integracija Withings ni konfigurirana. Prosimo, upo\u0161tevajte dokumentacijo." }, "create_entry": { "default": "Uspe\u0161no overjen z Withings." @@ -18,13 +17,6 @@ }, "description": "Kateri profil ste izbrali na spletni strani Withings? Pomembno je, da se profili ujemajo, sicer bodo podatki napa\u010dno ozna\u010deni.", "title": "Uporabni\u0161ki profil." - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "Izberite uporabni\u0161ki profil, za katerega \u017eelite, da se Home Assistant prika\u017ee s profilom Withings. Na Withings strani ne pozabite izbrati istega uporabnika sicer podatki ne bodo pravilno ozna\u010deni.", - "title": "Uporabni\u0161ki profil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/sv.json b/homeassistant/components/withings/.translations/sv.json index dc8954af2c7..dfaa09d52f0 100644 --- a/homeassistant/components/withings/.translations/sv.json +++ b/homeassistant/components/withings/.translations/sv.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "Skapandet av en auktoriseringsadress \u00f6verskred tidsgr\u00e4nsen.", - "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen.", - "no_flows": "Du m\u00e5ste konfigurera Withings innan du kan autentisera med den. L\u00e4s dokumentationen." + "missing_configuration": "Withings-integrationen \u00e4r inte konfigurerad. V\u00e4nligen f\u00f6lj dokumentationen." }, "create_entry": { "default": "Lyckad autentisering med Withings." @@ -18,13 +17,6 @@ }, "description": "Vilken profil valde du p\u00e5 Withings webbplats? Det \u00e4r viktigt att profilerna matchar, annars kommer data att vara felm\u00e4rkta.", "title": "Anv\u00e4ndarprofil." - }, - "user": { - "data": { - "profile": "Profil" - }, - "description": "V\u00e4lj en anv\u00e4ndarprofil som du vill att Home Assistant ska kartl\u00e4gga med en Withings-profil. Var noga med att v\u00e4lja samma anv\u00e4ndare p\u00e5 visningssidan eller s\u00e5 kommer inte data att betecknas korrekt.", - "title": "Anv\u00e4ndarprofil." } }, "title": "Withings" diff --git a/homeassistant/components/withings/.translations/zh-Hant.json b/homeassistant/components/withings/.translations/zh-Hant.json index 06870c4020a..61ae1fd8e06 100644 --- a/homeassistant/components/withings/.translations/zh-Hant.json +++ b/homeassistant/components/withings/.translations/zh-Hant.json @@ -2,8 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u7522\u751f\u8a8d\u8b49 URL \u6642\u903e\u6642\u3002", - "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002", - "no_flows": "\u5fc5\u9808\u5148\u8a2d\u5b9a Withings \u65b9\u80fd\u9032\u884c\u8a8d\u8b49\u3002\u8acb\u53c3\u95b1\u6587\u4ef6\u3002" + "missing_configuration": "Withings \u6574\u5408\u5c1a\u672a\u8a2d\u7f6e\uff0c\u8acb\u53c3\u95b1\u6587\u4ef6\u8aaa\u660e\u3002" }, "create_entry": { "default": "\u5df2\u6210\u529f\u8a8d\u8b49 Withings \u8a2d\u5099\u3002" @@ -18,13 +17,6 @@ }, "description": "\u65bc Withings \u7db2\u7ad9\u6240\u9078\u64c7\u7684\u500b\u4eba\u8a2d\u5b9a\u70ba\u4f55\uff1f\u5047\u5982\u500b\u4eba\u8a2d\u5b9a\u4e0d\u7b26\u5408\u7684\u8a71\uff0c\u8cc7\u6599\u5c07\u6703\u6a19\u793a\u932f\u8aa4\u3002", "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" - }, - "user": { - "data": { - "profile": "\u500b\u4eba\u8a2d\u5b9a" - }, - "description": "\u9078\u64c7 Home Assistant \u6240\u8981\u5c0d\u61c9\u4f7f\u7528\u7684 Withings \u500b\u4eba\u8a2d\u5b9a\u3002\u65bc Withings \u9801\u9762\u3001\u78ba\u5b9a\u9078\u53d6\u76f8\u540c\u7684\u4f7f\u7528\u8005\uff0c\u5426\u5247\u8cc7\u6599\u5c07\u7121\u6cd5\u6b63\u78ba\u6a19\u793a\u3002", - "title": "\u500b\u4eba\u8a2d\u5b9a\u3002" } }, "title": "Withings" diff --git a/homeassistant/components/wled/.translations/zh-Hant.json b/homeassistant/components/wled/.translations/zh-Hant.json index b72ef3d078c..14139a20401 100644 --- a/homeassistant/components/wled/.translations/zh-Hant.json +++ b/homeassistant/components/wled/.translations/zh-Hant.json @@ -18,7 +18,7 @@ }, "zeroconf_confirm": { "description": "\u662f\u5426\u8981\u65b0\u589e WLED \u540d\u7a31\u300c{name}\u300d\u8a2d\u5099\u81f3 Home Assistant\uff1f", - "title": "\u767c\u73fe\u5230 WLED \u8a2d\u5099" + "title": "\u81ea\u52d5\u63a2\u7d22\u5230 WLED \u8a2d\u5099" } }, "title": "WLED" diff --git a/homeassistant/components/wwlln/.translations/bg.json b/homeassistant/components/wwlln/.translations/bg.json index c083218c443..f252518fcab 100644 --- a/homeassistant/components/wwlln/.translations/bg.json +++ b/homeassistant/components/wwlln/.translations/bg.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435\u0442\u043e \u0432\u0435\u0447\u0435 \u0435 \u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u0430\u043d\u043e" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/ca.json b/homeassistant/components/wwlln/.translations/ca.json index 736689c34d5..f7fe15f27ec 100644 --- a/homeassistant/components/wwlln/.translations/ca.json +++ b/homeassistant/components/wwlln/.translations/ca.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Aquesta ubicaci\u00f3 ja est\u00e0 registrada.", - "window_too_small": "Una finestra massa petita pot provocar que Home Assistant perdi esdeveniments." - }, - "error": { - "identifier_exists": "Ubicaci\u00f3 ja registrada" + "already_configured": "Aquesta ubicaci\u00f3 ja est\u00e0 registrada." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/cy.json b/homeassistant/components/wwlln/.translations/cy.json index e9de2acbdc6..6050207304f 100644 --- a/homeassistant/components/wwlln/.translations/cy.json +++ b/homeassistant/components/wwlln/.translations/cy.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Enw eisoes wedi gofrestru" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/da.json b/homeassistant/components/wwlln/.translations/da.json index 5d4f4c40b5d..df10f39657a 100644 --- a/homeassistant/components/wwlln/.translations/da.json +++ b/homeassistant/components/wwlln/.translations/da.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Lokalitet er allerede registreret" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/de.json b/homeassistant/components/wwlln/.translations/de.json index c02da263f89..487f2294dc6 100644 --- a/homeassistant/components/wwlln/.translations/de.json +++ b/homeassistant/components/wwlln/.translations/de.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Dieser Standort ist bereits registriert.", - "window_too_small": "Ein zu kleines Fenster f\u00fchrt dazu, dass Home Assistant Ereignisse verpasst." - }, - "error": { - "identifier_exists": "Standort bereits registriert" + "already_configured": "Dieser Standort ist bereits registriert." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/en.json b/homeassistant/components/wwlln/.translations/en.json index a12a5079f9b..48896cc8682 100644 --- a/homeassistant/components/wwlln/.translations/en.json +++ b/homeassistant/components/wwlln/.translations/en.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "This location is already registered.", - "window_too_small": "A too-small window will cause Home Assistant to miss events." - }, - "error": { - "identifier_exists": "Location already registered" + "already_configured": "This location is already registered." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/es-419.json b/homeassistant/components/wwlln/.translations/es-419.json index d185410a4ef..6b2e5d23ffb 100644 --- a/homeassistant/components/wwlln/.translations/es-419.json +++ b/homeassistant/components/wwlln/.translations/es-419.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Lugar ya registrado" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/es.json b/homeassistant/components/wwlln/.translations/es.json index ee377673181..22eb2c1e704 100644 --- a/homeassistant/components/wwlln/.translations/es.json +++ b/homeassistant/components/wwlln/.translations/es.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada.", - "window_too_small": "Una ventana demasiado peque\u00f1a provocar\u00e1 que Home Assistant se pierda eventos." - }, - "error": { - "identifier_exists": "Ubicaci\u00f3n ya registrada" + "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/fr.json b/homeassistant/components/wwlln/.translations/fr.json index ad16a7e3a8d..d19114286ad 100644 --- a/homeassistant/components/wwlln/.translations/fr.json +++ b/homeassistant/components/wwlln/.translations/fr.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Cet emplacement est d\u00e9j\u00e0 enregistr\u00e9.", - "window_too_small": "Une fen\u00eatre trop petite emp\u00eachera Home Assistant de manquer des \u00e9v\u00e9nements." - }, - "error": { - "identifier_exists": "Emplacement d\u00e9j\u00e0 enregistr\u00e9" + "already_configured": "Cet emplacement est d\u00e9j\u00e0 enregistr\u00e9." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/hr.json b/homeassistant/components/wwlln/.translations/hr.json index 09ca1a0273f..3dec14ffa17 100644 --- a/homeassistant/components/wwlln/.translations/hr.json +++ b/homeassistant/components/wwlln/.translations/hr.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Lokacija je ve\u0107 registrirana" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/it.json b/homeassistant/components/wwlln/.translations/it.json index 35cbb8b9bc0..1733cfdf172 100644 --- a/homeassistant/components/wwlln/.translations/it.json +++ b/homeassistant/components/wwlln/.translations/it.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Questa posizione \u00e8 gi\u00e0 registrata.", - "window_too_small": "Una finestra troppo piccola far\u00e0 s\u00ec che Home Assistant perda gli eventi." - }, - "error": { - "identifier_exists": "Localit\u00e0 gi\u00e0 registrata" + "already_configured": "Questa posizione \u00e8 gi\u00e0 registrata." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/ko.json b/homeassistant/components/wwlln/.translations/ko.json index bc4a483a077..a71ebe3ea0c 100644 --- a/homeassistant/components/wwlln/.translations/ko.json +++ b/homeassistant/components/wwlln/.translations/ko.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "\uc774 \uc704\uce58\ub294 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4", - "window_too_small": "\ucc3d\uc774 \ub108\ubb34 \uc791\uc73c\uba74 Home Assistant \uac00 \uc774\ubca4\ud2b8\ub97c \ub193\uce60 \uc218 \uc788\uc2b5\ub2c8\ub2e4." - }, - "error": { - "identifier_exists": "\uc704\uce58\uac00 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" + "already_configured": "\uc774 \uc704\uce58\ub294 \uc774\ubbf8 \ub4f1\ub85d\ub418\uc5c8\uc2b5\ub2c8\ub2e4" }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/lb.json b/homeassistant/components/wwlln/.translations/lb.json index a580b639d96..9632cb372b2 100644 --- a/homeassistant/components/wwlln/.translations/lb.json +++ b/homeassistant/components/wwlln/.translations/lb.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "D\u00ebse Standuert ass scho registr\u00e9iert" }, - "error": { - "identifier_exists": "Standuert ass scho registr\u00e9iert" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/nl.json b/homeassistant/components/wwlln/.translations/nl.json index 8cf0e80806d..542c53f0c03 100644 --- a/homeassistant/components/wwlln/.translations/nl.json +++ b/homeassistant/components/wwlln/.translations/nl.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Locatie al geregistreerd" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/no.json b/homeassistant/components/wwlln/.translations/no.json index ca9822d2733..fab8810ba5e 100644 --- a/homeassistant/components/wwlln/.translations/no.json +++ b/homeassistant/components/wwlln/.translations/no.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Denne plasseringen er allerede registrert.", - "window_too_small": "Et for lite vindu vil f\u00f8re til at Home Assistant g\u00e5r glipp av hendelser." - }, - "error": { - "identifier_exists": "Lokasjon allerede registrert" + "already_configured": "Denne plasseringen er allerede registrert." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/pl.json b/homeassistant/components/wwlln/.translations/pl.json index a202c611086..22d84209b7f 100644 --- a/homeassistant/components/wwlln/.translations/pl.json +++ b/homeassistant/components/wwlln/.translations/pl.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Ta lokalizacja jest ju\u017c zarejestrowana.", - "window_too_small": "Zbyt ma\u0142e okno spowoduje, \u017ce Home Assistant przegapi wydarzenia." - }, - "error": { - "identifier_exists": "Lokalizacja jest ju\u017c zarejestrowana." + "already_configured": "Ta lokalizacja jest ju\u017c zarejestrowana." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/pt-BR.json b/homeassistant/components/wwlln/.translations/pt-BR.json index 30b39a4431c..296588f66a8 100644 --- a/homeassistant/components/wwlln/.translations/pt-BR.json +++ b/homeassistant/components/wwlln/.translations/pt-BR.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Localiza\u00e7\u00e3o j\u00e1 registrada" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/ru.json b/homeassistant/components/wwlln/.translations/ru.json index b0e39a51898..b67d70e057b 100644 --- a/homeassistant/components/wwlln/.translations/ru.json +++ b/homeassistant/components/wwlln/.translations/ru.json @@ -3,9 +3,6 @@ "abort": { "already_configured": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0434\u043b\u044f \u044d\u0442\u043e\u0433\u043e \u043c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u044f \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430." }, - "error": { - "identifier_exists": "\u041c\u0435\u0441\u0442\u043e\u043f\u043e\u043b\u043e\u0436\u0435\u043d\u0438\u0435 \u0443\u0436\u0435 \u0437\u0430\u0440\u0435\u0433\u0438\u0441\u0442\u0440\u0438\u0440\u043e\u0432\u0430\u043d\u043e." - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/sl.json b/homeassistant/components/wwlln/.translations/sl.json index 396180249e2..11fc4f00db8 100644 --- a/homeassistant/components/wwlln/.translations/sl.json +++ b/homeassistant/components/wwlln/.translations/sl.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "Ta lokacija je \u017ee registrirana.", - "window_too_small": "Premajhno okno bo povzro\u010dilo, da bo Home Assistant zamudil dogodke." - }, - "error": { - "identifier_exists": "Lokacija je \u017ee registrirana" + "already_configured": "Ta lokacija je \u017ee registrirana." }, "step": { "user": { diff --git a/homeassistant/components/wwlln/.translations/sv.json b/homeassistant/components/wwlln/.translations/sv.json index 4aa525f7a2a..3180c543452 100644 --- a/homeassistant/components/wwlln/.translations/sv.json +++ b/homeassistant/components/wwlln/.translations/sv.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "Platsen \u00e4r redan registrerad" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/zh-Hans.json b/homeassistant/components/wwlln/.translations/zh-Hans.json index d719802ad7a..e53d33512e1 100644 --- a/homeassistant/components/wwlln/.translations/zh-Hans.json +++ b/homeassistant/components/wwlln/.translations/zh-Hans.json @@ -1,8 +1,5 @@ { "config": { - "error": { - "identifier_exists": "\u4f4d\u7f6e\u5df2\u7ecf\u6ce8\u518c" - }, "step": { "user": { "data": { diff --git a/homeassistant/components/wwlln/.translations/zh-Hant.json b/homeassistant/components/wwlln/.translations/zh-Hant.json index b75c07a0813..fac13ffe77f 100644 --- a/homeassistant/components/wwlln/.translations/zh-Hant.json +++ b/homeassistant/components/wwlln/.translations/zh-Hant.json @@ -1,11 +1,7 @@ { "config": { "abort": { - "already_configured": "\u6b64\u4f4d\u7f6e\u5df2\u8a3b\u518a\u3002", - "window_too_small": "\u904e\u5c0f\u7684\u8996\u7a97\u5c07\u5c0e\u81f4 Home Assistant \u932f\u904e\u4e8b\u4ef6\u3002" - }, - "error": { - "identifier_exists": "\u5ea7\u6a19\u5df2\u8a3b\u518a" + "already_configured": "\u6b64\u4f4d\u7f6e\u5df2\u8a3b\u518a\u3002" }, "step": { "user": { From 9675cc5ed2338f3b51c04b27298338b456e900c9 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 7 Apr 2020 22:42:17 +0200 Subject: [PATCH 429/431] Updated frontend to 20200407.1 (#33799) --- 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 3c6e8478c09..efd9f99b18a 100644 --- a/homeassistant/components/frontend/manifest.json +++ b/homeassistant/components/frontend/manifest.json @@ -2,7 +2,7 @@ "domain": "frontend", "name": "Home Assistant Frontend", "documentation": "https://www.home-assistant.io/integrations/frontend", - "requirements": ["home-assistant-frontend==20200406.0"], + "requirements": ["home-assistant-frontend==20200407.1"], "dependencies": [ "api", "auth", diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index efc36dc1561..bf6888e7073 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -12,7 +12,7 @@ cryptography==2.8 defusedxml==0.6.0 distro==1.4.0 hass-nabucasa==0.32.2 -home-assistant-frontend==20200406.0 +home-assistant-frontend==20200407.1 importlib-metadata==1.5.0 jinja2>=2.11.1 netdisco==2.6.0 diff --git a/requirements_all.txt b/requirements_all.txt index 6d0e0e590eb..7cb73658492 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -704,7 +704,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200406.0 +home-assistant-frontend==20200407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 8a4af0beaf0..3403ad5a519 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -282,7 +282,7 @@ hole==0.5.1 holidays==0.10.1 # homeassistant.components.frontend -home-assistant-frontend==20200406.0 +home-assistant-frontend==20200407.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 From 837f7638cf9904bb6191c1493f0afced7257a880 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Wed, 8 Apr 2020 14:05:04 +0200 Subject: [PATCH 430/431] Bumped version to 0.108.0 --- homeassistant/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/homeassistant/const.py b/homeassistant/const.py index 9b82885ce72..4b829692ea5 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,7 +1,7 @@ """Constants used by Home Assistant components.""" MAJOR_VERSION = 0 MINOR_VERSION = 108 -PATCH_VERSION = "0b6" +PATCH_VERSION = "0" __short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" __version__ = f"{__short_version__}.{PATCH_VERSION}" REQUIRED_PYTHON_VER = (3, 7, 0) From 245eae89ebb97e2e53b475109b64c10fcbc47360 Mon Sep 17 00:00:00 2001 From: springstan <46536646+springstan@users.noreply.github.com> Date: Tue, 7 Apr 2020 18:22:03 +0200 Subject: [PATCH 431/431] Bump pyW215 to 0.7.0 (#33786) --- homeassistant/components/dlink/manifest.json | 2 +- requirements_all.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/dlink/manifest.json b/homeassistant/components/dlink/manifest.json index 7f5ff6cfd02..754a9ec5e03 100644 --- a/homeassistant/components/dlink/manifest.json +++ b/homeassistant/components/dlink/manifest.json @@ -2,7 +2,7 @@ "domain": "dlink", "name": "D-Link Wi-Fi Smart Plugs", "documentation": "https://www.home-assistant.io/integrations/dlink", - "requirements": ["pyW215==0.6.0"], + "requirements": ["pyW215==0.7.0"], "dependencies": [], "codeowners": [] } diff --git a/requirements_all.txt b/requirements_all.txt index 7cb73658492..15168678a85 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -1140,7 +1140,7 @@ pyRFXtrx==0.25 pyTibber==0.13.6 # homeassistant.components.dlink -pyW215==0.6.0 +pyW215==0.7.0 # homeassistant.components.w800rf32 pyW800rf32==0.1