diff --git a/.coveragerc b/.coveragerc index e36d6b252bd78f..251fe05c01446b 100644 --- a/.coveragerc +++ b/.coveragerc @@ -299,6 +299,7 @@ omit = homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* homeassistant/components/hlk_sw16/* + homeassistant/components/home_connect/* homeassistant/components/homematic/* homeassistant/components/homematic/climate.py homeassistant/components/homematic/cover.py @@ -878,6 +879,10 @@ omit = homeassistant/components/zoneminder/* homeassistant/components/supla/* homeassistant/components/zwave/util.py + homeassistant/components/zwave_mqtt/__init__.py + homeassistant/components/zwave_mqtt/discovery.py + homeassistant/components/zwave_mqtt/entity.py + homeassistant/components/zwave_mqtt/services.py [report] # Regexes for lines to exclude from consideration diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 05e062e43b9c0b..491cfc05d8aebd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ repos: - repo: https://github.com/asottile/pyupgrade - rev: v2.2.1 + rev: v2.3.0 hooks: - id: pyupgrade args: [--py37-plus] @@ -18,9 +18,9 @@ repos: - id: codespell args: - --ignore-words-list=hass,alot,datas,dof,dur,farenheit,hist,iff,ines,ist,lightsensor,mut,nd,pres,referer,ser,serie,te,technik,ue,uint,visability,wan,wanna,withing - - --skip="./.*,*.json" + - --skip="./.*,*.csv,*.json" - --quiet-level=2 - exclude_types: [json] + exclude_types: [csv, json] - repo: https://gitlab.com/pycqa/flake8 rev: 3.7.9 hooks: diff --git a/.travis.yml b/.travis.yml index 6add8c15bfc5a4..a01398651dab64 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ addons: - libswscale-dev - libswresample-dev - libavfilter-dev + sources: + - sourceline: ppa:savoury1/ffmpeg4 + matrix: fast_finish: true include: diff --git a/CODEOWNERS b/CODEOWNERS index a021a4a28ed175..1f53b1292b067f 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -53,6 +53,7 @@ homeassistant/components/azure_service_bus/* @hfurubotten homeassistant/components/beewi_smartclim/* @alemuro homeassistant/components/bitcoin/* @fabaff homeassistant/components/bizkaibus/* @UgaitzEtxebarria +homeassistant/components/blebox/* @gadgetmobile homeassistant/components/blink/* @fronzbot homeassistant/components/bmp280/* @belidzs homeassistant/components/bmw_connected_drive/* @gerard33 @@ -163,6 +164,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/history/* @home-assistant/core homeassistant/components/hive/* @Rendili @KJonline +homeassistant/components/home_connect/* @DavidMStraub homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco homeassistant/components/homekit_controller/* @Jc2k @@ -196,7 +198,7 @@ homeassistant/components/ipp/* @ctalkington homeassistant/components/iqvia/* @bachya homeassistant/components/irish_rail_transport/* @ttroy50 homeassistant/components/islamic_prayer_times/* @engrbm87 -homeassistant/components/isy994/* @bdraco +homeassistant/components/isy994/* @bdraco @shbatm homeassistant/components/izone/* @Swamp-Ig homeassistant/components/jewish_calendar/* @tsvi homeassistant/components/juicenet/* @jesserockz @@ -240,7 +242,7 @@ homeassistant/components/minecraft_server/* @elmurato homeassistant/components/minio/* @tkislan homeassistant/components/mobile_app/* @robbiet480 homeassistant/components/modbus/* @adamchengtkc @janiversen -homeassistant/components/monoprice/* @etsinko +homeassistant/components/monoprice/* @etsinko @OnFreund homeassistant/components/moon/* @fabaff homeassistant/components/mpd/* @fabaff homeassistant/components/mqtt/* @home-assistant/core @emontnemery @@ -463,6 +465,7 @@ homeassistant/components/zha/* @dmulcahey @adminiuga homeassistant/components/zone/* @home-assistant/core homeassistant/components/zoneminder/* @rohankapoorcom homeassistant/components/zwave/* @home-assistant/z-wave +homeassistant/components/zwave_mqtt/* @cgarwood @marcelveldt @MartinHjelmare # Individual files homeassistant/components/demo/weather @fabaff diff --git a/homeassistant/components/acer_projector/manifest.json b/homeassistant/components/acer_projector/manifest.json index 85ff4a3f5b1930..861e483adb85f5 100644 --- a/homeassistant/components/acer_projector/manifest.json +++ b/homeassistant/components/acer_projector/manifest.json @@ -2,6 +2,6 @@ "domain": "acer_projector", "name": "Acer Projector", "documentation": "https://www.home-assistant.io/integrations/acer_projector", - "requirements": ["pyserial==3.1.1"], + "requirements": ["pyserial==3.4"], "codeowners": [] } diff --git a/homeassistant/components/adguard/__init__.py b/homeassistant/components/adguard/__init__.py index 6996a2b0d51af4..f968f524f3db26 100644 --- a/homeassistant/components/adguard/__init__.py +++ b/homeassistant/components/adguard/__init__.py @@ -206,4 +206,5 @@ def device_info(self) -> Dict[str, Any]: "name": "AdGuard Home", "manufacturer": "AdGuard Team", "sw_version": self.hass.data[DOMAIN].get(DATA_ADGUARD_VERION), + "entry_type": "service", } diff --git a/homeassistant/components/adguard/strings.json b/homeassistant/components/adguard/strings.json index 471708879b496a..f5f780c70b5def 100644 --- a/homeassistant/components/adguard/strings.json +++ b/homeassistant/components/adguard/strings.json @@ -4,10 +4,10 @@ "user": { "description": "Set up your AdGuard Home instance to allow monitoring and control.", "data": { - "host": "Host", - "password": "Password", - "port": "Port", - "username": "Username", + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "port": "[%key:common::config_flow::data::port%]", + "username": "[%key:common::config_flow::data::username%]", "ssl": "AdGuard Home uses a SSL certificate", "verify_ssl": "AdGuard Home uses a proper certificate" } diff --git a/homeassistant/components/adguard/translations/es-419.json b/homeassistant/components/adguard/translations/es-419.json index 5a36b35d028a7d..f2ce862b083514 100644 --- a/homeassistant/components/adguard/translations/es-419.json +++ b/homeassistant/components/adguard/translations/es-419.json @@ -16,6 +16,7 @@ }, "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", "ssl": "AdGuard Home utiliza un certificado SSL", diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index 099fdfc5df71b8..4c46e7b3e7d3b8 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -331,6 +331,13 @@ def update(): self.update_from_latest_data() + async def async_update(self): + """Update the entity. + + Only used by the generic entity update service. + """ + await self.coordinator.async_request_refresh() + @callback def update_from_latest_data(self): """Update the entity from the latest data.""" diff --git a/homeassistant/components/airvisual/strings.json b/homeassistant/components/airvisual/strings.json index 8b9978b611fda5..07c27c01a91596 100644 --- a/homeassistant/components/airvisual/strings.json +++ b/homeassistant/components/airvisual/strings.json @@ -5,7 +5,7 @@ "title": "Configure a Geography", "description": "Use the AirVisual cloud API to monitor a geographical location.", "data": { - "api_key": "API Key", + "api_key": "[%key:common::config_flow::data::api_key%]", "latitude": "Latitude", "longitude": "Longitude" } diff --git a/homeassistant/components/airvisual/translations/es-419.json b/homeassistant/components/airvisual/translations/es-419.json index aea8c1ad574031..7b3116fe6366aa 100644 --- a/homeassistant/components/airvisual/translations/es-419.json +++ b/homeassistant/components/airvisual/translations/es-419.json @@ -4,7 +4,9 @@ "already_configured": "Estas coordenadas ya han sido registradas." }, "error": { - "invalid_api_key": "Clave de API inv\u00e1lida" + "general_error": "Se ha producido un error desconocido.", + "invalid_api_key": "Clave de API inv\u00e1lida", + "unable_to_connect": "No se puede conectar a la unidad Node/Pro." }, "step": { "geography": { @@ -13,12 +15,15 @@ "latitude": "Latitud", "longitude": "Longitud" }, + "description": "Use la API de AirVisual para monitorear una ubicaci\u00f3n geogr\u00e1fica.", "title": "Configurar una geograf\u00eda" }, "node_pro": { "data": { + "ip_address": "Direcci\u00f3n IP/nombre de host de la unidad", "password": "Contrase\u00f1a de la unidad" }, + "description": "Monitoree una unidad AirVisual personal. La contrase\u00f1a se puede recuperar de la interfaz de usuario de la unidad.", "title": "Configurar un AirVisual Node/Pro" }, "user": { diff --git a/homeassistant/components/airvisual/translations/fr.json b/homeassistant/components/airvisual/translations/fr.json index d2013b0f17dd50..1d0f35a437f896 100644 --- a/homeassistant/components/airvisual/translations/fr.json +++ b/homeassistant/components/airvisual/translations/fr.json @@ -4,7 +4,9 @@ "already_configured": "Cette cl\u00e9 API est d\u00e9j\u00e0 utilis\u00e9e." }, "error": { - "invalid_api_key": "Cl\u00e9 API invalide" + "general_error": "Une erreur inconnue est survenue.", + "invalid_api_key": "Cl\u00e9 API invalide", + "unable_to_connect": "Impossible de se connecter \u00e0 l'unit\u00e9 Node / Pro." }, "step": { "geography": { @@ -12,13 +14,25 @@ "api_key": "Cl\u00e9 d'API", "latitude": "Latitude", "longitude": "Longitude" - } + }, + "title": "Configurer une g\u00e9ographie" + }, + "node_pro": { + "data": { + "ip_address": "Adresse IP / nom d'h\u00f4te de l'unit\u00e9", + "password": "Mot de passe de l'unit\u00e9" + }, + "description": "Surveillez une unit\u00e9 AirVisual personnelle. Le mot de passe peut \u00eatre r\u00e9cup\u00e9r\u00e9 dans l'interface utilisateur de l'unit\u00e9.", + "title": "Configurer un AirVisual Node/Pro" }, "user": { "data": { "api_key": "Cl\u00e9 API", + "cloud_api": "Localisation g\u00e9ographique", "latitude": "Latitude", - "longitude": "Longitude" + "longitude": "Longitude", + "node_pro": "AirVisual Node Pro", + "type": "Type d'int\u00e9gration" }, "description": "Surveiller la qualit\u00e9 de l\u2019air dans un emplacement g\u00e9ographique.", "title": "Configurer AirVisual" diff --git a/homeassistant/components/alexa/entities.py b/homeassistant/components/alexa/entities.py index e22c5c62db907d..09ce71bb3bc8ab 100644 --- a/homeassistant/components/alexa/entities.py +++ b/homeassistant/components/alexa/entities.py @@ -386,7 +386,7 @@ class CoverCapabilities(AlexaEntity): def default_display_categories(self): """Return the display categories for this entity.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class == cover.DEVICE_CLASS_GARAGE: + if device_class in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): return [DisplayCategory.GARAGE_DOOR] if device_class == cover.DEVICE_CLASS_DOOR: return [DisplayCategory.DOOR] @@ -408,7 +408,7 @@ def default_display_categories(self): def interfaces(self): """Yield the supported interfaces.""" device_class = self.entity.attributes.get(ATTR_DEVICE_CLASS) - if device_class != cover.DEVICE_CLASS_GARAGE: + if device_class not in (cover.DEVICE_CLASS_GARAGE, cover.DEVICE_CLASS_GATE): yield AlexaPowerController(self.entity) supported = self.entity.attributes.get(ATTR_SUPPORTED_FEATURES, 0) diff --git a/homeassistant/components/alpha_vantage/manifest.json b/homeassistant/components/alpha_vantage/manifest.json index dad5fc88e804c0..28103980869ac4 100644 --- a/homeassistant/components/alpha_vantage/manifest.json +++ b/homeassistant/components/alpha_vantage/manifest.json @@ -2,6 +2,6 @@ "domain": "alpha_vantage", "name": "Alpha Vantage", "documentation": "https://www.home-assistant.io/integrations/alpha_vantage", - "requirements": ["alpha_vantage==2.1.3"], + "requirements": ["alpha_vantage==2.2.0"], "codeowners": ["@fabaff"] } diff --git a/homeassistant/components/atag/translations/es-419.json b/homeassistant/components/atag/translations/es-419.json index a833218e31137d..214dd0e9004b90 100644 --- a/homeassistant/components/atag/translations/es-419.json +++ b/homeassistant/components/atag/translations/es-419.json @@ -9,6 +9,7 @@ "step": { "user": { "data": { + "host": "Host", "port": "Puerto (10000)" }, "title": "Conectarse al dispositivo" diff --git a/homeassistant/components/atag/translations/fr.json b/homeassistant/components/atag/translations/fr.json index ace565408f641a..f7cf39f001ca43 100644 --- a/homeassistant/components/atag/translations/fr.json +++ b/homeassistant/components/atag/translations/fr.json @@ -1,5 +1,11 @@ { "config": { + "abort": { + "already_configured": "Un seul appareil Atag peut \u00eatre ajout\u00e9 \u00e0 Home Assistant" + }, + "error": { + "connection_error": "Impossible de se connecter, veuillez r\u00e9essayer" + }, "step": { "user": { "data": { @@ -9,5 +15,6 @@ "title": "Se connecter \u00e0 l'appareil" } } - } + }, + "title": "Atag" } \ No newline at end of file diff --git a/homeassistant/components/atag/water_heater.py b/homeassistant/components/atag/water_heater.py index bb1f72d6a8e096..2b2093c3c7ee31 100644 --- a/homeassistant/components/atag/water_heater.py +++ b/homeassistant/components/atag/water_heater.py @@ -49,7 +49,7 @@ def operation_list(self): """List of available operation modes.""" return OPERATION_LIST - async def set_temperature(self, **kwargs): + async def async_set_temperature(self, **kwargs): """Set new target temperature.""" if await self.coordinator.atag.dhw_set_temp(kwargs.get(ATTR_TEMPERATURE)): self.async_write_ha_state() diff --git a/homeassistant/components/auth/translations/no.json b/homeassistant/components/auth/translations/no.json index 48b5db8a3b6063..1758219c56a4c8 100644 --- a/homeassistant/components/auth/translations/no.json +++ b/homeassistant/components/auth/translations/no.json @@ -26,7 +26,7 @@ "step": { "init": { "description": "For \u00e5 aktivere tofaktorautentisering ved hjelp av tidsbaserte engangspassord, skann QR-koden med autentiseringsappen din. Hvis du ikke har en, kan vi anbefale enten [Google Authenticator](https://support.google.com/accounts/answer/1066447) eller [Authy](https://authy.com/). \n\n {qr_code} \n \nEtter at du har skannet koden, skriver du inn den seks-sifrede koden fra appen din for \u00e5 kontrollere oppsettet. Dersom du har problemer med \u00e5 skanne QR-koden kan du taste inn f\u00f8lgende kode manuelt: **`{code}`**.", - "title": "Konfigurer tofaktorautentisering ved hjelp av TOTP" + "title": "Sett opp tofaktorautentisering ved hjelp av TOTP" } }, "title": "TOTP" diff --git a/homeassistant/components/axis/translations/es-419.json b/homeassistant/components/axis/translations/es-419.json index a86131723e3ae5..b5d1cb4ca7b700 100644 --- a/homeassistant/components/axis/translations/es-419.json +++ b/homeassistant/components/axis/translations/es-419.json @@ -12,9 +12,11 @@ "device_unavailable": "El dispositivo no est\u00e1 disponible", "faulty_credentials": "Credenciales de usuario incorrectas" }, + "flow_title": "Dispositivo Axis: {name} ({host})", "step": { "user": { "data": { + "host": "Host", "password": "Contrase\u00f1a", "port": "Puerto", "username": "Nombre de usuario" diff --git a/homeassistant/components/binary_sensor/translations/es-419.json b/homeassistant/components/binary_sensor/translations/es-419.json index 5bada49741e19d..d8cc4219097870 100644 --- a/homeassistant/components/binary_sensor/translations/es-419.json +++ b/homeassistant/components/binary_sensor/translations/es-419.json @@ -28,6 +28,7 @@ "is_not_occupied": "{entity_name} no est\u00e1 ocupado", "is_not_open": "{entity_name} est\u00e1 cerrado", "is_not_plugged_in": "{entity_name} est\u00e1 desconectado", + "is_not_powered": "{entity_name} no tiene encendido", "is_not_present": "{entity_name} no est\u00e1 presente", "is_not_unsafe": "{entity_name} es seguro", "is_occupied": "{entity_name} est\u00e1 ocupado", @@ -68,13 +69,16 @@ "not_locked": "{entity_name} desbloqueado", "not_moist": "{entity_name} se sec\u00f3", "not_moving": "{entity_name} dej\u00f3 de moverse", + "not_occupied": "{entity_name} se desocup\u00f3", "not_opened": "{entity_name} cerrado", "not_plugged_in": "{entity_name} desconectado", + "not_powered": "{entity_name} no encendido", "not_present": "{entity_name} no presente", "not_unsafe": "{entity_name} se volvi\u00f3 seguro", "occupied": "{entity_name} se ocup\u00f3", "opened": "{entity_name} abierto", "plugged_in": "{entity_name} enchufado", + "powered": "{entity_name} encendido", "present": "{entity_name} presente", "problem": "{entity_name} comenz\u00f3 a detectar problemas", "smoke": "{entity_name} comenz\u00f3 a detectar humo", diff --git a/homeassistant/components/blebox/__init__.py b/homeassistant/components/blebox/__init__.py new file mode 100644 index 00000000000000..dcdd4c7f1e4ec5 --- /dev/null +++ b/homeassistant/components/blebox/__init__.py @@ -0,0 +1,121 @@ +"""The BleBox devices integration.""" +import asyncio +import logging + +from blebox_uniapi.error import Error +from blebox_uniapi.products import Products +from blebox_uniapi.session import ApiHost + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import Entity + +from .const import DEFAULT_SETUP_TIMEOUT, DOMAIN, PRODUCT + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["cover"] + +PARALLEL_UPDATES = 0 + + +async def async_setup(hass: HomeAssistant, config: dict): + """Set up the BleBox devices component.""" + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up BleBox devices from a config entry.""" + + websession = async_get_clientsession(hass) + + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + timeout = DEFAULT_SETUP_TIMEOUT + + api_host = ApiHost(host, port, timeout, websession, hass.loop) + + try: + product = await Products.async_from_host(api_host) + except Error as ex: + _LOGGER.error("Identify failed at %s:%d (%s)", api_host.host, api_host.port, ex) + raise ConfigEntryNotReady from ex + + domain = hass.data.setdefault(DOMAIN, {}) + domain_entry = domain.setdefault(entry.entry_id, {}) + product = domain_entry.setdefault(PRODUCT, product) + + 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, platform) + for platform in PLATFORMS + ] + ) + ) + + if unload_ok: + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok + + +@callback +def create_blebox_entities(product, async_add, entity_klass, entity_type): + """Create entities from a BleBox product's features.""" + + entities = [] + for feature in product.features[entity_type]: + entities.append(entity_klass(feature)) + + async_add(entities, True) + + +class BleBoxEntity(Entity): + """Implements a common class for entities representing a BleBox feature.""" + + def __init__(self, feature): + """Initialize a BleBox entity.""" + self._feature = feature + + @property + def name(self): + """Return the internal entity name.""" + return self._feature.full_name + + @property + def unique_id(self): + """Return a unique id.""" + return self._feature.unique_id + + async def async_update(self): + """Update the entity state.""" + try: + await self._feature.async_update() + except Error as ex: + _LOGGER.error("Updating '%s' failed: %s", self.name, ex) + + @property + def device_info(self): + """Return device information for this entity.""" + product = self._feature.product + return { + "identifiers": {(DOMAIN, product.unique_id)}, + "name": product.name, + "manufacturer": product.brand, + "model": product.model, + "sw_version": product.firmware_version, + } diff --git a/homeassistant/components/blebox/config_flow.py b/homeassistant/components/blebox/config_flow.py new file mode 100644 index 00000000000000..1c73346ddf9ecf --- /dev/null +++ b/homeassistant/components/blebox/config_flow.py @@ -0,0 +1,128 @@ +"""Config flow for BleBox devices integration.""" +import logging + +from blebox_uniapi.error import Error, UnsupportedBoxVersion +from blebox_uniapi.products import Products +from blebox_uniapi.session import ApiHost +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import ( + ADDRESS_ALREADY_CONFIGURED, + CANNOT_CONNECT, + DEFAULT_HOST, + DEFAULT_PORT, + DEFAULT_SETUP_TIMEOUT, + DOMAIN, + UNKNOWN, + UNSUPPORTED_VERSION, +) + +_LOGGER = logging.getLogger(__name__) + + +def host_port(data): + """Return a list with host and port.""" + return (data[CONF_HOST], data[CONF_PORT]) + + +def create_schema(previous_input=None): + """Create a schema with given values as default.""" + if previous_input is not None: + host, port = host_port(previous_input) + else: + host = DEFAULT_HOST + port = DEFAULT_PORT + + return vol.Schema( + { + vol.Required(CONF_HOST, default=host): str, + vol.Required(CONF_PORT, default=port): int, + } + ) + + +LOG_MSG = { + UNSUPPORTED_VERSION: "Outdated firmware", + CANNOT_CONNECT: "Failed to identify device", + UNKNOWN: "Unknown error while identifying device", +} + + +class BleBoxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for BleBox devices.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + def __init__(self): + """Initialize the BleBox config flow.""" + self.device_config = {} + + def handle_step_exception( + self, step, exception, schema, host, port, message_id, log_fn + ): + """Handle step exceptions.""" + + log_fn("%s at %s:%d (%s)", LOG_MSG[message_id], host, port, exception) + + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={"base": message_id}, + description_placeholders={"address": f"{host}:{port}"}, + ) + + async def async_step_user(self, user_input=None): + """Handle initial user-triggered config step.""" + + hass = self.hass + schema = create_schema(user_input) + + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=schema, + errors={}, + description_placeholders={}, + ) + + addr = host_port(user_input) + + for entry in hass.config_entries.async_entries(DOMAIN): + if addr == host_port(entry.data): + host, port = addr + return self.async_abort( + reason=ADDRESS_ALREADY_CONFIGURED, + description_placeholders={"address": f"{host}:{port}"}, + ) + + websession = async_get_clientsession(hass) + api_host = ApiHost(*addr, DEFAULT_SETUP_TIMEOUT, websession, hass.loop, _LOGGER) + + try: + product = await Products.async_from_host(api_host) + + except UnsupportedBoxVersion as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, UNSUPPORTED_VERSION, _LOGGER.debug + ) + + except Error as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, CANNOT_CONNECT, _LOGGER.warning + ) + + except RuntimeError as ex: + return self.handle_step_exception( + "user", ex, schema, *addr, UNKNOWN, _LOGGER.error + ) + + # Check if configured but IP changed since + await self.async_set_unique_id(product.unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry(title=product.name, data=user_input) diff --git a/homeassistant/components/blebox/const.py b/homeassistant/components/blebox/const.py new file mode 100644 index 00000000000000..a53ec39fd4702e --- /dev/null +++ b/homeassistant/components/blebox/const.py @@ -0,0 +1,45 @@ +"""Constants for the BleBox devices integration.""" + +from homeassistant.components.cover import ( + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHUTTER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, +) + +DOMAIN = "blebox" +PRODUCT = "product" + +DEFAULT_SETUP_TIMEOUT = 3 + +# translation strings +ADDRESS_ALREADY_CONFIGURED = "address_already_configured" +CANNOT_CONNECT = "cannot_connect" +UNSUPPORTED_VERSION = "unsupported_version" +UNKNOWN = "unknown" + +BLEBOX_TO_HASS_DEVICE_CLASSES = { + "shutter": DEVICE_CLASS_SHUTTER, + "gatebox": DEVICE_CLASS_DOOR, + "gate": DEVICE_CLASS_GATE, +} + +BLEBOX_TO_HASS_COVER_STATES = { + None: None, + 0: STATE_CLOSING, # moving down + 1: STATE_OPENING, # moving up + 2: STATE_OPEN, # manually stopped + 3: STATE_CLOSED, # lower limit + 4: STATE_OPEN, # upper limit / open + # gateController + 5: STATE_OPEN, # overload + 6: STATE_OPEN, # motor failure + # 7 is not used + 8: STATE_OPEN, # safety stop +} + +DEFAULT_HOST = "192.168.0.2" +DEFAULT_PORT = 80 diff --git a/homeassistant/components/blebox/cover.py b/homeassistant/components/blebox/cover.py new file mode 100644 index 00000000000000..2a8f02192671fa --- /dev/null +++ b/homeassistant/components/blebox/cover.py @@ -0,0 +1,97 @@ +"""BleBox cover entity.""" + +from homeassistant.components.cover import ( + ATTR_POSITION, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPENING, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, + CoverEntity, +) + +from . import BleBoxEntity, create_blebox_entities +from .const import ( + BLEBOX_TO_HASS_COVER_STATES, + BLEBOX_TO_HASS_DEVICE_CLASSES, + DOMAIN, + PRODUCT, +) + + +async def async_setup_entry(hass, config_entry, async_add): + """Set up a BleBox entry.""" + + product = hass.data[DOMAIN][config_entry.entry_id][PRODUCT] + create_blebox_entities(product, async_add, BleBoxCoverEntity, "covers") + return True + + +class BleBoxCoverEntity(BleBoxEntity, CoverEntity): + """Representation of a BleBox cover feature.""" + + @property + def state(self): + """Return the equivalent HA cover state.""" + return BLEBOX_TO_HASS_COVER_STATES[self._feature.state] + + @property + def device_class(self): + """Return the device class.""" + return BLEBOX_TO_HASS_DEVICE_CLASSES[self._feature.device_class] + + @property + def supported_features(self): + """Return the supported cover features.""" + position = SUPPORT_SET_POSITION if self._feature.is_slider else 0 + stop = SUPPORT_STOP if self._feature.has_stop else 0 + + return position | stop | SUPPORT_OPEN | SUPPORT_CLOSE + + @property + def current_cover_position(self): + """Return the current cover position.""" + position = self._feature.current + if position == -1: # possible for shutterBox + return None + + return None if position is None else 100 - position + + @property + def is_opening(self): + """Return whether cover is opening.""" + return self._is_state(STATE_OPENING) + + @property + def is_closing(self): + """Return whether cover is closing.""" + return self._is_state(STATE_CLOSING) + + @property + def is_closed(self): + """Return whether cover is closed.""" + return self._is_state(STATE_CLOSED) + + async def async_open_cover(self, **kwargs): + """Open the cover position.""" + await self._feature.async_open() + + async def async_close_cover(self, **kwargs): + """Close the cover position.""" + await self._feature.async_close() + + async def async_set_cover_position(self, **kwargs): + """Set the cover position.""" + + position = kwargs[ATTR_POSITION] + await self._feature.async_set_position(100 - position) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + await self._feature.async_stop() + + def _is_state(self, state_name): + value = self.state + return None if value is None else value == state_name diff --git a/homeassistant/components/blebox/manifest.json b/homeassistant/components/blebox/manifest.json new file mode 100644 index 00000000000000..703d9042270d43 --- /dev/null +++ b/homeassistant/components/blebox/manifest.json @@ -0,0 +1,8 @@ +{ + "domain": "blebox", + "name": "BleBox devices", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/blebox", + "requirements": ["blebox_uniapi==1.3.2"], + "codeowners": [ "@gadgetmobile" ] +} diff --git a/homeassistant/components/blebox/strings.json b/homeassistant/components/blebox/strings.json new file mode 100644 index 00000000000000..8106388dfa9208 --- /dev/null +++ b/homeassistant/components/blebox/strings.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This BleBox device is already configured.", + "address_already_configured": "A BleBox device is already configured at {address}." + }, + "error": { + "cannot_connect": "Unable to connect to the BleBox device. (Check the logs for errors.)", + "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first.", + "unknown": "Unknown error while connecting to the BleBox device. (Check the logs for errors.)" + }, + "flow_title": "BleBox device: {name} ({host})", + "step": { + "user": { + "description": "Set up your BleBox to integrate with Home Assistant.", + "data": { + "host": "IP address", + "port": "Port" + }, + "title": "Set up your BleBox device" + } + } + } +} diff --git a/homeassistant/components/blebox/translations/en.json b/homeassistant/components/blebox/translations/en.json new file mode 100644 index 00000000000000..8106388dfa9208 --- /dev/null +++ b/homeassistant/components/blebox/translations/en.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "This BleBox device is already configured.", + "address_already_configured": "A BleBox device is already configured at {address}." + }, + "error": { + "cannot_connect": "Unable to connect to the BleBox device. (Check the logs for errors.)", + "unsupported_version": "BleBox device has outdated firmware. Please upgrade it first.", + "unknown": "Unknown error while connecting to the BleBox device. (Check the logs for errors.)" + }, + "flow_title": "BleBox device: {name} ({host})", + "step": { + "user": { + "description": "Set up your BleBox to integrate with Home Assistant.", + "data": { + "host": "IP address", + "port": "Port" + }, + "title": "Set up your BleBox device" + } + } + } +} diff --git a/homeassistant/components/braviatv/manifest.json b/homeassistant/components/braviatv/manifest.json index cde236b4ca4d8a..98c1dca08d2c67 100644 --- a/homeassistant/components/braviatv/manifest.json +++ b/homeassistant/components/braviatv/manifest.json @@ -2,7 +2,7 @@ "domain": "braviatv", "name": "Sony Bravia TV", "documentation": "https://www.home-assistant.io/integrations/braviatv", - "requirements": ["bravia-tv==1.0.2"], + "requirements": ["bravia-tv==1.0.3"], "codeowners": ["@robbiet480", "@bieniu"], "config_flow": true } diff --git a/homeassistant/components/braviatv/translations/es-419.json b/homeassistant/components/braviatv/translations/es-419.json index 48457826a5250c..820ea329a0cb39 100644 --- a/homeassistant/components/braviatv/translations/es-419.json +++ b/homeassistant/components/braviatv/translations/es-419.json @@ -4,6 +4,8 @@ "already_configured": "Esta televisi\u00f3n ya est\u00e1 configurada." }, "error": { + "cannot_connect": "No se pudo conectar, host inv\u00e1lido o c\u00f3digo PIN.", + "invalid_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos.", "unsupported_model": "Su modelo de televisi\u00f3n no es compatible." }, "step": { @@ -11,9 +13,14 @@ "data": { "pin": "C\u00f3digo PIN" }, + "description": "Ingrese el c\u00f3digo PIN que se muestra en la televisi\u00f3n Sony Bravia. \n\nSi no se muestra el c\u00f3digo PIN, debe cancelar el registro de Home Assistant en su televisi\u00f3n, vaya a: Configuraci\u00f3n -> Red -> Configuraci\u00f3n del dispositivo remoto - > Cancelar registro del dispositivo remoto.", "title": "Autorizar Sony Bravia TV" }, "user": { + "data": { + "host": "Nombre de host de TV o direcci\u00f3n IP" + }, + "description": "Configure la integraci\u00f3n de Sony Bravia TV. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/braviatv \n\n Aseg\u00farese de que su televisi\u00f3n est\u00e9 encendida.", "title": "Sony Bravia TV" } } diff --git a/homeassistant/components/braviatv/translations/no.json b/homeassistant/components/braviatv/translations/no.json index 45644446a3bba7..f9d034d48fb742 100644 --- a/homeassistant/components/braviatv/translations/no.json +++ b/homeassistant/components/braviatv/translations/no.json @@ -20,7 +20,7 @@ "data": { "host": "TV-vertsnavn eller IP-adresse" }, - "description": "Konfigurer Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: https://www.home-assistant.io/integrations/braviatv \n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.", + "description": "Sett opp Sony Bravia TV-integrasjon. Hvis du har problemer med konfigurasjonen, g\u00e5 til: https://www.home-assistant.io/integrations/braviatv \n\n Forsikre deg om at TV-en er sl\u00e5tt p\u00e5.", "title": "" } } diff --git a/homeassistant/components/brother/translations/es-419.json b/homeassistant/components/brother/translations/es-419.json index 0cb35449bc517d..286851ba4543a2 100644 --- a/homeassistant/components/brother/translations/es-419.json +++ b/homeassistant/components/brother/translations/es-419.json @@ -6,12 +6,14 @@ }, "error": { "connection_error": "Error de conexi\u00f3n.", - "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible." + "snmp_error": "El servidor SNMP est\u00e1 apagado o la impresora no es compatible.", + "wrong_host": "Nombre de host o direcci\u00f3n IP no v\u00e1lidos." }, "flow_title": "Impresora Brother: {model} {serial_number}", "step": { "user": { "data": { + "host": "Nombre de host de la impresora o direcci\u00f3n IP", "type": "Tipo de impresora" }, "description": "Configure la integraci\u00f3n de la impresora Brother. Si tiene problemas con la configuraci\u00f3n, vaya a: https://www.home-assistant.io/integrations/brother", diff --git a/homeassistant/components/brother/translations/no.json b/homeassistant/components/brother/translations/no.json index 0235c4d1693fbd..51c75ce779f289 100644 --- a/homeassistant/components/brother/translations/no.json +++ b/homeassistant/components/brother/translations/no.json @@ -16,7 +16,7 @@ "host": "Vertsnavn eller IP-adresse til skriveren", "type": "Skriver type" }, - "description": "Konfigurer Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: https://www.home-assistant.io/integrations/brother", + "description": "Sett opp Brother skriver integrasjonen. Hvis du har problemer med konfigurasjonen, bes\u00f8k dokumentasjonen her: https://www.home-assistant.io/integrations/brother", "title": "Brother skriver" }, "zeroconf_confirm": { diff --git a/homeassistant/components/canary/alarm_control_panel.py b/homeassistant/components/canary/alarm_control_panel.py index a5930e658fb9be..ea0e3078b0c269 100644 --- a/homeassistant/components/canary/alarm_control_panel.py +++ b/homeassistant/components/canary/alarm_control_panel.py @@ -24,10 +24,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Canary alarms.""" data = hass.data[DATA_CANARY] - devices = [] - - for location in data.locations: - devices.append(CanaryAlarm(data, location.location_id)) + devices = [CanaryAlarm(data, location.location_id) for location in data.locations] add_entities(devices, True) diff --git a/homeassistant/components/canary/camera.py b/homeassistant/components/canary/camera.py index 870256ffcff5ea..3ba7f094da145c 100644 --- a/homeassistant/components/canary/camera.py +++ b/homeassistant/components/canary/camera.py @@ -29,6 +29,9 @@ def setup_platform(hass, config, add_entities, discovery_info=None): """Set up the Canary sensors.""" + if discovery_info is not None: + return + data = hass.data[DATA_CANARY] devices = [] diff --git a/homeassistant/components/cert_expiry/translations/es-419.json b/homeassistant/components/cert_expiry/translations/es-419.json index 772e37e25c8250..2809d3c28993bf 100644 --- a/homeassistant/components/cert_expiry/translations/es-419.json +++ b/homeassistant/components/cert_expiry/translations/es-419.json @@ -1,9 +1,11 @@ { "config": { "abort": { + "already_configured": "Esta combinaci\u00f3n de host y puerto ya est\u00e1 configurada", "import_failed": "La importaci\u00f3n desde la configuraci\u00f3n fall\u00f3" }, "error": { + "connection_refused": "Conexi\u00f3n rechazada al conectarse al host", "connection_timeout": "Tiempo de espera al conectarse a este host", "resolve_failed": "Este host no puede resolverse" }, diff --git a/homeassistant/components/climate/translations/es-419.json b/homeassistant/components/climate/translations/es-419.json index d61483edda2732..569d5766f74579 100644 --- a/homeassistant/components/climate/translations/es-419.json +++ b/homeassistant/components/climate/translations/es-419.json @@ -1,10 +1,17 @@ { "device_automation": { "action_type": { - "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}" + "set_hvac_mode": "Cambiar el modo HVAC en {entity_name}", + "set_preset_mode": "Cambiar el ajuste preestablecido en el valor de {entity_name}" }, "condition_type": { - "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico" + "is_hvac_mode": "{entity_name} est\u00e1 configurado en un modo HVAC espec\u00edfico", + "is_preset_mode": "{entity_name} est\u00e1 configurado en un modo preestablecido espec\u00edfico" + }, + "trigger_type": { + "current_humidity_changed": "{entity_name} ha cambiado la humedad medida", + "current_temperature_changed": "{entity_name} ha cambiado la temperatura medida", + "hvac_mode_changed": "{entity_name} modo HVAC cambi\u00f3" } }, "state": { diff --git a/homeassistant/components/config/device_registry.py b/homeassistant/components/config/device_registry.py index 08f53f948fe09b..5b12ccb92eb5c1 100644 --- a/homeassistant/components/config/device_registry.py +++ b/homeassistant/components/config/device_registry.py @@ -71,6 +71,7 @@ def _entry_dict(entry): "model": entry.model, "name": entry.name, "sw_version": entry.sw_version, + "entry_type": entry.entry_type, "id": entry.id, "via_device_id": entry.via_device_id, "area_id": entry.area_id, diff --git a/homeassistant/components/coolmaster/translations/es-419.json b/homeassistant/components/coolmaster/translations/es-419.json index e1da9263a0c1b5..8cdf9675fd2094 100644 --- a/homeassistant/components/coolmaster/translations/es-419.json +++ b/homeassistant/components/coolmaster/translations/es-419.json @@ -1,8 +1,13 @@ { "config": { + "error": { + "connection_error": "Error al conectarse a la instancia de CoolMasterNet. Por favor revise su host.", + "no_units": "No se encontraron unidades de HVAC en el host CoolMasterNet." + }, "step": { "user": { "data": { + "host": "Host", "off": "Puede ser apagado" }, "title": "Configure los detalles de su conexi\u00f3n CoolMasterNet." diff --git a/homeassistant/components/coronavirus/translations/es-419.json b/homeassistant/components/coronavirus/translations/es-419.json new file mode 100644 index 00000000000000..1a1139a8f31913 --- /dev/null +++ b/homeassistant/components/coronavirus/translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "Este pa\u00eds ya est\u00e1 configurado." + }, + "step": { + "user": { + "data": { + "country": "Pa\u00eds" + }, + "title": "Seleccione un pa\u00eds para monitorear" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/cover/__init__.py b/homeassistant/components/cover/__init__.py index 84494e60c6ac39..ef17abc8a4076f 100644 --- a/homeassistant/components/cover/__init__.py +++ b/homeassistant/components/cover/__init__.py @@ -94,10 +94,12 @@ async def async_setup(hass, config): await component.async_setup(config) - component.async_register_entity_service(SERVICE_OPEN_COVER, {}, "async_open_cover") + component.async_register_entity_service( + SERVICE_OPEN_COVER, {}, "async_open_cover", [SUPPORT_OPEN] + ) component.async_register_entity_service( - SERVICE_CLOSE_COVER, {}, "async_close_cover" + SERVICE_CLOSE_COVER, {}, "async_close_cover", [SUPPORT_CLOSE] ) component.async_register_entity_service( @@ -108,22 +110,27 @@ async def async_setup(hass, config): ) }, "async_set_cover_position", + [SUPPORT_SET_POSITION], ) - component.async_register_entity_service(SERVICE_STOP_COVER, {}, "async_stop_cover") + component.async_register_entity_service( + SERVICE_STOP_COVER, {}, "async_stop_cover", [SUPPORT_STOP] + ) - component.async_register_entity_service(SERVICE_TOGGLE, {}, "async_toggle") + component.async_register_entity_service( + SERVICE_TOGGLE, {}, "async_toggle", [SUPPORT_OPEN | SUPPORT_CLOSE] + ) component.async_register_entity_service( - SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt" + SERVICE_OPEN_COVER_TILT, {}, "async_open_cover_tilt", [SUPPORT_OPEN_TILT] ) component.async_register_entity_service( - SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt" + SERVICE_CLOSE_COVER_TILT, {}, "async_close_cover_tilt", [SUPPORT_CLOSE_TILT] ) component.async_register_entity_service( - SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt" + SERVICE_STOP_COVER_TILT, {}, "async_stop_cover_tilt", [SUPPORT_STOP_TILT] ) component.async_register_entity_service( @@ -134,10 +141,14 @@ async def async_setup(hass, config): ) }, "async_set_cover_tilt_position", + [SUPPORT_SET_TILT_POSITION], ) component.async_register_entity_service( - SERVICE_TOGGLE_COVER_TILT, {}, "async_toggle_tilt" + SERVICE_TOGGLE_COVER_TILT, + {}, + "async_toggle_tilt", + [SUPPORT_OPEN_TILT | SUPPORT_CLOSE_TILT], ) return True diff --git a/homeassistant/components/cover/translations/es-419.json b/homeassistant/components/cover/translations/es-419.json index 3593ba289604e6..c6f9f7db7dd183 100644 --- a/homeassistant/components/cover/translations/es-419.json +++ b/homeassistant/components/cover/translations/es-419.json @@ -1,4 +1,30 @@ { + "device_automation": { + "action_type": { + "close": "Cerrar {entity_name}", + "close_tilt": "Cerrar la inclinaci\u00f3n de {entity_name}", + "open": "Abrir {entity_name}", + "open_tilt": "Abrir la inclinaci\u00f3n de {entity_name}", + "set_position": "Establecer la posici\u00f3n de {entity_name}", + "set_tilt_position": "Establecer la posici\u00f3n de inclinaci\u00f3n {entity_name}" + }, + "condition_type": { + "is_closed": "{entity_name} est\u00e1 cerrado", + "is_closing": "{entity_name} se est\u00e1 cerrando", + "is_open": "{entity_name} est\u00e1 abierto", + "is_opening": "{entity_name} se est\u00e1 abriendo", + "is_position": "La posici\u00f3n actual de {entity_name} es", + "is_tilt_position": "La posici\u00f3n de inclinaci\u00f3n actual de {entity_name} es" + }, + "trigger_type": { + "closed": "{entity_name} cerrado", + "closing": "{entity_name} cerrando", + "opened": "{entity_name} abierto", + "opening": "{entity_name} abriendo", + "position": "Cambios de posici\u00f3n de {entity_name}", + "tilt_position": "Cambios en la posici\u00f3n de inclinaci\u00f3n cambi\u00f3 de {entity_name}" + } + }, "state": { "_": { "closed": "Cerrado", diff --git a/homeassistant/components/deconz/translations/es-419.json b/homeassistant/components/deconz/translations/es-419.json index 8208e2578b0315..208616b7ebe385 100644 --- a/homeassistant/components/deconz/translations/es-419.json +++ b/homeassistant/components/deconz/translations/es-419.json @@ -11,6 +11,7 @@ "error": { "no_key": "No se pudo obtener una clave de API" }, + "flow_title": "Puerta de enlace Zigbee deCONZ ({host})", "step": { "hassio_confirm": { "description": "\u00bfDesea configurar Home Assistant para conectarse a la puerta de enlace deCONZ proporcionada por el complemento hass.io {addon}?", @@ -31,28 +32,83 @@ }, "manual_input": { "data": { + "host": "Host", "port": "Puerto" - } + }, + "title": "Configurar la puerta de enlace deCONZ" + }, + "user": { + "data": { + "host": "Seleccione la puerta de enlace descubierta deCONZ" + }, + "title": "Seleccione la puerta de enlace deCONZ" } } }, "device_automation": { "trigger_subtype": { "both_buttons": "Ambos botones", + "bottom_buttons": "Botones inferiores", "button_1": "Primer bot\u00f3n", "button_2": "Segundo bot\u00f3n", "button_3": "Tercer bot\u00f3n", "button_4": "Cuarto bot\u00f3n", "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Aumentar intensidad", "left": "Izquierda", "open": "Abrir", "right": "Derecha", + "side_1": "Lado 1", + "side_2": "Lado 2", + "side_3": "Lado 3", + "side_4": "Lado 4", + "side_5": "Lado 5", + "side_6": "Lado 6", + "top_buttons": "Botones superiores", "turn_off": "Apagar", "turn_on": "Encender" }, "trigger_type": { + "remote_awakened": "Dispositivo despertado", + "remote_button_double_press": "El bot\u00f3n \"{subtype}\" fue presionado 2 veces", + "remote_button_long_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado continuamente", + "remote_button_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 4 veces", + "remote_button_quintuple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 5 veces", "remote_button_rotated": "Bot\u00f3n girado \"{subtype}\"", - "remote_gyro_activated": "Dispositivo agitado" + "remote_button_rotation_stopped": "Se detuvo la rotaci\u00f3n del bot\u00f3n \"{subtype}\"", + "remote_button_short_press": "Se presion\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_short_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_triple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 3 veces", + "remote_double_tap": "Dispositivo \"{subtype}\" doble toque", + "remote_double_tap_any_side": "Dispositivo con doble toque en cualquier lado", + "remote_falling": "Dispositivo en ca\u00edda libre", + "remote_flip_180_degrees": "Dispositivo volteado 180 grados", + "remote_flip_90_degrees": "Dispositivo volteado 90 grados", + "remote_gyro_activated": "Dispositivo agitado", + "remote_moved": "Dispositivo movido con \"{subtype}\" arriba", + "remote_moved_any_side": "Dispositivo movido con cualquier lado hacia arriba", + "remote_rotate_from_side_1": "Dispositivo girado de \"lado 1\" a \"{subtype}\"", + "remote_rotate_from_side_2": "Dispositivo girado del \"lado 2\" al \"{subtype}\"", + "remote_rotate_from_side_3": "Dispositivo girado del \"lado 3\" al \"{subtype}\"", + "remote_rotate_from_side_4": "Dispositivo girado del \"lado 4\" al \"{subtype}\"", + "remote_rotate_from_side_5": "Dispositivo girado del \"lado 5\" al \"{subtype}\"", + "remote_rotate_from_side_6": "Dispositivo girado de \"lado 6\" a \"{subtype}\"", + "remote_turned_clockwise": "Dispositivo girado en sentido de las agujas del reloj", + "remote_turned_counter_clockwise": "Dispositivo girado en sentido contrario a las agujas del reloj" + } + }, + "options": { + "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", + "title": "Opciones de deCONZ" + } } } } \ No newline at end of file diff --git a/homeassistant/components/deconz/translations/es.json b/homeassistant/components/deconz/translations/es.json index 5ef7c0cc5d985f..3299ecbdc55d0e 100644 --- a/homeassistant/components/deconz/translations/es.json +++ b/homeassistant/components/deconz/translations/es.json @@ -1,10 +1,10 @@ { "config": { "abort": { - "already_configured": "El puente ya esta configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", - "no_bridges": "No se han descubierto puentes deCONZ", - "not_deconz_bridge": "No es un puente deCONZ", + "already_configured": "La pasarela ya est\u00e1 configurada", + "already_in_progress": "La configuraci\u00f3n del flujo para la pasarela ya est\u00e1 en curso.", + "no_bridges": "No se han descubierto pasarelas deCONZ", + "not_deconz_bridge": "No es una pasarela deCONZ", "one_instance_only": "El componente solo admite una instancia de deCONZ", "updated_instance": "Instancia deCONZ actualizada con nueva direcci\u00f3n de host" }, diff --git a/homeassistant/components/deconz/translations/no.json b/homeassistant/components/deconz/translations/no.json index e1587a07957e4e..5cdfc70959d9e4 100644 --- a/homeassistant/components/deconz/translations/no.json +++ b/homeassistant/components/deconz/translations/no.json @@ -11,7 +11,7 @@ "error": { "no_key": "Kunne ikke f\u00e5 en API-n\u00f8kkel" }, - "flow_title": "", + "flow_title": "deCONZ Zigbee gateway ({host})", "step": { "hassio_confirm": { "description": "Vil du konfigurere Home Assistant til \u00e5 koble seg til deCONZ-gateway levert av Hass.io-tillegget {addon} ?", diff --git a/homeassistant/components/demo/fan.py b/homeassistant/components/demo/fan.py index 966ba51cacb795..4b75e81715335d 100644 --- a/homeassistant/components/demo/fan.py +++ b/homeassistant/components/demo/fan.py @@ -37,12 +37,12 @@ def __init__(self, hass, name: str, supported_features: int) -> None: self.hass = hass self._supported_features = supported_features self._speed = STATE_OFF - self.oscillating = None + self._oscillating = None self._direction = None self._name = name if supported_features & SUPPORT_OSCILLATE: - self.oscillating = False + self._oscillating = False if supported_features & SUPPORT_DIRECTION: self._direction = "forward" @@ -89,7 +89,7 @@ def set_direction(self, direction: str) -> None: def oscillate(self, oscillating: bool) -> None: """Set oscillation.""" - self.oscillating = oscillating + self._oscillating = oscillating self.schedule_update_ha_state() @property @@ -97,6 +97,11 @@ def current_direction(self) -> str: """Fan direction.""" return self._direction + @property + def oscillating(self) -> bool: + """Oscillating.""" + return self._oscillating + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/demo/translations/es-419.json b/homeassistant/components/demo/translations/es-419.json index a9abb4aacd9cfc..8057621520ae94 100644 --- a/homeassistant/components/demo/translations/es-419.json +++ b/homeassistant/components/demo/translations/es-419.json @@ -1,3 +1,20 @@ { + "options": { + "step": { + "options_1": { + "data": { + "bool": "Booleano opcional", + "int": "Entrada num\u00e9rica" + } + }, + "options_2": { + "data": { + "multi": "Selecci\u00f3n m\u00faltiple", + "select": "Seleccione una opci\u00f3n", + "string": "Valor de cadena" + } + } + } + }, "title": "Demo" } \ No newline at end of file diff --git a/homeassistant/components/directv/translations/es-419.json b/homeassistant/components/directv/translations/es-419.json new file mode 100644 index 00000000000000..6db50cd6b5a69d --- /dev/null +++ b/homeassistant/components/directv/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El receptor de DirecTV ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente" + }, + "flow_title": "DirecTV: {name}", + "step": { + "ssdp_confirm": { + "description": "\u00bfDesea configurar {name}?", + "title": "Conectarse al receptor DirecTV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "title": "Conectarse al receptor DirecTV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/discovery/__init__.py b/homeassistant/components/discovery/__init__.py index 227995db971c8f..87c0e41533b125 100644 --- a/homeassistant/components/discovery/__init__.py +++ b/homeassistant/components/discovery/__init__.py @@ -32,7 +32,6 @@ SERVICE_HASS_IOS_APP = "hass_ios" SERVICE_HASSIO = "hassio" SERVICE_HEOS = "heos" -SERVICE_IGD = "igd" SERVICE_KONNECTED = "konnected" SERVICE_MOBILE_APP = "hass_mobile_app" SERVICE_NETGEAR = "netgear_router" @@ -48,7 +47,6 @@ CONFIG_ENTRY_HANDLERS = { SERVICE_DAIKIN: "daikin", SERVICE_TELLDUSLIVE: "tellduslive", - SERVICE_IGD: "upnp", } SERVICE_HANDLERS = { diff --git a/homeassistant/components/doorbird/strings.json b/homeassistant/components/doorbird/strings.json index e27083d2e0933f..2fc7cf5beef79d 100644 --- a/homeassistant/components/doorbird/strings.json +++ b/homeassistant/components/doorbird/strings.json @@ -12,10 +12,10 @@ "user": { "title": "Connect to the DoorBird", "data": { - "password": "Password", + "password": "[%key:common::config_flow::data::password%]", "host": "Host (IP Address)", "name": "Device Name", - "username": "Username" + "username": "[%key:common::config_flow::data::username%]" } } }, @@ -26,9 +26,9 @@ }, "flow_title": "DoorBird {name} ({host})", "error": { - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", - "cannot_connect": "Failed to connect, please try again" + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]", + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]" } } } diff --git a/homeassistant/components/doorbird/translations/es-419.json b/homeassistant/components/doorbird/translations/es-419.json index 1a412b38246e21..5ac195e007f90c 100644 --- a/homeassistant/components/doorbird/translations/es-419.json +++ b/homeassistant/components/doorbird/translations/es-419.json @@ -1,19 +1,25 @@ { "config": { "abort": { + "already_configured": "Este DoorBird ya est\u00e1 configurado", + "link_local_address": "Las direcciones locales de enlace no son compatibles", "not_doorbird_device": "Este dispositivo no es un DoorBird" }, "error": { "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", "unknown": "Error inesperado" }, + "flow_title": "DoorBird {name} ({host})", "step": { "user": { "data": { + "host": "Host (direcci\u00f3n IP)", "name": "Nombre del dispositivo", "password": "Contrase\u00f1a", "username": "Nombre de usuario" - } + }, + "title": "Conectar con DoorBird" } } }, diff --git a/homeassistant/components/dynalite/convert_config.py b/homeassistant/components/dynalite/convert_config.py index 2e21d5edd9b4ff..3cc9372eb0bfe2 100644 --- a/homeassistant/components/dynalite/convert_config.py +++ b/homeassistant/components/dynalite/convert_config.py @@ -32,47 +32,128 @@ CONF_TIME_COVER, ) -CONF_MAP = { - CONF_ACTIVE: dyn_const.CONF_ACTIVE, +ACTIVE_MAP = { ACTIVE_INIT: dyn_const.ACTIVE_INIT, + False: dyn_const.ACTIVE_OFF, ACTIVE_OFF: dyn_const.ACTIVE_OFF, ACTIVE_ON: dyn_const.ACTIVE_ON, - CONF_AREA: dyn_const.CONF_AREA, - CONF_AUTO_DISCOVER: dyn_const.CONF_AUTO_DISCOVER, - CONF_CHANNEL: dyn_const.CONF_CHANNEL, - CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, - CONF_TYPE: dyn_const.CONF_CHANNEL_TYPE, - CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, - CONF_DEFAULT: dyn_const.CONF_DEFAULT, - CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, - CONF_DURATION: dyn_const.CONF_DURATION, - CONF_FADE: dyn_const.CONF_FADE, - CONF_HOST: dyn_const.CONF_HOST, - CONF_NAME: dyn_const.CONF_NAME, - CONF_NO_DEFAULT: dyn_const.CONF_NO_DEFAULT, - CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, - CONF_POLL_TIMER: dyn_const.CONF_POLL_TIMER, - CONF_PORT: dyn_const.CONF_PORT, - CONF_PRESET: dyn_const.CONF_PRESET, + True: dyn_const.ACTIVE_ON, +} + +TEMPLATE_MAP = { CONF_ROOM: dyn_const.CONF_ROOM, - CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, - CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, - CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, - CONF_TEMPLATE: dyn_const.CONF_TEMPLATE, - CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, CONF_TIME_COVER: dyn_const.CONF_TIME_COVER, } +def convert_with_map(config, conf_map): + """Create the initial converted map with just the basic key:value pairs updated.""" + result = {} + for conf in conf_map: + if conf in config: + result[conf_map[conf]] = config[conf] + return result + + +def convert_channel(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for a channel.""" + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_FADE: dyn_const.CONF_FADE, + CONF_TYPE: dyn_const.CONF_CHANNEL_TYPE, + } + return convert_with_map(config, my_map) + + +def convert_preset(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for a preset.""" + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_FADE: dyn_const.CONF_FADE, + } + return convert_with_map(config, my_map) + + +def convert_area(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for an area.""" + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_FADE: dyn_const.CONF_FADE, + CONF_NO_DEFAULT: dyn_const.CONF_NO_DEFAULT, + CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, + CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, + CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, + CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, + CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, + CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, + CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, + CONF_DURATION: dyn_const.CONF_DURATION, + CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, + } + result = convert_with_map(config, my_map) + if CONF_CHANNEL in config: + result[dyn_const.CONF_CHANNEL] = { + channel: convert_channel(channel_conf) + for (channel, channel_conf) in config[CONF_CHANNEL].items() + } + if CONF_PRESET in config: + result[dyn_const.CONF_PRESET] = { + preset: convert_preset(preset_conf) + for (preset, preset_conf) in config[CONF_PRESET].items() + } + if CONF_TEMPLATE in config: + result[dyn_const.CONF_TEMPLATE] = TEMPLATE_MAP[config[CONF_TEMPLATE]] + return result + + +def convert_default(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for the platform defaults.""" + return convert_with_map(config, {CONF_FADE: dyn_const.CONF_FADE}) + + +def convert_template(config: Dict[str, Any]) -> Dict[str, Any]: + """Convert the config for a template.""" + my_map = { + CONF_ROOM_ON: dyn_const.CONF_ROOM_ON, + CONF_ROOM_OFF: dyn_const.CONF_ROOM_OFF, + CONF_CHANNEL_COVER: dyn_const.CONF_CHANNEL_COVER, + CONF_DEVICE_CLASS: dyn_const.CONF_DEVICE_CLASS, + CONF_OPEN_PRESET: dyn_const.CONF_OPEN_PRESET, + CONF_CLOSE_PRESET: dyn_const.CONF_CLOSE_PRESET, + CONF_STOP_PRESET: dyn_const.CONF_STOP_PRESET, + CONF_DURATION: dyn_const.CONF_DURATION, + CONF_TILT_TIME: dyn_const.CONF_TILT_TIME, + } + return convert_with_map(config, my_map) + + def convert_config(config: Dict[str, Any]) -> Dict[str, Any]: """Convert a config dict by replacing component consts with library consts.""" - result = {} - for (key, value) in config.items(): - if isinstance(value, dict): - new_value = convert_config(value) - elif isinstance(value, str): - new_value = CONF_MAP.get(value, value) - else: - new_value = value - result[CONF_MAP.get(key, key)] = new_value + my_map = { + CONF_NAME: dyn_const.CONF_NAME, + CONF_HOST: dyn_const.CONF_HOST, + CONF_PORT: dyn_const.CONF_PORT, + CONF_AUTO_DISCOVER: dyn_const.CONF_AUTO_DISCOVER, + CONF_POLL_TIMER: dyn_const.CONF_POLL_TIMER, + } + result = convert_with_map(config, my_map) + if CONF_AREA in config: + result[dyn_const.CONF_AREA] = { + area: convert_area(area_conf) + for (area, area_conf) in config[CONF_AREA].items() + } + if CONF_DEFAULT in config: + result[dyn_const.CONF_DEFAULT] = convert_default(config[CONF_DEFAULT]) + if CONF_ACTIVE in config: + result[dyn_const.CONF_ACTIVE] = ACTIVE_MAP[config[CONF_ACTIVE]] + if CONF_PRESET in config: + result[dyn_const.CONF_PRESET] = { + preset: convert_preset(preset_conf) + for (preset, preset_conf) in config[CONF_PRESET].items() + } + if CONF_TEMPLATE in config: + result[dyn_const.CONF_TEMPLATE] = { + TEMPLATE_MAP[template]: convert_template(template_conf) + for (template, template_conf) in config[CONF_TEMPLATE].items() + } return result diff --git a/homeassistant/components/dynalite/cover.py b/homeassistant/components/dynalite/cover.py index a5c25945aa81d2..e44fd150f384f8 100644 --- a/homeassistant/components/dynalite/cover.py +++ b/homeassistant/components/dynalite/cover.py @@ -5,7 +5,6 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from .const import DEFAULT_COVER_CLASS from .dynalitebase import DynaliteBase, async_setup_entry_base @@ -32,9 +31,8 @@ class DynaliteCover(DynaliteBase, CoverEntity): def device_class(self) -> str: """Return the class of the device.""" dev_cls = self._device.device_class - if dev_cls in DEVICE_CLASSES: - return dev_cls - return DEFAULT_COVER_CLASS + assert dev_cls in DEVICE_CLASSES + return dev_cls @property def current_cover_position(self) -> int: diff --git a/homeassistant/components/ecobee/translations/es-419.json b/homeassistant/components/ecobee/translations/es-419.json index ff9c1f53decaaa..50eab590a1c591 100644 --- a/homeassistant/components/ecobee/translations/es-419.json +++ b/homeassistant/components/ecobee/translations/es-419.json @@ -9,12 +9,15 @@ }, "step": { "authorize": { - "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar." + "description": "Autorice esta aplicaci\u00f3n en https://www.ecobee.com/consumerportal/index.html con c\u00f3digo PIN: \n\n {pin} \n \n Luego, presione Enviar.", + "title": "Autorizar aplicaci\u00f3n en ecobee.com" }, "user": { "data": { "api_key": "Clave API" - } + }, + "description": "Ingrese la clave API obtenida de ecobee.com.", + "title": "Clave API ecobee" } } } diff --git a/homeassistant/components/edl21/sensor.py b/homeassistant/components/edl21/sensor.py index a4e0ca734fabcd..6a9330cae1c2b5 100644 --- a/homeassistant/components/edl21/sensor.py +++ b/homeassistant/components/edl21/sensor.py @@ -8,6 +8,7 @@ import voluptuous as vol from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import ( @@ -15,6 +16,7 @@ async_dispatcher_send, ) from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.helpers.typing import Optional from homeassistant.util.dt import utcnow @@ -26,7 +28,12 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=60) SIGNAL_EDL21_TELEGRAM = "edl21_telegram" -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({vol.Required(CONF_SERIAL_PORT): cv.string}) +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( + { + vol.Required(CONF_SERIAL_PORT): cv.string, + vol.Optional(CONF_NAME, default=""): cv.string, + }, +) async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -74,6 +81,7 @@ def __init__(self, hass, config, async_add_entities) -> None: self._registered_obis = set() self._hass = hass self._async_add_entities = async_add_entities + self._name = config[CONF_NAME] self._proto = SmlProtocol(config[CONF_SERIAL_PORT]) self._proto.add_listener(self.event, ["SmlGetListResponse"]) @@ -85,19 +93,35 @@ def event(self, message_body) -> None: """Handle events from pysml.""" assert isinstance(message_body, SmlGetListResponse) + electricity_id = None + for telegram in message_body.get("valList", []): + if telegram.get("objName") == "1-0:0.0.9*255": + electricity_id = telegram.get("value") + break + + if electricity_id is None: + return + electricity_id = electricity_id.replace(" ", "") + new_entities = [] for telegram in message_body.get("valList", []): obis = telegram.get("objName") if not obis: continue - if obis in self._registered_obis: - async_dispatcher_send(self._hass, SIGNAL_EDL21_TELEGRAM, telegram) + if (electricity_id, obis) in self._registered_obis: + async_dispatcher_send( + self._hass, SIGNAL_EDL21_TELEGRAM, electricity_id, telegram + ) else: name = self._OBIS_NAMES.get(obis) if name: - new_entities.append(EDL21Entity(obis, name, telegram)) - self._registered_obis.add(obis) + if self._name: + name = f"{self._name}: {name}" + new_entities.append( + EDL21Entity(electricity_id, obis, name, telegram) + ) + self._registered_obis.add((electricity_id, obis)) elif obis not in self._OBIS_BLACKLIST: _LOGGER.warning( "Unhandled sensor %s detected. Please report at " @@ -107,16 +131,41 @@ def event(self, message_body) -> None: self._OBIS_BLACKLIST.add(obis) if new_entities: - self._async_add_entities(new_entities, update_before_add=True) + self._hass.loop.create_task(self.add_entities(new_entities)) + + async def add_entities(self, new_entities) -> None: + """Migrate old unique IDs, then add entities to hass.""" + registry = await async_get_registry(self._hass) + + for entity in new_entities: + old_entity_id = registry.async_get_entity_id( + "sensor", DOMAIN, entity.old_unique_id + ) + if old_entity_id is not None: + _LOGGER.debug( + "Migrating unique_id from [%s] to [%s]", + entity.old_unique_id, + entity.unique_id, + ) + if registry.async_get_entity_id("sensor", DOMAIN, entity.unique_id): + registry.async_remove(old_entity_id) + else: + registry.async_update_entity( + old_entity_id, new_unique_id=entity.unique_id + ) + + self._async_add_entities(new_entities, update_before_add=True) class EDL21Entity(Entity): """Entity reading values from EDL21 telegram.""" - def __init__(self, obis, name, telegram): + def __init__(self, electricity_id, obis, name, telegram): """Initialize an EDL21Entity.""" + self._electricity_id = electricity_id self._obis = obis self._name = name + self._unique_id = f"{electricity_id}_{obis}" self._telegram = telegram self._min_time = MIN_TIME_BETWEEN_UPDATES self._last_update = utcnow() @@ -132,8 +181,10 @@ async def async_added_to_hass(self): """Run when entity about to be added to hass.""" @callback - def handle_telegram(telegram): + def handle_telegram(electricity_id, telegram): """Update attributes from last received telegram for this object.""" + if self._electricity_id != electricity_id: + return if self._obis != telegram.get("objName"): return if self._telegram == telegram: @@ -164,6 +215,11 @@ def should_poll(self) -> bool: @property def unique_id(self) -> str: """Return a unique ID.""" + return self._unique_id + + @property + def old_unique_id(self) -> str: + """Return a less unique ID as used in the first version of edl21.""" return self._obis @property diff --git a/homeassistant/components/elgato/translations/es-419.json b/homeassistant/components/elgato/translations/es-419.json index 46a008009b9fe7..9d12537851d3ff 100644 --- a/homeassistant/components/elgato/translations/es-419.json +++ b/homeassistant/components/elgato/translations/es-419.json @@ -1,14 +1,24 @@ { "config": { + "abort": { + "already_configured": "Este dispositivo Elgato Key Light ya est\u00e1 configurado.", + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "error": { + "connection_error": "No se pudo conectar al dispositivo Elgato Key Light." + }, + "flow_title": "Elgato Key Light: {serial_number}", "step": { "user": { "data": { "host": "Host o direcci\u00f3n IP", "port": "N\u00famero de puerto" }, - "description": "Configure su Elgato Key Light para integrarse con Home Assistant." + "description": "Configure su Elgato Key Light para integrarse con Home Assistant.", + "title": "Vincule su Elgato Key Light" }, "zeroconf_confirm": { + "description": "\u00bfDesea agregar el disposiivo Elgato Key Light con el n\u00famero de serie `{serial_number}` a Home Assistant?", "title": "Dispositivo Elgato Key Light descubierto" } } diff --git a/homeassistant/components/elgato/translations/no.json b/homeassistant/components/elgato/translations/no.json index 34a9bfed772c85..2d60155cbb8a97 100644 --- a/homeassistant/components/elgato/translations/no.json +++ b/homeassistant/components/elgato/translations/no.json @@ -7,7 +7,7 @@ "error": { "connection_error": "Kunne ikke koble til Elgato Key Light-enheten." }, - "flow_title": "", + "flow_title": "Elgato Key Light: {serial_number}", "step": { "user": { "data": { diff --git a/homeassistant/components/elkm1/translations/es-419.json b/homeassistant/components/elkm1/translations/es-419.json new file mode 100644 index 00000000000000..02271c4ea6c184 --- /dev/null +++ b/homeassistant/components/elkm1/translations/es-419.json @@ -0,0 +1,27 @@ +{ + "config": { + "abort": { + "address_already_configured": "Un ElkM1 con esta direcci\u00f3n ya est\u00e1 configurado", + "already_configured": "Un ElkM1 con este prefijo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "address": "La direcci\u00f3n IP, dominio o puerto serie si se conecta via serial.", + "password": "Contrase\u00f1a (solo segura).", + "prefix": "Un prefijo \u00fanico (d\u00e9jelo en blanco si solo tiene un ElkM1).", + "protocol": "Protocolo", + "temperature_unit": "La unidad de temperatura que utiliza ElkM1.", + "username": "Nombre de usuario (solo seguro)." + }, + "description": "La cadena de direcci\u00f3n debe tener 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 estar en la forma 'tty[:baudios]'. Ejemplo: '/dev/ttyS1'. La velocidad en baudios es opcional y su valor predeterminado es 115200.", + "title": "Con\u00e9ctese al control Elk-M1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/emulated_hue/__init__.py b/homeassistant/components/emulated_hue/__init__.py index da6e7acab404ef..5dbd52d09b1632 100644 --- a/homeassistant/components/emulated_hue/__init__.py +++ b/homeassistant/components/emulated_hue/__init__.py @@ -14,6 +14,7 @@ from .hue_api import ( HueAllGroupsStateView, HueAllLightsStateView, + HueConfigView, HueFullStateView, HueGroupView, HueOneLightChangeView, @@ -119,6 +120,7 @@ async def async_setup(hass, yaml_config): HueAllGroupsStateView(config).register(app, app.router) HueGroupView(config).register(app, app.router) HueFullStateView(config).register(app, app.router) + HueConfigView(config).register(app, app.router) upnp_listener = UPNPResponderThread( config.host_ip_addr, diff --git a/homeassistant/components/emulated_hue/hue_api.py b/homeassistant/components/emulated_hue/hue_api.py index 9637b0fb371822..069a4b60d0cf07 100644 --- a/homeassistant/components/emulated_hue/hue_api.py +++ b/homeassistant/components/emulated_hue/hue_api.py @@ -89,7 +89,7 @@ HUE_API_STATE_CT_MIN = 153 # Color temp HUE_API_STATE_CT_MAX = 500 -HUE_API_USERNAME = "12345678901234567890" +HUE_API_USERNAME = "nouser" UNAUTHORIZED_USER = [ {"error": {"address": "/", "description": "unauthorized user", "type": "1"}} ] @@ -226,14 +226,47 @@ def get(self, request, username): "config": { "mac": "00:00:00:00:00:00", "swversion": "01003542", + "apiversion": "1.17.0", "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + "linkbutton": True, }, } return self.json(json_response) +class HueConfigView(HomeAssistantView): + """Return config view of emulated hue.""" + + url = "/api/{username}/config" + name = "emulated_hue:username:config" + requires_auth = False + + def __init__(self, config): + """Initialize the instance of the view.""" + self.config = config + + @core.callback + def get(self, request, username): + """Process a request to get the configuration.""" + if not is_local(request[KEY_REAL_IP]): + return self.json_message("only local IPs allowed", HTTP_UNAUTHORIZED) + if username != HUE_API_USERNAME: + return self.json(UNAUTHORIZED_USER) + + json_response = { + "mac": "00:00:00:00:00:00", + "swversion": "01003542", + "apiversion": "1.17.0", + "whitelist": {HUE_API_USERNAME: {"name": "HASS BRIDGE"}}, + "ipaddress": f"{self.config.advertise_ip}:{self.config.advertise_port}", + "linkbutton": True, + } + + return self.json(json_response) + + class HueOneLightStateView(HomeAssistantView): """Handle requests for getting info about a single entity.""" @@ -506,7 +539,9 @@ async def put(self, request, username, entity_number): # Create success responses for all received keys json_response = [ - create_hue_success_response(entity_id, HUE_API_STATE_ON, parsed[STATE_ON]) + create_hue_success_response( + entity_number, HUE_API_STATE_ON, parsed[STATE_ON] + ) ] for (key, val) in ( @@ -517,7 +552,7 @@ async def put(self, request, username, entity_number): ): if parsed[key] is not None: json_response.append( - create_hue_success_response(entity_id, val, parsed[key]) + create_hue_success_response(entity_number, val, parsed[key]) ) return self.json(json_response) @@ -710,9 +745,9 @@ def entity_to_json(config, entity): return retval -def create_hue_success_response(entity_id, attr, value): +def create_hue_success_response(entity_number, attr, value): """Create a success response for an attribute set on a light.""" - success_key = f"/lights/{entity_id}/state/{attr}" + success_key = f"/lights/{entity_number}/state/{attr}" return {"success": {success_key: value}} diff --git a/homeassistant/components/emulated_hue/upnp.py b/homeassistant/components/emulated_hue/upnp.py index c10fb3b826b9bd..f0fe392f865d69 100644 --- a/homeassistant/components/emulated_hue/upnp.py +++ b/homeassistant/components/emulated_hue/upnp.py @@ -42,7 +42,7 @@ def get(self, request): Philips hue bridge 2015 BSB002 http://www.meethue.com -1234 +001788FFFE23BFC2 uuid:2f402f80-da50-11e1-9b23-001788255acc @@ -77,10 +77,10 @@ def __init__( CACHE-CONTROL: max-age=60 EXT: LOCATION: http://{advertise_ip}:{advertise_port}/description.xml -SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/0.1 -hue-bridgeid: 1234 -ST: urn:schemas-upnp-org:device:basic:1 -USN: uuid:Socket-1_0-221438K0100073::urn:schemas-upnp-org:device:basic:1 +SERVER: FreeRTOS/6.0.5, UPnP/1.0, IpBridge/1.16.0 +hue-bridgeid: 001788FFFE23BFC2 +ST: upnp:rootdevice +USN: uuid:2f402f80-da50-11e1-9b23-00178829d301::upnp:rootdevice """ diff --git a/homeassistant/components/emulated_roku/translations/es-419.json b/homeassistant/components/emulated_roku/translations/es-419.json index 85d75c81ff36bb..9aa7ff0bc9200c 100644 --- a/homeassistant/components/emulated_roku/translations/es-419.json +++ b/homeassistant/components/emulated_roku/translations/es-419.json @@ -7,7 +7,9 @@ "user": { "data": { "host_ip": "IP del host", - "name": "Nombre" + "listen_port": "Puerto de escucha", + "name": "Nombre", + "upnp_bind_multicast": "Enlazar multidifusi\u00f3n (verdadero/falso)" }, "title": "Definir la configuraci\u00f3n del servidor." } diff --git a/homeassistant/components/esphome/translations/de.json b/homeassistant/components/esphome/translations/de.json index 64262aa654a57a..299660cb3480d0 100644 --- a/homeassistant/components/esphome/translations/de.json +++ b/homeassistant/components/esphome/translations/de.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ist bereits konfiguriert" + "already_configured": "ESP ist bereits konfiguriert", + "already_in_progress": "Die ESP-Konfiguration wird bereits ausgef\u00fchrt" }, "error": { "connection_error": "Keine Verbindung zum ESP m\u00f6glich. Achte darauf, dass deine YAML-Datei eine Zeile 'api:' enth\u00e4lt.", diff --git a/homeassistant/components/esphome/translations/es-419.json b/homeassistant/components/esphome/translations/es-419.json index 7bbe61aceb2def..2774ff7ea68816 100644 --- a/homeassistant/components/esphome/translations/es-419.json +++ b/homeassistant/components/esphome/translations/es-419.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP ya est\u00e1 configurado" + "already_configured": "ESP ya est\u00e1 configurado", + "already_in_progress": "La configuraci\u00f3n de ESP ya est\u00e1 en progreso" }, "error": { "connection_error": "No se puede conectar a ESP. Aseg\u00farese de que su archivo YAML contenga una l\u00ednea 'api:'.", diff --git a/homeassistant/components/esphome/translations/it.json b/homeassistant/components/esphome/translations/it.json index 050c122249553a..60ba2a31890d0e 100644 --- a/homeassistant/components/esphome/translations/it.json +++ b/homeassistant/components/esphome/translations/it.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "ESP \u00e8 gi\u00e0 configurato" + "already_configured": "ESP \u00e8 gi\u00e0 configurato", + "already_in_progress": "La configurazione ESP \u00e8 gi\u00e0 in corso" }, "error": { "connection_error": "Impossibile connettersi ad ESP. Assicurati che il tuo file YAML contenga una riga \"api:\".", diff --git a/homeassistant/components/esphome/translations/no.json b/homeassistant/components/esphome/translations/no.json index 37e3b881b8b4e3..c04f0c2a09d414 100644 --- a/homeassistant/components/esphome/translations/no.json +++ b/homeassistant/components/esphome/translations/no.json @@ -8,7 +8,7 @@ "invalid_password": "Ugyldig passord!", "resolve_error": "Kan ikke l\u00f8se adressen til ESP. Hvis denne feilen vedvarer, m\u00e5 du [angi en statisk IP-adresse](https://esphomelib.com/esphomeyaml/components/wifi.html#manual-ips)" }, - "flow_title": "", + "flow_title": "ESPHome: {name}", "step": { "authenticate": { "data": { diff --git a/homeassistant/components/fan/__init__.py b/homeassistant/components/fan/__init__.py index a395a5da47004d..4531656f7afcbd 100644 --- a/homeassistant/components/fan/__init__.py +++ b/homeassistant/components/fan/__init__.py @@ -45,12 +45,6 @@ ATTR_OSCILLATING = "oscillating" ATTR_DIRECTION = "direction" -PROP_TO_ATTR = { - "speed": ATTR_SPEED, - "oscillating": ATTR_OSCILLATING, - "current_direction": ATTR_DIRECTION, -} - @bind_hass def is_on(hass, entity_id: str) -> bool: @@ -166,23 +160,32 @@ def current_direction(self) -> Optional[str]: """Return the current direction of the fan.""" return None + @property + def oscillating(self): + """Return whether or not the fan is currently oscillating.""" + return None + @property def capability_attributes(self): """Return capability attributes.""" - return {ATTR_SPEED_LIST: self.speed_list} + if self.supported_features & SUPPORT_SET_SPEED: + return {ATTR_SPEED_LIST: self.speed_list} + return {} @property def state_attributes(self) -> dict: """Return optional state attributes.""" data = {} + supported_features = self.supported_features + + if supported_features & SUPPORT_DIRECTION: + data[ATTR_DIRECTION] = self.current_direction - for prop, attr in PROP_TO_ATTR.items(): - if not hasattr(self, prop): - continue + if supported_features & SUPPORT_OSCILLATE: + data[ATTR_OSCILLATING] = self.oscillating - value = getattr(self, prop) - if value is not None: - data[attr] = value + if supported_features & SUPPORT_SET_SPEED: + data[ATTR_SPEED] = self.speed return data diff --git a/homeassistant/components/fan/translations/es-419.json b/homeassistant/components/fan/translations/es-419.json index 6060cff985a74e..8e9611bdff9953 100644 --- a/homeassistant/components/fan/translations/es-419.json +++ b/homeassistant/components/fan/translations/es-419.json @@ -1,5 +1,9 @@ { "device_automation": { + "action_type": { + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagado", "is_on": "{entity_name} est\u00e1 encendido" diff --git a/homeassistant/components/flume/translations/es-419.json b/homeassistant/components/flume/translations/es-419.json new file mode 100644 index 00000000000000..026875846c6977 --- /dev/null +++ b/homeassistant/components/flume/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "Esta cuenta ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "client_id": "Identificaci\u00f3n del cliente", + "client_secret": "Secreto del cliente", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Para acceder a la API personal de Flume, deber\u00e1 solicitar un 'ID de cliente' y un 'Secreto de cliente' en https://portal.flumetech.com/settings#token", + "title": "Con\u00e9ctese a su cuenta Flume" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/flunearyou/translations/es-419.json b/homeassistant/components/flunearyou/translations/es-419.json new file mode 100644 index 00000000000000..9626a509bca206 --- /dev/null +++ b/homeassistant/components/flunearyou/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "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": "Monitoree los repotes basados en el usuario y los CDC para un par de coordenadas.", + "title": "Configurar Flu Near You (Gripe cerca de usted)" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fortigate/device_tracker.py b/homeassistant/components/fortigate/device_tracker.py index b51dc6843aaf13..23df0ee266e008 100644 --- a/homeassistant/components/fortigate/device_tracker.py +++ b/homeassistant/components/fortigate/device_tracker.py @@ -60,7 +60,7 @@ async def async_scan_devices(self): await self.async_update_info() return [device.mac for device in self.last_results] - async def get_device_name(self, device): + def get_device_name(self, device): """Return the name of the given device or None if we don't know.""" name = next( (result.hostname for result in self.last_results if result.mac == device), diff --git a/homeassistant/components/freebox/translations/es-419.json b/homeassistant/components/freebox/translations/es-419.json new file mode 100644 index 00000000000000..015835514536d3 --- /dev/null +++ b/homeassistant/components/freebox/translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "Host ya configurado" + }, + "error": { + "connection_failed": "No se pudo conectar, intente nuevamente", + "register_failed": "No se pudo registrar, intente de nuevo", + "unknown": "Error desconocido: vuelva a intentarlo m\u00e1s tarde" + }, + "step": { + "link": { + "description": "Haga clic en \"Enviar\", luego toque la flecha derecha en el enrutador para registrar Freebox con Home Assistant. \n\n![Location of button on the router](/static/images/config_freebox.png)", + "title": "Enlazar enrutador Freebox" + }, + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Freebox" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/es-419.json b/homeassistant/components/fritzbox/translations/es-419.json new file mode 100644 index 00000000000000..4e8003a06d8597 --- /dev/null +++ b/homeassistant/components/fritzbox/translations/es-419.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "Este AVM FRITZ!Box ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n de AVM FRITZ!Box ya est\u00e1 en progreso.", + "not_found": "No se encontr\u00f3 ning\u00fan AVM FRITZ!Box compatible en la red.", + "not_supported": "Conectado a AVM FRITZ!Box pero no puede controlar dispositivos Smart Home." + }, + "error": { + "auth_failed": "El nombre de usuario y/o la contrase\u00f1a son incorrectos." + }, + "flow_title": "AVM FRITZ!Box: {name}", + "step": { + "confirm": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "\u00bfDesea configurar {name}?", + "title": "AVM FRITZ!Box" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Ingrese la informaci\u00f3n de su AVM FRITZ!Box.", + "title": "AVM FRITZ!Box" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/fritzbox/translations/fr.json b/homeassistant/components/fritzbox/translations/fr.json index 5072d62374bdb4..9a17f540ed1042 100644 --- a/homeassistant/components/fritzbox/translations/fr.json +++ b/homeassistant/components/fritzbox/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "not_supported": "Connect\u00e9 \u00e0 AVM FRITZ! Box mais impossible de contr\u00f4ler les appareils Smart Home." + }, "error": { "auth_failed": "Le nom d'utilisateur et / ou le mot de passe sont incorrects." }, diff --git a/homeassistant/components/fritzbox/translations/no.json b/homeassistant/components/fritzbox/translations/no.json index 85027c50af946f..284782755bd881 100644 --- a/homeassistant/components/fritzbox/translations/no.json +++ b/homeassistant/components/fritzbox/translations/no.json @@ -9,7 +9,7 @@ "error": { "auth_failed": "Brukernavn og/eller passord er feil." }, - "flow_title": "", + "flow_title": "AVM FRITZ!Box: {name}", "step": { "confirm": { "data": { diff --git a/homeassistant/components/frontend/manifest.json b/homeassistant/components/frontend/manifest.json index 9759c38af7db34..c4cff5ca28bbc9 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==20200427.1"], + "requirements": ["home-assistant-frontend==20200505.0"], "dependencies": [ "api", "auth", diff --git a/homeassistant/components/garmin_connect/strings.json b/homeassistant/components/garmin_connect/strings.json index 1f14d91e04aef0..a68a505613aa65 100644 --- a/homeassistant/components/garmin_connect/strings.json +++ b/homeassistant/components/garmin_connect/strings.json @@ -1,6 +1,8 @@ { "config": { - "abort": { "already_configured": "This account is already configured." }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + }, "error": { "cannot_connect": "Failed to connect, please try again.", "invalid_auth": "Invalid authentication.", diff --git a/homeassistant/components/garmin_connect/translations/es-419.json b/homeassistant/components/garmin_connect/translations/es-419.json index 6e20b4cd2cc18a..42263ce0780ad1 100644 --- a/homeassistant/components/garmin_connect/translations/es-419.json +++ b/homeassistant/components/garmin_connect/translations/es-419.json @@ -15,7 +15,8 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, - "description": "Ingrese sus credenciales." + "description": "Ingrese sus credenciales.", + "title": "Garmin Connect" } } } diff --git a/homeassistant/components/gdacs/translations/es-419.json b/homeassistant/components/gdacs/translations/es-419.json new file mode 100644 index 00000000000000..6b6999a196e246 --- /dev/null +++ b/homeassistant/components/gdacs/translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geofency/translations/no.json b/homeassistant/components/geofency/translations/no.json index ea9e1827b63736..8e66cab4c9c8b5 100644 --- a/homeassistant/components/geofency/translations/no.json +++ b/homeassistant/components/geofency/translations/no.json @@ -9,7 +9,7 @@ }, "step": { "user": { - "description": "Er du sikker p\u00e5 at du vil konfigurere Geofency Webhook?", + "description": "Er du sikker p\u00e5 at du vil sette opp Geofency Webhook?", "title": "Sett opp Geofency Webhook" } } diff --git a/homeassistant/components/geonetnz_quakes/translations/es-419.json b/homeassistant/components/geonetnz_quakes/translations/es-419.json new file mode 100644 index 00000000000000..7afffd7bb97bf3 --- /dev/null +++ b/homeassistant/components/geonetnz_quakes/translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "already_configured": "La ubicaci\u00f3n ya est\u00e1 configurada." + }, + "step": { + "user": { + "data": { + "mmi": "MMI", + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/geonetnz_volcano/translations/es-419.json b/homeassistant/components/geonetnz_volcano/translations/es-419.json new file mode 100644 index 00000000000000..c26033e18619cb --- /dev/null +++ b/homeassistant/components/geonetnz_volcano/translations/es-419.json @@ -0,0 +1,15 @@ +{ + "config": { + "error": { + "identifier_exists": "Lugar ya registrado" + }, + "step": { + "user": { + "data": { + "radius": "Radio" + }, + "title": "Complete los detalles de su filtro." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/gios/translations/es-419.json b/homeassistant/components/gios/translations/es-419.json index 53439a7ab7be40..848247bdf75d24 100644 --- a/homeassistant/components/gios/translations/es-419.json +++ b/homeassistant/components/gios/translations/es-419.json @@ -1,10 +1,21 @@ { "config": { + "abort": { + "already_configured": "La integraci\u00f3n de GIO\u015a para esta estaci\u00f3n de medici\u00f3n ya est\u00e1 configurada." + }, + "error": { + "cannot_connect": "No se puede conectar al servidor GIO\u015a.", + "invalid_sensors_data": "Datos de sensores no v\u00e1lidos para esta estaci\u00f3n de medici\u00f3n.", + "wrong_station_id": "La identificaci\u00f3n de la estaci\u00f3n de medici\u00f3n no es correcta." + }, "step": { "user": { "data": { - "name": "Nombre de la integraci\u00f3n" - } + "name": "Nombre de la integraci\u00f3n", + "station_id": "Identificaci\u00f3n de la estaci\u00f3n de medici\u00f3n" + }, + "description": "Establecer la integraci\u00f3n de la calidad del aire GIO\u015a (Inspecci\u00f3n Jefe de Protecci\u00f3n Ambiental de Polonia). Si necesita ayuda con la configuraci\u00f3n, eche un vistazo aqu\u00ed: https://www.home-assistant.io/integrations/gios", + "title": "GIO\u015a (Polish Chief Inspectorate Of Environmental Protection)" } } } diff --git a/homeassistant/components/glances/translations/es-419.json b/homeassistant/components/glances/translations/es-419.json index 6debc6da6c1677..5e060b20d47183 100644 --- a/homeassistant/components/glances/translations/es-419.json +++ b/homeassistant/components/glances/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, "error": { "cannot_connect": "No se puede conectar al host", "wrong_version": "Versi\u00f3n no compatible (2 o 3 solamente)" @@ -7,13 +10,16 @@ "step": { "user": { "data": { + "host": "Host", "name": "Nombre", "password": "Contrase\u00f1a", "port": "Puerto", + "ssl": "Use SSL/TLS para conectarse al sistema Glances", "username": "Nombre de usuario", "verify_ssl": "Verificar la certificaci\u00f3n del sistema", "version": "Versi\u00f3n de API de Glances (2 o 3)" - } + }, + "title": "Configurar Glances" } } }, diff --git a/homeassistant/components/google_assistant/const.py b/homeassistant/components/google_assistant/const.py index 37fef6f2d794af..9a133b5e6b7d39 100644 --- a/homeassistant/components/google_assistant/const.py +++ b/homeassistant/components/google_assistant/const.py @@ -124,6 +124,7 @@ DEVICE_CLASS_TO_GOOGLE_TYPES = { (cover.DOMAIN, cover.DEVICE_CLASS_GARAGE): TYPE_GARAGE, + (cover.DOMAIN, cover.DEVICE_CLASS_GATE): TYPE_GARAGE, (cover.DOMAIN, cover.DEVICE_CLASS_DOOR): TYPE_DOOR, (switch.DOMAIN, switch.DEVICE_CLASS_SWITCH): TYPE_SWITCH, (switch.DOMAIN, switch.DEVICE_CLASS_OUTLET): TYPE_OUTLET, diff --git a/homeassistant/components/griddy/translations/es-419.json b/homeassistant/components/griddy/translations/es-419.json new file mode 100644 index 00000000000000..652c8484b4e727 --- /dev/null +++ b/homeassistant/components/griddy/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "Esta zona de carga ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "loadzone": "Zona de carga (punto de asentamiento)" + }, + "description": "Su zona de carga est\u00e1 en su cuenta de Griddy en \"Cuenta > Medidor > Zona de carga\".", + "title": "Configura tu zona de carga Griddy" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hangouts/translations/es-419.json b/homeassistant/components/hangouts/translations/es-419.json index 9ff97592d91660..a8ae41ec21e267 100644 --- a/homeassistant/components/hangouts/translations/es-419.json +++ b/homeassistant/components/hangouts/translations/es-419.json @@ -6,6 +6,7 @@ }, "error": { "invalid_2fa": "Autenticaci\u00f3n de 2 factores no v\u00e1lida, intente nuevamente.", + "invalid_2fa_method": "M\u00e9todo 2FA no v\u00e1lido (verificar en el tel\u00e9fono).", "invalid_login": "Inicio de sesi\u00f3n no v\u00e1lido, por favor, int\u00e9ntalo de nuevo." }, "step": { diff --git a/homeassistant/components/harmony/translations/es-419.json b/homeassistant/components/harmony/translations/es-419.json new file mode 100644 index 00000000000000..83781be522eb1c --- /dev/null +++ b/homeassistant/components/harmony/translations/es-419.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "flow_title": "Logitech Harmony Hub {name}", + "step": { + "link": { + "description": "\u00bfDesea configurar {name} ({host})?", + "title": "Configurar Logitech Harmony Hub" + }, + "user": { + "data": { + "host": "Nombre de host o direcci\u00f3n IP", + "name": "Nombre del concentrador" + }, + "title": "Configurar Logitech Harmony Hub" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "activity": "La actividad predeterminada para ejecutar cuando no se especifica ninguno.", + "delay_secs": "El retraso entre el env\u00edo de comandos." + }, + "description": "Ajuste las opciones de Harmony Hub" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/harmony/translations/no.json b/homeassistant/components/harmony/translations/no.json index 871b3161fcf832..9cae066320845b 100644 --- a/homeassistant/components/harmony/translations/no.json +++ b/homeassistant/components/harmony/translations/no.json @@ -7,7 +7,7 @@ "cannot_connect": "Klarte ikke \u00e5 koble til, vennligst pr\u00f8v igjen", "unknown": "Uventet feil" }, - "flow_title": "", + "flow_title": "Logitech Harmony Hub {name}", "step": { "link": { "description": "Vil du konfigurere {name} ({host})?", diff --git a/homeassistant/components/heos/translations/es-419.json b/homeassistant/components/heos/translations/es-419.json index 01338dc5af3f84..902f65bf5e1c86 100644 --- a/homeassistant/components/heos/translations/es-419.json +++ b/homeassistant/components/heos/translations/es-419.json @@ -8,6 +8,10 @@ }, "step": { "user": { + "data": { + "access_token": "Host", + "host": "Host" + }, "description": "Ingrese el nombre de host o la direcci\u00f3n IP de un dispositivo Heos (preferiblemente uno conectado por cable a la red).", "title": "Con\u00e9ctate a Heos" } diff --git a/homeassistant/components/hisense_aehw4a1/translations/es-419.json b/homeassistant/components/hisense_aehw4a1/translations/es-419.json new file mode 100644 index 00000000000000..c9c4270360ae5b --- /dev/null +++ b/homeassistant/components/hisense_aehw4a1/translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos Hisense AEH-W4A1 en la red.", + "single_instance_allowed": "Solo es posible una \u00fanica configuraci\u00f3n de Hisense AEH-W4A1." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar Hisense AEH-W4A1?", + "title": "Hisense AEH-W4A1" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hisense_aehw4a1/translations/no.json b/homeassistant/components/hisense_aehw4a1/translations/no.json index 0b0bf55d7afce8..bc048ef22868d9 100644 --- a/homeassistant/components/hisense_aehw4a1/translations/no.json +++ b/homeassistant/components/hisense_aehw4a1/translations/no.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere Hisense AEH-W4A1?", + "description": "Vil du sette opp Hisense AEH-W4A1?", "title": "" } } diff --git a/homeassistant/components/home_connect/__init__.py b/homeassistant/components/home_connect/__init__.py new file mode 100644 index 00000000000000..4e5759635773ea --- /dev/null +++ b/homeassistant/components/home_connect/__init__.py @@ -0,0 +1,106 @@ +"""Support for BSH Home Connect appliances.""" + +import asyncio +from datetime import timedelta +import logging + +from requests import HTTPError +import voluptuous as vol + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET +from homeassistant.core import HomeAssistant +from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv +from homeassistant.util import Throttle + +from . import api, config_flow +from .const import DOMAIN, OAUTH2_AUTHORIZE, OAUTH2_TOKEN + +_LOGGER = logging.getLogger(__name__) + +SCAN_INTERVAL = timedelta(minutes=1) + +CONFIG_SCHEMA = vol.Schema( + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_CLIENT_ID): cv.string, + vol.Required(CONF_CLIENT_SECRET): cv.string, + } + ) + }, + extra=vol.ALLOW_EXTRA, +) + +PLATFORMS = ["binary_sensor", "sensor", "switch"] + + +async def async_setup(hass: HomeAssistant, config: dict) -> bool: + """Set up Home Connect component.""" + hass.data[DOMAIN] = {} + + if DOMAIN not in config: + return True + + config_flow.OAuth2FlowHandler.async_register_implementation( + hass, + config_entry_oauth2_flow.LocalOAuth2Implementation( + hass, + DOMAIN, + config[DOMAIN][CONF_CLIENT_ID], + config[DOMAIN][CONF_CLIENT_SECRET], + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, + ), + ) + + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Home Connect from a config entry.""" + implementation = await config_entry_oauth2_flow.async_get_config_entry_implementation( + hass, entry + ) + + hc_api = api.ConfigEntryAuth(hass, entry, implementation) + + hass.data[DOMAIN][entry.entry_id] = hc_api + + await update_all_devices(hass, 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) -> 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 + + +@Throttle(SCAN_INTERVAL) +async def update_all_devices(hass, entry): + """Update all the devices.""" + data = hass.data[DOMAIN] + hc_api = data[entry.entry_id] + try: + await hass.async_add_executor_job(hc_api.get_devices) + for device_dict in hc_api.devices: + await hass.async_add_executor_job(device_dict["device"].initialize) + except HTTPError as err: + _LOGGER.warning("Cannot update devices: %s", err.response.status_code) diff --git a/homeassistant/components/home_connect/api.py b/homeassistant/components/home_connect/api.py new file mode 100644 index 00000000000000..a208f9c7f0f4d6 --- /dev/null +++ b/homeassistant/components/home_connect/api.py @@ -0,0 +1,372 @@ +"""API for Home Connect bound to HASS OAuth.""" + +from asyncio import run_coroutine_threadsafe +import logging + +import homeconnect +from homeconnect.api import HomeConnectError + +from homeassistant import config_entries, core +from homeassistant.const import DEVICE_CLASS_TIMESTAMP, TIME_SECONDS, UNIT_PERCENTAGE +from homeassistant.helpers import config_entry_oauth2_flow +from homeassistant.helpers.dispatcher import dispatcher_send + +from .const import ( + BSH_ACTIVE_PROGRAM, + BSH_POWER_OFF, + BSH_POWER_STANDBY, + SIGNAL_UPDATE_ENTITIES, +) + +_LOGGER = logging.getLogger(__name__) + + +class ConfigEntryAuth(homeconnect.HomeConnectAPI): + """Provide Home Connect authentication tied to an OAuth2 based config entry.""" + + def __init__( + self, + hass: core.HomeAssistant, + config_entry: config_entries.ConfigEntry, + implementation: config_entry_oauth2_flow.AbstractOAuth2Implementation, + ): + """Initialize Home Connect Auth.""" + self.hass = hass + self.config_entry = config_entry + self.session = config_entry_oauth2_flow.OAuth2Session( + hass, config_entry, implementation + ) + super().__init__(self.session.token) + self.devices = [] + + def refresh_tokens(self) -> dict: + """Refresh and return new Home Connect tokens using Home Assistant OAuth2 session.""" + run_coroutine_threadsafe( + self.session.async_ensure_token_valid(), self.hass.loop + ).result() + + return self.session.token + + def get_devices(self): + """Get a dictionary of devices.""" + appl = self.get_appliances() + devices = [] + for app in appl: + if app.type == "Dryer": + device = Dryer(self.hass, app) + elif app.type == "Washer": + device = Washer(self.hass, app) + elif app.type == "Dishwasher": + device = Dishwasher(self.hass, app) + elif app.type == "FridgeFreezer": + device = FridgeFreezer(self.hass, app) + elif app.type == "Oven": + device = Oven(self.hass, app) + elif app.type == "CoffeeMaker": + device = CoffeeMaker(self.hass, app) + elif app.type == "Hood": + device = Hood(self.hass, app) + elif app.type == "Hob": + device = Hob(self.hass, app) + else: + _LOGGER.warning("Appliance type %s not implemented.", app.type) + continue + devices.append({"device": device, "entities": device.get_entity_info()}) + self.devices = devices + return devices + + +class HomeConnectDevice: + """Generic Home Connect device.""" + + # for some devices, this is instead BSH_POWER_STANDBY + # see https://developer.home-connect.com/docs/settings/power_state + power_off_state = BSH_POWER_OFF + + def __init__(self, hass, appliance): + """Initialize the device class.""" + self.hass = hass + self.appliance = appliance + + def initialize(self): + """Fetch the info needed to initialize the device.""" + try: + self.appliance.get_status() + except (HomeConnectError, ValueError): + _LOGGER.debug("Unable to fetch appliance status. Probably offline.") + try: + self.appliance.get_settings() + except (HomeConnectError, ValueError): + _LOGGER.debug("Unable to fetch settings. Probably offline.") + try: + program_active = self.appliance.get_programs_active() + except (HomeConnectError, ValueError): + _LOGGER.debug("Unable to fetch active programs. Probably offline.") + program_active = None + if program_active and "key" in program_active: + self.appliance.status[BSH_ACTIVE_PROGRAM] = {"value": program_active["key"]} + self.appliance.listen_events(callback=self.event_callback) + + def event_callback(self, appliance): + """Handle event.""" + _LOGGER.debug("Update triggered on %s", appliance.name) + _LOGGER.debug(self.appliance.status) + dispatcher_send(self.hass, SIGNAL_UPDATE_ENTITIES, appliance.haId) + + +class DeviceWithPrograms(HomeConnectDevice): + """Device with programs.""" + + PROGRAMS = [] + + def get_programs_available(self): + """Get the available programs.""" + return self.PROGRAMS + + def get_program_switches(self): + """Get a dictionary with info about program switches. + + There will be one switch for each program. + """ + programs = self.get_programs_available() + return [{"device": self, "program_name": p["name"]} for p in programs] + + def get_program_sensors(self): + """Get a dictionary with info about program sensors. + + There will be one of the four types of sensors for each + device. + """ + sensors = { + "Remaining Program Time": (None, None, DEVICE_CLASS_TIMESTAMP, 1), + "Duration": (TIME_SECONDS, "mdi:update", None, 1), + "Program Progress": (UNIT_PERCENTAGE, "mdi:progress-clock", None, 1), + } + return [ + { + "device": self, + "desc": k, + "unit": unit, + "key": "BSH.Common.Option.{}".format(k.replace(" ", "")), + "icon": icon, + "device_class": device_class, + "sign": sign, + } + for k, (unit, icon, device_class, sign) in sensors.items() + ] + + +class DeviceWithDoor(HomeConnectDevice): + """Device that has a door sensor.""" + + def get_door_entity(self): + """Get a dictionary with info about the door binary sensor.""" + return { + "device": self, + "desc": "Door", + "device_class": "door", + } + + +class Dryer(DeviceWithDoor, DeviceWithPrograms): + """Dryer class.""" + + PROGRAMS = [ + {"name": "LaundryCare.Dryer.Program.Cotton"}, + {"name": "LaundryCare.Dryer.Program.Synthetic"}, + {"name": "LaundryCare.Dryer.Program.Mix"}, + {"name": "LaundryCare.Dryer.Program.Blankets"}, + {"name": "LaundryCare.Dryer.Program.BusinessShirts"}, + {"name": "LaundryCare.Dryer.Program.DownFeathers"}, + {"name": "LaundryCare.Dryer.Program.Hygiene"}, + {"name": "LaundryCare.Dryer.Program.Jeans"}, + {"name": "LaundryCare.Dryer.Program.Outdoor"}, + {"name": "LaundryCare.Dryer.Program.SyntheticRefresh"}, + {"name": "LaundryCare.Dryer.Program.Towels"}, + {"name": "LaundryCare.Dryer.Program.Delicates"}, + {"name": "LaundryCare.Dryer.Program.Super40"}, + {"name": "LaundryCare.Dryer.Program.Shirts15"}, + {"name": "LaundryCare.Dryer.Program.Pillow"}, + {"name": "LaundryCare.Dryer.Program.AntiShrink"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class Dishwasher(DeviceWithDoor, DeviceWithPrograms): + """Dishwasher class.""" + + PROGRAMS = [ + {"name": "Dishcare.Dishwasher.Program.Auto1"}, + {"name": "Dishcare.Dishwasher.Program.Auto2"}, + {"name": "Dishcare.Dishwasher.Program.Auto3"}, + {"name": "Dishcare.Dishwasher.Program.Eco50"}, + {"name": "Dishcare.Dishwasher.Program.Quick45"}, + {"name": "Dishcare.Dishwasher.Program.Intensiv70"}, + {"name": "Dishcare.Dishwasher.Program.Normal65"}, + {"name": "Dishcare.Dishwasher.Program.Glas40"}, + {"name": "Dishcare.Dishwasher.Program.GlassCare"}, + {"name": "Dishcare.Dishwasher.Program.NightWash"}, + {"name": "Dishcare.Dishwasher.Program.Quick65"}, + {"name": "Dishcare.Dishwasher.Program.Normal45"}, + {"name": "Dishcare.Dishwasher.Program.Intensiv45"}, + {"name": "Dishcare.Dishwasher.Program.AutoHalfLoad"}, + {"name": "Dishcare.Dishwasher.Program.IntensivPower"}, + {"name": "Dishcare.Dishwasher.Program.MagicDaily"}, + {"name": "Dishcare.Dishwasher.Program.Super60"}, + {"name": "Dishcare.Dishwasher.Program.Kurz60"}, + {"name": "Dishcare.Dishwasher.Program.ExpressSparkle65"}, + {"name": "Dishcare.Dishwasher.Program.MachineCare"}, + {"name": "Dishcare.Dishwasher.Program.SteamFresh"}, + {"name": "Dishcare.Dishwasher.Program.MaximumCleaning"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class Oven(DeviceWithDoor, DeviceWithPrograms): + """Oven class.""" + + PROGRAMS = [ + {"name": "Cooking.Oven.Program.HeatingMode.PreHeating"}, + {"name": "Cooking.Oven.Program.HeatingMode.HotAir"}, + {"name": "Cooking.Oven.Program.HeatingMode.TopBottomHeating"}, + {"name": "Cooking.Oven.Program.HeatingMode.PizzaSetting"}, + {"name": "Cooking.Oven.Program.Microwave.600Watt"}, + ] + + power_off_state = BSH_POWER_STANDBY + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class Washer(DeviceWithDoor, DeviceWithPrograms): + """Washer class.""" + + PROGRAMS = [ + {"name": "LaundryCare.Washer.Program.Cotton"}, + {"name": "LaundryCare.Washer.Program.Cotton.CottonEco"}, + {"name": "LaundryCare.Washer.Program.EasyCare"}, + {"name": "LaundryCare.Washer.Program.Mix"}, + {"name": "LaundryCare.Washer.Program.DelicatesSilk"}, + {"name": "LaundryCare.Washer.Program.Wool"}, + {"name": "LaundryCare.Washer.Program.Sensitive"}, + {"name": "LaundryCare.Washer.Program.Auto30"}, + {"name": "LaundryCare.Washer.Program.Auto40"}, + {"name": "LaundryCare.Washer.Program.Auto60"}, + {"name": "LaundryCare.Washer.Program.Chiffon"}, + {"name": "LaundryCare.Washer.Program.Curtains"}, + {"name": "LaundryCare.Washer.Program.DarkWash"}, + {"name": "LaundryCare.Washer.Program.Dessous"}, + {"name": "LaundryCare.Washer.Program.Monsoon"}, + {"name": "LaundryCare.Washer.Program.Outdoor"}, + {"name": "LaundryCare.Washer.Program.PlushToy"}, + {"name": "LaundryCare.Washer.Program.ShirtsBlouses"}, + {"name": "LaundryCare.Washer.Program.SportFitness"}, + {"name": "LaundryCare.Washer.Program.Towels"}, + {"name": "LaundryCare.Washer.Program.WaterProof"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return { + "binary_sensor": [door_entity], + "switch": program_switches, + "sensor": program_sensors, + } + + +class CoffeeMaker(DeviceWithPrograms): + """Coffee maker class.""" + + PROGRAMS = [ + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Espresso"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoMacchiato"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Coffee"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Cappuccino"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.LatteMacchiato"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.CaffeLatte"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Americano"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.EspressoDoppio"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.FlatWhite"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Galao"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.MilkFroth"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.WarmMilk"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.Beverage.Ristretto"}, + {"name": "ConsumerProducts.CoffeeMaker.Program.CoffeeWorld.Cortado"}, + ] + + power_off_state = BSH_POWER_STANDBY + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return {"switch": program_switches, "sensor": program_sensors} + + +class Hood(DeviceWithPrograms): + """Hood class.""" + + PROGRAMS = [ + {"name": "Cooking.Common.Program.Hood.Automatic"}, + {"name": "Cooking.Common.Program.Hood.Venting"}, + {"name": "Cooking.Common.Program.Hood.DelayedShutOff"}, + ] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return {"switch": program_switches, "sensor": program_sensors} + + +class FridgeFreezer(DeviceWithDoor): + """Fridge/Freezer class.""" + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + door_entity = self.get_door_entity() + return {"binary_sensor": [door_entity]} + + +class Hob(DeviceWithPrograms): + """Hob class.""" + + PROGRAMS = [{"name": "Cooking.Hob.Program.PowerLevelMode"}] + + def get_entity_info(self): + """Get a dictionary with infos about the associated entities.""" + program_sensors = self.get_program_sensors() + program_switches = self.get_program_switches() + return {"switch": program_switches, "sensor": program_sensors} diff --git a/homeassistant/components/home_connect/binary_sensor.py b/homeassistant/components/home_connect/binary_sensor.py new file mode 100644 index 00000000000000..4810231b432075 --- /dev/null +++ b/homeassistant/components/home_connect/binary_sensor.py @@ -0,0 +1,65 @@ +"""Provides a binary sensor for Home Connect.""" +import logging + +from homeassistant.components.binary_sensor import BinarySensorEntity + +from .const import BSH_DOOR_STATE, DOMAIN +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect binary sensor.""" + + def get_entities(): + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("binary_sensor", []) + entities += [HomeConnectBinarySensor(**d) for d in entity_dicts] + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectBinarySensor(HomeConnectEntity, BinarySensorEntity): + """Binary sensor for Home Connect.""" + + def __init__(self, device, desc, device_class): + """Initialize the entity.""" + super().__init__(device, desc) + self._device_class = device_class + self._state = None + + @property + def is_on(self): + """Return true if the binary sensor is on.""" + return bool(self._state) + + @property + def available(self): + """Return true if the binary sensor is available.""" + return self._state is not None + + async def async_update(self): + """Update the binary sensor's status.""" + state = self.device.appliance.status.get(BSH_DOOR_STATE, {}) + if not state: + self._state = None + elif state.get("value") in [ + "BSH.Common.EnumType.DoorState.Closed", + "BSH.Common.EnumType.DoorState.Locked", + ]: + self._state = False + elif state.get("value") == "BSH.Common.EnumType.DoorState.Open": + self._state = True + else: + _LOGGER.warning("Unexpected value for HomeConnect door state: %s", state) + self._state = None + _LOGGER.debug("Updated, new state: %s", self._state) + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/home_connect/config_flow.py b/homeassistant/components/home_connect/config_flow.py new file mode 100644 index 00000000000000..4a714bac73f15d --- /dev/null +++ b/homeassistant/components/home_connect/config_flow.py @@ -0,0 +1,23 @@ +"""Config flow for Home Connect.""" +import logging + +from homeassistant import config_entries +from homeassistant.helpers import config_entry_oauth2_flow + +from .const import DOMAIN + +_LOGGER = logging.getLogger(__name__) + + +class OAuth2FlowHandler( + config_entry_oauth2_flow.AbstractOAuth2FlowHandler, domain=DOMAIN +): + """Config flow to handle Home Connect OAuth2 authentication.""" + + DOMAIN = DOMAIN + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH + + @property + def logger(self) -> logging.Logger: + """Return logger.""" + return logging.getLogger(__name__) diff --git a/homeassistant/components/home_connect/const.py b/homeassistant/components/home_connect/const.py new file mode 100644 index 00000000000000..10eb5dfd1e37c4 --- /dev/null +++ b/homeassistant/components/home_connect/const.py @@ -0,0 +1,16 @@ +"""Constants for the Home Connect integration.""" + +DOMAIN = "home_connect" + +OAUTH2_AUTHORIZE = "https://api.home-connect.com/security/oauth/authorize" +OAUTH2_TOKEN = "https://api.home-connect.com/security/oauth/token" + +BSH_POWER_STATE = "BSH.Common.Setting.PowerState" +BSH_POWER_ON = "BSH.Common.EnumType.PowerState.On" +BSH_POWER_OFF = "BSH.Common.EnumType.PowerState.Off" +BSH_POWER_STANDBY = "BSH.Common.EnumType.PowerState.Standby" +BSH_ACTIVE_PROGRAM = "BSH.Common.Root.ActiveProgram" +BSH_OPERATION_STATE = "BSH.Common.Status.OperationState" +BSH_DOOR_STATE = "BSH.Common.Status.DoorState" + +SIGNAL_UPDATE_ENTITIES = "home_connect.update_entities" diff --git a/homeassistant/components/home_connect/entity.py b/homeassistant/components/home_connect/entity.py new file mode 100644 index 00000000000000..12f86059023ccd --- /dev/null +++ b/homeassistant/components/home_connect/entity.py @@ -0,0 +1,67 @@ +"""Home Connect entity base class.""" + +import logging + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity import Entity + +from .api import HomeConnectDevice +from .const import DOMAIN, SIGNAL_UPDATE_ENTITIES + +_LOGGER = logging.getLogger(__name__) + + +class HomeConnectEntity(Entity): + """Generic Home Connect entity (base class).""" + + def __init__(self, device: HomeConnectDevice, desc: str) -> None: + """Initialize the entity.""" + self.device = device + self.desc = desc + self._name = f"{self.device.appliance.name} {desc}" + + async def async_added_to_hass(self): + """Register callbacks.""" + self.async_on_remove( + async_dispatcher_connect( + self.hass, SIGNAL_UPDATE_ENTITIES, self._update_callback + ) + ) + + @callback + def _update_callback(self, ha_id): + """Update data.""" + if ha_id == self.device.appliance.haId: + self.async_entity_update() + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def name(self): + """Return the name of the node (used for Entity_ID).""" + return self._name + + @property + def unique_id(self): + """Return the unique id base on the id returned by Home Connect and the entity name.""" + return f"{self.device.appliance.haId}-{self.desc}" + + @property + def device_info(self): + """Return info about the device.""" + return { + "identifiers": {(DOMAIN, self.device.appliance.haId)}, + "name": self.device.appliance.name, + "manufacturer": self.device.appliance.brand, + "model": self.device.appliance.vib, + } + + @callback + def async_entity_update(self): + """Update the entity.""" + _LOGGER.debug("Entity update triggered on %s", self) + self.async_schedule_update_ha_state(True) diff --git a/homeassistant/components/home_connect/manifest.json b/homeassistant/components/home_connect/manifest.json new file mode 100644 index 00000000000000..5c330f760b0b32 --- /dev/null +++ b/homeassistant/components/home_connect/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "home_connect", + "name": "Home Connect", + "documentation": "https://www.home-assistant.io/integrations/home_connect", + "dependencies": ["http"], + "codeowners": ["@DavidMStraub"], + "requirements": ["homeconnect==0.5"], + "config_flow": true +} diff --git a/homeassistant/components/home_connect/sensor.py b/homeassistant/components/home_connect/sensor.py new file mode 100644 index 00000000000000..add1a0084b3605 --- /dev/null +++ b/homeassistant/components/home_connect/sensor.py @@ -0,0 +1,92 @@ +"""Provides a sensor for Home Connect.""" + +from datetime import timedelta +import logging + +from homeassistant.const import DEVICE_CLASS_TIMESTAMP +import homeassistant.util.dt as dt_util + +from .const import DOMAIN +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect sensor.""" + + def get_entities(): + """Get a list of entities.""" + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("sensor", []) + entities += [HomeConnectSensor(**d) for d in entity_dicts] + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectSensor(HomeConnectEntity): + """Sensor class for Home Connect.""" + + def __init__(self, device, desc, key, unit, icon, device_class, sign=1): + """Initialize the entity.""" + super().__init__(device, desc) + self._state = None + self._key = key + self._unit = unit + self._icon = icon + self._device_class = device_class + self._sign = sign + + @property + def state(self): + """Return true if the binary sensor is on.""" + return self._state + + @property + def available(self): + """Return true if the sensor is available.""" + return self._state is not None + + async def async_update(self): + """Update the sensos status.""" + status = self.device.appliance.status + if self._key not in status: + self._state = None + else: + if self.device_class == DEVICE_CLASS_TIMESTAMP: + if "value" not in status[self._key]: + self._state = None + elif ( + self._state is not None + and self._sign == 1 + and self._state < dt_util.utcnow() + ): + # if the date is supposed to be in the future but we're + # already past it, set state to None. + self._state = None + else: + seconds = self._sign * float(status[self._key]["value"]) + self._state = ( + dt_util.utcnow() + timedelta(seconds=seconds) + ).isoformat() + else: + self._state = status[self._key].get("value") + _LOGGER.debug("Updated, new state: %s", self._state) + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit + + @property + def icon(self): + """Return the icon.""" + return self._icon + + @property + def device_class(self): + """Return the device class.""" + return self._device_class diff --git a/homeassistant/components/home_connect/strings.json b/homeassistant/components/home_connect/strings.json new file mode 100644 index 00000000000000..6125897c962422 --- /dev/null +++ b/homeassistant/components/home_connect/strings.json @@ -0,0 +1,15 @@ +{ + "config": { + "step": { + "pick_implementation": { + "title": "Pick Authentication Method" + } + }, + "abort": { + "missing_configuration": "The Home Connect component is not configured. Please follow the documentation." + }, + "create_entry": { + "default": "Successfully authenticated with Home Connect." + } + } +} diff --git a/homeassistant/components/home_connect/switch.py b/homeassistant/components/home_connect/switch.py new file mode 100644 index 00000000000000..c5fcdef25b7c90 --- /dev/null +++ b/homeassistant/components/home_connect/switch.py @@ -0,0 +1,158 @@ +"""Provides a switch for Home Connect.""" +import logging + +from homeconnect.api import HomeConnectError + +from homeassistant.components.switch import SwitchEntity + +from .const import ( + BSH_ACTIVE_PROGRAM, + BSH_OPERATION_STATE, + BSH_POWER_ON, + BSH_POWER_STATE, + DOMAIN, +) +from .entity import HomeConnectEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up the Home Connect switch.""" + + def get_entities(): + """Get a list of entities.""" + entities = [] + hc_api = hass.data[DOMAIN][config_entry.entry_id] + for device_dict in hc_api.devices: + entity_dicts = device_dict.get("entities", {}).get("switch", []) + entity_list = [HomeConnectProgramSwitch(**d) for d in entity_dicts] + entity_list += [HomeConnectPowerSwitch(device_dict["device"])] + entities += entity_list + return entities + + async_add_entities(await hass.async_add_executor_job(get_entities), True) + + +class HomeConnectProgramSwitch(HomeConnectEntity, SwitchEntity): + """Switch class for Home Connect.""" + + def __init__(self, device, program_name): + """Initialize the entity.""" + desc = " ".join(["Program", program_name.split(".")[-1]]) + super().__init__(device, desc) + self.program_name = program_name + self._state = None + self._remote_allowed = None + + @property + def is_on(self): + """Return true if the switch is on.""" + return bool(self._state) + + @property + def available(self): + """Return true if the entity is available.""" + return True + + async def async_turn_on(self, **kwargs): + """Start the program.""" + _LOGGER.debug("Tried to turn on program %s", self.program_name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.start_program, self.program_name + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to start program: %s", err) + self.async_entity_update() + + async def async_turn_off(self, **kwargs): + """Stop the program.""" + _LOGGER.debug("Tried to stop program %s", self.program_name) + try: + await self.hass.async_add_executor_job(self.device.appliance.stop_program) + except HomeConnectError as err: + _LOGGER.error("Error while trying to stop program: %s", err) + self.async_entity_update() + + async def async_update(self): + """Update the switch's status.""" + state = self.device.appliance.status.get(BSH_ACTIVE_PROGRAM, {}) + if state.get("value") == self.program_name: + self._state = True + else: + self._state = False + _LOGGER.debug("Updated, new state: %s", self._state) + + +class HomeConnectPowerSwitch(HomeConnectEntity, SwitchEntity): + """Power switch class for Home Connect.""" + + def __init__(self, device): + """Inititialize the entity.""" + super().__init__(device, "Power") + self._state = None + + @property + def is_on(self): + """Return true if the switch is on.""" + return bool(self._state) + + async def async_turn_on(self, **kwargs): + """Switch the device on.""" + _LOGGER.debug("Tried to switch on %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, BSH_POWER_STATE, BSH_POWER_ON, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn on device: %s", err) + self._state = False + self.async_entity_update() + + async def async_turn_off(self, **kwargs): + """Switch the device off.""" + _LOGGER.debug("tried to switch off %s", self.name) + try: + await self.hass.async_add_executor_job( + self.device.appliance.set_setting, + BSH_POWER_STATE, + self.device.power_off_state, + ) + except HomeConnectError as err: + _LOGGER.error("Error while trying to turn off device: %s", err) + self._state = True + self.async_entity_update() + + async def async_update(self): + """Update the switch's status.""" + if ( + self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value") + == BSH_POWER_ON + ): + self._state = True + elif ( + self.device.appliance.status.get(BSH_POWER_STATE, {}).get("value") + == self.device.power_off_state + ): + self._state = False + elif self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get( + "value", None + ) in [ + "BSH.Common.EnumType.OperationState.Ready", + "BSH.Common.EnumType.OperationState.DelayedStart", + "BSH.Common.EnumType.OperationState.Run", + "BSH.Common.EnumType.OperationState.Pause", + "BSH.Common.EnumType.OperationState.ActionRequired", + "BSH.Common.EnumType.OperationState.Aborting", + "BSH.Common.EnumType.OperationState.Finished", + ]: + self._state = True + elif ( + self.device.appliance.status.get(BSH_OPERATION_STATE, {}).get("value") + == "BSH.Common.EnumType.OperationState.Inactive" + ): + self._state = False + else: + self._state = None + _LOGGER.debug("Updated, new state: %s", self._state) diff --git a/homeassistant/components/homekit/__init__.py b/homeassistant/components/homekit/__init__.py index 184fce2309bba7..d9ce432835b88d 100644 --- a/homeassistant/components/homekit/__init__.py +++ b/homeassistant/components/homekit/__init__.py @@ -527,6 +527,7 @@ async def _async_register_bridge(self): def _start(self, bridged_states): from . import ( # noqa: F401 pylint: disable=unused-import, import-outside-toplevel + type_cameras, type_covers, type_fans, type_lights, diff --git a/homeassistant/components/homekit/accessories.py b/homeassistant/components/homekit/accessories.py index ddafbd8fa66a56..ac6e8969d91f56 100644 --- a/homeassistant/components/homekit/accessories.py +++ b/homeassistant/components/homekit/accessories.py @@ -164,13 +164,12 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "media_player": device_class = state.attributes.get(ATTR_DEVICE_CLASS) - feature_list = config.get(CONF_FEATURE_LIST) + feature_list = config.get(CONF_FEATURE_LIST, []) if device_class == DEVICE_CLASS_TV: a_type = "TelevisionMediaPlayer" - else: - if feature_list and validate_media_player_features(state, feature_list): - a_type = "MediaPlayer" + elif validate_media_player_features(state, feature_list): + a_type = "MediaPlayer" elif state.domain == "sensor": device_class = state.attributes.get(ATTR_DEVICE_CLASS) @@ -209,6 +208,9 @@ def get_accessory(hass, driver, state, aid, config): elif state.domain == "water_heater": a_type = "WaterHeater" + elif state.domain == "camera": + a_type = "Camera" + if a_type is None: return None @@ -220,10 +222,19 @@ class HomeAccessory(Accessory): """Adapter class for Accessory.""" def __init__( - self, hass, driver, name, entity_id, aid, config, category=CATEGORY_OTHER + self, + hass, + driver, + name, + entity_id, + aid, + config, + *args, + category=CATEGORY_OTHER, + **kwargs, ): """Initialize a Accessory object.""" - super().__init__(driver, name, aid=aid) + super().__init__(driver=driver, display_name=name, aid=aid, *args, **kwargs) model = split_entity_id(entity_id)[0].replace("_", " ").title() self.set_info_service( firmware_revision=__version__, @@ -460,6 +471,18 @@ def __init__(self, hass, driver, name): def setup_message(self): """Prevent print of pyhap setup message to terminal.""" + def get_snapshot(self, info): + """Get snapshot from accessory if supported.""" + acc = self.accessories.get(info["aid"]) + if acc is None: + raise ValueError("Requested snapshot for missing accessory") + if not hasattr(acc, "get_snapshot"): + raise ValueError( + "Got a request for snapshot, but the Accessory " + 'does not define a "get_snapshot" method' + ) + return acc.get_snapshot(info) + class HomeDriver(AccessoryDriver): """Adapter class for AccessoryDriver.""" diff --git a/homeassistant/components/homekit/config_flow.py b/homeassistant/components/homekit/config_flow.py index 0f83b7a3c24fa1..039b0ef063ad5b 100644 --- a/homeassistant/components/homekit/config_flow.py +++ b/homeassistant/components/homekit/config_flow.py @@ -38,6 +38,7 @@ "alarm_control_panel", "automation", "binary_sensor", + "camera", "climate", "cover", "demo", diff --git a/homeassistant/components/homekit/const.py b/homeassistant/components/homekit/const.py index ab0c15ee9a7eb9..f660af9bba8ad0 100644 --- a/homeassistant/components/homekit/const.py +++ b/homeassistant/components/homekit/const.py @@ -19,6 +19,8 @@ # #### Config #### CONF_ADVERTISE_IP = "advertise_ip" +CONF_AUDIO_MAP = "audio_map" +CONF_AUDIO_PACKET_SIZE = "audio_packet_size" CONF_AUTO_START = "auto_start" CONF_ENTITY_CONFIG = "entity_config" CONF_FEATURE = "feature" @@ -27,16 +29,31 @@ CONF_LINKED_BATTERY_SENSOR = "linked_battery_sensor" CONF_LINKED_BATTERY_CHARGING_SENSOR = "linked_battery_charging_sensor" CONF_LOW_BATTERY_THRESHOLD = "low_battery_threshold" +CONF_MAX_FPS = "max_fps" +CONF_MAX_HEIGHT = "max_height" +CONF_MAX_WIDTH = "max_width" CONF_SAFE_MODE = "safe_mode" CONF_ZEROCONF_DEFAULT_INTERFACE = "zeroconf_default_interface" +CONF_STREAM_ADDRESS = "stream_address" +CONF_STREAM_SOURCE = "stream_source" +CONF_SUPPORT_AUDIO = "support_audio" +CONF_VIDEO_MAP = "video_map" +CONF_VIDEO_PACKET_SIZE = "video_packet_size" # #### Config Defaults #### +DEFAULT_AUDIO_MAP = "0:a:0" +DEFAULT_AUDIO_PACKET_SIZE = 188 DEFAULT_AUTO_START = True DEFAULT_LOW_BATTERY_THRESHOLD = 20 +DEFAULT_MAX_FPS = 30 +DEFAULT_MAX_HEIGHT = 1080 +DEFAULT_MAX_WIDTH = 1920 DEFAULT_PORT = 51827 DEFAULT_CONFIG_FLOW_PORT = 51828 DEFAULT_SAFE_MODE = False DEFAULT_ZEROCONF_DEFAULT_INTERFACE = False +DEFAULT_VIDEO_MAP = "0:v:0" +DEFAULT_VIDEO_PACKET_SIZE = 1316 # #### Features #### FEATURE_ON_OFF = "on_off" diff --git a/homeassistant/components/homekit/manifest.json b/homeassistant/components/homekit/manifest.json index 796bb3933f75d2..3d0c84d31b5cc5 100644 --- a/homeassistant/components/homekit/manifest.json +++ b/homeassistant/components/homekit/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit", "documentation": "https://www.home-assistant.io/integrations/homekit", "requirements": ["HAP-python==2.8.3","fnvhash==0.1.0","PyQRCode==1.2.1","base36==0.1.1"], - "dependencies": ["http"], + "dependencies": ["http", "camera", "ffmpeg"], "after_dependencies": ["logbook"], "codeowners": ["@bdraco"], "config_flow": true diff --git a/homeassistant/components/homekit/translations/de.json b/homeassistant/components/homekit/translations/de.json new file mode 100644 index 00000000000000..fe4fc52c695722 --- /dev/null +++ b/homeassistant/components/homekit/translations/de.json @@ -0,0 +1,29 @@ +{ + "config": { + "step": { + "user": { + "data": { + "include_domains": "Einzubeziehende Domains" + } + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "safe_mode": "Abgesicherter Modus (nur aktivieren, wenn das Pairing fehlschl\u00e4gt)" + }, + "title": "Erweiterte Konfiguration" + }, + "exclude": { + "data": { + "exclude_entities": "Auszuschlie\u00dfende Entit\u00e4ten" + } + }, + "yaml": { + "description": "Dieser Eintrag wird \u00fcber YAML gesteuert" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/en.json b/homeassistant/components/homekit/translations/en.json index 08ebfa44c45564..8d43341c61b070 100644 --- a/homeassistant/components/homekit/translations/en.json +++ b/homeassistant/components/homekit/translations/en.json @@ -1,17 +1,33 @@ { - "title": "HomeKit Bridge", - "options": { + "config": { + "abort": { + "port_name_in_use": "A bridge with the same name or port is already configured." + }, "step": { - "yaml": { - "title": "Adjust HomeKit Bridge Options", - "description": "This entry is controlled via YAML" + "pairing": { + "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d.", + "title": "Pair HomeKit Bridge" }, - "init": { + "user": { "data": { - "include_domains": "[%key:component::homekit::config::step::user::data::include_domains%]" + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "include_domains": "Domains to include" }, - "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", - "title": "Select domains to bridge." + "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", + "title": "Activate HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", + "safe_mode": "Safe Mode (enable only if pairing fails)", + "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" + }, + "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", + "title": "Advanced Configuration" }, "exclude": { "data": { @@ -20,34 +36,18 @@ "description": "Choose the entities that you do NOT want to be bridged.", "title": "Exclude entities in selected domains from bridge" }, - "advanced": { - "data": { - "auto_start": "[%key:component::homekit::config::step::user::data::auto_start%]", - "safe_mode": "Safe Mode (enable only if pairing fails)", - "zeroconf_default_interface": "Use default zeroconf interface (enable if the bridge cannot be found in the Home app)" - }, - "description": "These settings only need to be adjusted if the HomeKit bridge is not functional.", - "title": "Advanced Configuration" - } - } - }, - "config": { - "step": { - "user": { + "init": { "data": { - "auto_start": "Autostart (disable if using Z-Wave or other delayed start system)", "include_domains": "Domains to include" }, - "description": "A HomeKit Bridge will allow you to access your Home Assistant entities in HomeKit. HomeKit Bridges are limited to 150 accessories per instance including the bridge itself. If you wish to bridge more than the maximum number of accessories, it is recommended that you use multiple HomeKit bridges for different domains. Detailed entity configuration is only available via YAML for the primary bridge.", - "title": "Activate HomeKit Bridge" + "description": "Entities in the \u201cDomains to include\u201d will be bridged to HomeKit. You will be able to select which entities to exclude from this list on the next screen.", + "title": "Select domains to bridge." }, - "pairing": { - "title": "Pair HomeKit Bridge", - "description": "As soon as the {name} bridge is ready, pairing will be available in \u201cNotifications\u201d as \u201cHomeKit Bridge Setup\u201d." + "yaml": { + "description": "This entry is controlled via YAML", + "title": "Adjust HomeKit Bridge Options" } - }, - "abort": { - "port_name_in_use": "A bridge with the same name or port is already configured." } - } -} + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es-419.json b/homeassistant/components/homekit/translations/es-419.json new file mode 100644 index 00000000000000..a3f1b1acc17876 --- /dev/null +++ b/homeassistant/components/homekit/translations/es-419.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Un puente con el mismo nombre o puerto ya est\u00e1 configurado." + }, + "step": { + "pairing": { + "description": "Tan pronto como el puente {name} est\u00e9 listo, el emparejamiento estar\u00e1 disponible en \"Notificaciones\" como \"Configuraci\u00f3n del puente HomeKit\".", + "title": "Emparejar HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", + "include_domains": "Dominios para incluir" + }, + "description": "Un HomeKit Bridge le permitir\u00e1 acceder a sus entidades de Home Assistant en HomeKit. Los puentes HomeKit est\u00e1n limitados a 150 accesorios por instancia, incluido el puente mismo. Si desea unir m\u00e1s de la cantidad m\u00e1xima de accesorios, se recomienda que use m\u00faltiples puentes HomeKit para diferentes dominios. La configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible a trav\u00e9s de YAML para el puente primario.", + "title": "Activar HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Inicio autom\u00e1tico (deshabilitar si se usa Z-Wave u otro sistema de inicio diferido)", + "safe_mode": "Modo seguro (habil\u00edtelo solo si falla el emparejamiento)", + "zeroconf_default_interface": "Use la interfaz zeroconf predeterminada (habil\u00edtela si no se puede encontrar el puente en la aplicaci\u00f3n Inicio)" + }, + "description": "Esta configuraci\u00f3n solo necesita ser ajustada si el puente HomeKit no es funcional.", + "title": "Configuraci\u00f3n avanzada" + }, + "exclude": { + "data": { + "exclude_entities": "Entidades a excluir" + }, + "description": "Seleccione las entidades que NO desea puentear.", + "title": "Excluir entidades en dominios seleccionados del puente" + }, + "init": { + "data": { + "include_domains": "Dominios para incluir" + }, + "description": "Las entidades en los \"Dominios para incluir\" se vincular\u00e1n a HomeKit. Podr\u00e1 seleccionar qu\u00e9 entidades excluir de esta lista en la siguiente pantalla.", + "title": "Seleccione dominios para puentear." + }, + "yaml": { + "description": "Esta entrada se controla a trav\u00e9s de YAML", + "title": "Ajuste las opciones de puente de HomeKit" + } + } + }, + "title": "Puente HomeKit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/es.json b/homeassistant/components/homekit/translations/es.json new file mode 100644 index 00000000000000..7e5b7477a60c1f --- /dev/null +++ b/homeassistant/components/homekit/translations/es.json @@ -0,0 +1,18 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Ya est\u00e1 configurada una pasarela con el mismo nombre o puerto." + }, + "step": { + "pairing": { + "description": "Tan pronto como la pasarela {name} est\u00e9 lista, la vinculaci\u00f3n estar\u00e1 disponible en \"Notificaciones\" como \"configuraci\u00f3n de pasarela Homekit\"", + "title": "Vincular pasarela Homekit" + }, + "user": { + "description": "Una pasarela Homekit permitir\u00e1 a Homekit acceder a sus entidades de Home Assistant. La pasarela Homekit est\u00e1 limitada a 150 accesorios por instancia incluyendo la propia pasarela. Si desea enlazar m\u00e1s del m\u00e1ximo n\u00famero de accesorios, se recomienda que use multiples pasarelas Homekit para diferentes dominios. Configuraci\u00f3n detallada de la entidad solo est\u00e1 disponible via YAML para la pasarela primaria.", + "title": "Activar pasarela Homekit" + } + } + }, + "title": "Pasarela Homekit" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/it.json b/homeassistant/components/homekit/translations/it.json new file mode 100644 index 00000000000000..9ca0b69165c948 --- /dev/null +++ b/homeassistant/components/homekit/translations/it.json @@ -0,0 +1,53 @@ +{ + "config": { + "abort": { + "port_name_in_use": "Un bridge con lo stesso nome o porta \u00e8 gi\u00e0 configurato." + }, + "step": { + "pairing": { + "description": "Non appena il bridge {name} \u00e8 pronto, l'associazione sar\u00e0 disponibile in \"Notifiche\" come \"HomeKit Bridge Setup\".", + "title": "Associa HomeKit Bridge" + }, + "user": { + "data": { + "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", + "include_domains": "Domini da includere" + }, + "description": "Un HomeKit Bridge ti permetter\u00e0 di accedere alle tue entit\u00e0 Home Assistant in HomeKit. Gli HomeKit Bridges sono limitati a 150 accessori per istanza, incluso il bridge stesso. Se si desidera collegare un numero di accessori superiore al massimo consentito, si consiglia di utilizzare pi\u00f9 HomeKit Bridge per domini diversi. La configurazione dettagliata delle entit\u00e0 \u00e8 disponibile solo tramite YAML per il bridge primario.", + "title": "Attiva HomeKit Bridge" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "Avvio automatico (disabilitare se si utilizza Z-Wave o un altro sistema di avvio ritardato)", + "safe_mode": "Modalit\u00e0 provvisoria (attivare solo in caso di errore di associazione)", + "zeroconf_default_interface": "Utilizzare l'interfaccia zeroconf predefinita (abilitare se il bridge non pu\u00f2 essere trovato nell'app Home)" + }, + "description": "Queste impostazioni devono essere modificate solo se il bridge HomeKit non funziona.", + "title": "Configurazione Avanzata" + }, + "exclude": { + "data": { + "exclude_entities": "Entit\u00e0 da escludere" + }, + "description": "Scegliere le entit\u00e0 che NON si desidera collegare.", + "title": "Escludere le entit\u00e0 dal bridge nei domini selezionati " + }, + "init": { + "data": { + "include_domains": "Domini da includere" + }, + "description": "Le entit\u00e0 nei \"Domini da includere\" saranno collegate a HomeKit. Sarai in grado di selezionare quali entit\u00e0 escludere da questo elenco nella schermata successiva.", + "title": "Selezionare i domini al bridge." + }, + "yaml": { + "description": "Questa voce \u00e8 controllata tramite YAML", + "title": "Regolare le opzioni di HomeKit Bridge" + } + } + }, + "title": "HomeKit Bridge" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/translations/zh-Hans.json b/homeassistant/components/homekit/translations/zh-Hans.json new file mode 100644 index 00000000000000..c47a6fe104fc48 --- /dev/null +++ b/homeassistant/components/homekit/translations/zh-Hans.json @@ -0,0 +1,49 @@ +{ + "config": { + "abort": { + "port_name_in_use": "\u5df2\u914d\u7f6e\u8fc7\u5177\u6709\u76f8\u540c\u540d\u79f0\u6216\u7aef\u53e3\u7684\u6865\u63a5\u5668\u3002" + }, + "step": { + "pairing": { + "description": "\u4e00\u65e6\u6865\u63a5\u5668 {name} \u51c6\u5907\u5c31\u7eea\uff0c\u5c31\u53ef\u4ee5\u5728\u201c\u901a\u77e5\u201d\u627e\u5230\u201cHomeKit \u6865\u63a5\u5668\u914d\u7f6e\u201d\u8fdb\u884c\u914d\u5bf9\u3002", + "title": "\u914d\u5bf9 HomeKit \u6865\u63a5\u5668" + }, + "user": { + "data": { + "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", + "include_domains": "\u8981\u5305\u542b\u7684\u57df" + }, + "description": "HomeKit \u6865\u63a5\u5668\u53ef\u4ee5\u8ba9\u60a8\u901a\u8fc7 HomeKit \u8bbf\u95ee Home Assistant \u4e2d\u7684\u5b9e\u4f53\u3002\u6bcf\u4e2a\u6865\u63a5\u5668\u5b9e\u4f8b\u6700\u591a\u53ef\u6a21\u62df 150 \u4e2a\u914d\u4ef6\uff0c\u5305\u62ec\u6865\u63a5\u5668\u672c\u8eab\u3002\u5982\u679c\u60a8\u5e0c\u671b\u6865\u63a5\u7684\u914d\u4ef6\u591a\u4e8e\u6b64\u6570\u91cf\uff0c\u5efa\u8bae\u4e3a\u4e0d\u540c\u7684\u57df\u4f7f\u7528\u591a\u4e2a HomeKit \u6865\u63a5\u5668\u3002\u8be6\u7ec6\u7684\u5b9e\u4f53\u914d\u7f6e\u4ec5\u53ef\u7528\u4e8e\u4e3b\u6865\u63a5\u5668\uff0c\u4e14\u987b\u901a\u8fc7 YAML \u914d\u7f6e\u3002", + "title": "\u6fc0\u6d3b HomeKit \u6865\u63a5\u5668" + } + } + }, + "options": { + "step": { + "advanced": { + "data": { + "auto_start": "\u81ea\u52a8\u542f\u52a8\uff08\u5982\u679c\u4f7f\u7528 Z-Wave \u6216\u5176\u4ed6\u5ef6\u8fdf\u542f\u52a8\u7cfb\u7edf\uff0c\u8bf7\u7981\u7528\u6b64\u9879\uff09", + "safe_mode": "\u5b89\u5168\u6a21\u5f0f\uff08\u4ec5\u5728\u914d\u5bf9\u5931\u8d25\u65f6\u542f\u7528\uff09", + "zeroconf_default_interface": "\u4f7f\u7528\u9ed8\u8ba4\u7684 zeroconf \u63a5\u53e3\uff08\u5982\u679c\u5728\u201c\u5bb6\u5ead\u201d\u5e94\u7528\u7a0b\u5e8f\u4e2d\u627e\u4e0d\u5230\u6865\u63a5\u5668\u5219\u542f\u7528\uff09" + }, + "description": "\u8fd9\u4e9b\u8bbe\u7f6e\u53ea\u6709\u5f53 HomeKit \u6865\u63a5\u5668\u529f\u80fd\u4e0d\u6b63\u5e38\u65f6\u624d\u9700\u8981\u8c03\u6574\u3002", + "title": "\u9ad8\u7ea7\u914d\u7f6e" + }, + "exclude": { + "data": { + "exclude_entities": "\u8981\u6392\u9664\u7684\u5b9e\u4f53" + }, + "description": "\u9009\u62e9\u4e0d\u9700\u8981\u6865\u63a5\u7684\u5b9e\u4f53\u3002", + "title": "\u5bf9\u9009\u62e9\u7684\u57df\u6392\u9664\u5b9e\u4f53" + }, + "init": { + "data": { + "include_domains": "\u8981\u5305\u542b\u7684\u57df" + }, + "description": "\u201c\u8981\u5305\u542b\u7684\u57df\u201d\u4e2d\u7684\u5b9e\u4f53\u5c06\u88ab\u6865\u63a5\u5230 HomeKit\u3002\u5728\u4e0b\u4e00\u9875\u53ef\u4ee5\u9009\u62e9\u8981\u6392\u9664\u5176\u4e2d\u7684\u54ea\u4e9b\u5b9e\u4f53\u3002", + "title": "\u9009\u62e9\u8981\u6865\u63a5\u7684\u57df\u3002" + } + } + }, + "title": "HomeKit \u6865\u63a5\u5668" +} \ No newline at end of file diff --git a/homeassistant/components/homekit/type_cameras.py b/homeassistant/components/homekit/type_cameras.py new file mode 100644 index 00000000000000..50c02a3956223d --- /dev/null +++ b/homeassistant/components/homekit/type_cameras.py @@ -0,0 +1,241 @@ +"""Class to hold all camera accessories.""" +import asyncio +import logging + +from haffmpeg.core import HAFFmpeg +from pyhap.camera import ( + VIDEO_CODEC_PARAM_LEVEL_TYPES, + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES, + Camera as PyhapCamera, +) +from pyhap.const import CATEGORY_CAMERA + +from homeassistant.components.camera.const import DOMAIN as DOMAIN_CAMERA +from homeassistant.components.ffmpeg import DATA_FFMPEG +from homeassistant.util import get_local_ip + +from .accessories import TYPES, HomeAccessory +from .const import ( + CONF_AUDIO_MAP, + CONF_AUDIO_PACKET_SIZE, + CONF_MAX_FPS, + CONF_MAX_HEIGHT, + CONF_MAX_WIDTH, + CONF_STREAM_ADDRESS, + CONF_STREAM_SOURCE, + CONF_SUPPORT_AUDIO, + CONF_VIDEO_MAP, + CONF_VIDEO_PACKET_SIZE, +) +from .util import CAMERA_SCHEMA + +_LOGGER = logging.getLogger(__name__) + +VIDEO_OUTPUT = ( + "-map {v_map} -an " + "-c:v libx264 -profile:v {v_profile} -tune zerolatency -pix_fmt yuv420p " + "-r {fps} " + "-b:v {v_max_bitrate}k -bufsize {v_bufsize}k -maxrate {v_max_bitrate}k " + "-payload_type 99 " + "-ssrc {v_ssrc} -f rtp " + "-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params {v_srtp_key} " + "srtp://{address}:{v_port}?rtcpport={v_port}&" + "localrtcpport={v_port}&pkt_size={v_pkt_size}" +) + +AUDIO_ENCODER_OPUS = "libopus -application lowdelay" + +AUDIO_OUTPUT = ( + "-map {a_map} -vn " + "-c:a {a_encoder} " + "-ac 1 -ar {a_sample_rate}k " + "-b:a {a_max_bitrate}k -bufsize {a_bufsize}k " + "-payload_type 110 " + "-ssrc {a_ssrc} -f rtp " + "-srtp_out_suite AES_CM_128_HMAC_SHA1_80 -srtp_out_params {a_srtp_key} " + "srtp://{address}:{a_port}?rtcpport={a_port}&" + "localrtcpport={a_port}&pkt_size={a_pkt_size}" +) + +SLOW_RESOLUTIONS = [ + (320, 180, 15), + (320, 240, 15), +] + +RESOLUTIONS = [ + (320, 180), + (320, 240), + (480, 270), + (480, 360), + (640, 360), + (640, 480), + (1024, 576), + (1024, 768), + (1280, 720), + (1280, 960), + (1920, 1080), +] + +VIDEO_PROFILE_NAMES = ["baseline", "main", "high"] + + +@TYPES.register("Camera") +class Camera(HomeAccessory, PyhapCamera): + """Generate a Camera accessory.""" + + def __init__(self, hass, driver, name, entity_id, aid, config): + """Initialize a Camera accessory object.""" + self._ffmpeg = hass.data[DATA_FFMPEG] + self._camera = hass.data[DOMAIN_CAMERA] + config_w_defaults = CAMERA_SCHEMA(config) + + max_fps = config_w_defaults[CONF_MAX_FPS] + max_width = config_w_defaults[CONF_MAX_WIDTH] + max_height = config_w_defaults[CONF_MAX_HEIGHT] + resolutions = [ + (w, h, fps) + for w, h, fps in SLOW_RESOLUTIONS + if w <= max_width and h <= max_height and fps < max_fps + ] + [ + (w, h, max_fps) + for w, h in RESOLUTIONS + if w <= max_width and h <= max_height + ] + + video_options = { + "codec": { + "profiles": [ + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES["BASELINE"], + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES["MAIN"], + VIDEO_CODEC_PARAM_PROFILE_ID_TYPES["HIGH"], + ], + "levels": [ + VIDEO_CODEC_PARAM_LEVEL_TYPES["TYPE3_1"], + VIDEO_CODEC_PARAM_LEVEL_TYPES["TYPE3_2"], + VIDEO_CODEC_PARAM_LEVEL_TYPES["TYPE4_0"], + ], + }, + "resolutions": resolutions, + } + audio_options = {"codecs": [{"type": "OPUS", "samplerate": 24}]} + + stream_address = config_w_defaults.get(CONF_STREAM_ADDRESS, get_local_ip()) + + options = { + "video": video_options, + "audio": audio_options, + "address": stream_address, + "srtp": True, + } + + super().__init__( + hass, + driver, + name, + entity_id, + aid, + config_w_defaults, + category=CATEGORY_CAMERA, + options=options, + ) + + def update_state(self, new_state): + """Handle state change to update HomeKit value.""" + pass # pylint: disable=unnecessary-pass + + async def _async_get_stream_source(self): + """Find the camera stream source url.""" + camera = self._camera.get_entity(self.entity_id) + if not camera or not camera.is_on: + return None + stream_source = self.config.get(CONF_STREAM_SOURCE) + if stream_source: + return stream_source + try: + return await camera.stream_source() + except Exception: # pylint: disable=broad-except + _LOGGER.exception( + "Failed to get stream source - this could be a transient error or your camera might not be compatible with HomeKit yet" + ) + + async def start_stream(self, session_info, stream_config): + """Start a new stream with the given configuration.""" + _LOGGER.debug( + "[%s] Starting stream with the following parameters: %s", + session_info["id"], + stream_config, + ) + input_source = await self._async_get_stream_source() + if not input_source: + _LOGGER.error("Camera has no stream source") + return False + if "-i " not in input_source: + input_source = "-i " + input_source + output_vars = stream_config.copy() + output_vars.update( + { + "v_profile": VIDEO_PROFILE_NAMES[ + int.from_bytes(stream_config["v_profile_id"], byteorder="big") + ], + "v_bufsize": stream_config["v_max_bitrate"] * 2, + "v_map": self.config[CONF_VIDEO_MAP], + "v_pkt_size": self.config[CONF_VIDEO_PACKET_SIZE], + "a_bufsize": stream_config["a_max_bitrate"] * 2, + "a_map": self.config[CONF_AUDIO_MAP], + "a_pkt_size": self.config[CONF_AUDIO_PACKET_SIZE], + "a_encoder": AUDIO_ENCODER_OPUS, + } + ) + output = VIDEO_OUTPUT.format(**output_vars) + if self.config[CONF_SUPPORT_AUDIO]: + output = output + " " + AUDIO_OUTPUT.format(**output_vars) + _LOGGER.debug("FFmpeg output settings: %s", output) + stream = HAFFmpeg(self._ffmpeg.binary, loop=self.driver.loop) + opened = await stream.open( + cmd=[], input_source=input_source, output=output, stdout_pipe=False + ) + if not opened: + _LOGGER.error("Failed to open ffmpeg stream") + return False + session_info["stream"] = stream + _LOGGER.info( + "[%s] Started stream process - PID %d", + session_info["id"], + stream.process.pid, + ) + return True + + async def stop_stream(self, session_info): + """Stop the stream for the given ``session_id``.""" + session_id = session_info["id"] + stream = session_info.get("stream") + if not stream: + _LOGGER.debug("No stream for session ID %s", session_id) + _LOGGER.info("[%s] Stopping stream.", session_id) + + try: + await stream.close() + return + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to gracefully close stream.") + + try: + await stream.kill() + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Failed to forcefully close stream.") + _LOGGER.debug("Stream process stopped forcefully.") + + async def reconfigure_stream(self, session_info, stream_config): + """Reconfigure the stream so that it uses the given ``stream_config``.""" + return True + + def get_snapshot(self, image_size): + """Return a jpeg of a snapshot from the camera.""" + return ( + asyncio.run_coroutine_threadsafe( + self.hass.components.camera.async_get_image(self.entity_id), + self.hass.loop, + ) + .result() + .content + ) diff --git a/homeassistant/components/homekit/type_media_players.py b/homeassistant/components/homekit/type_media_players.py index 154355a0da382a..209baad125ed8f 100644 --- a/homeassistant/components/homekit/type_media_players.py +++ b/homeassistant/components/homekit/type_media_players.py @@ -64,6 +64,7 @@ SERV_TELEVISION, SERV_TELEVISION_SPEAKER, ) +from .util import get_media_player_features _LOGGER = logging.getLogger(__name__) @@ -83,10 +84,12 @@ # 15: "Information", } +# Names may not contain special characters +# or emjoi (/ is a special character for Apple) MODE_FRIENDLY_NAME = { FEATURE_ON_OFF: "Power", - FEATURE_PLAY_PAUSE: "Play/Pause", - FEATURE_PLAY_STOP: "Play/Stop", + FEATURE_PLAY_PAUSE: "Play-Pause", + FEATURE_PLAY_STOP: "Play-Stop", FEATURE_TOGGLE_MUTE: "Mute", } @@ -105,7 +108,9 @@ def __init__(self, *args): FEATURE_PLAY_STOP: None, FEATURE_TOGGLE_MUTE: None, } - feature_list = self.config[CONF_FEATURE_LIST] + feature_list = self.config.get( + CONF_FEATURE_LIST, get_media_player_features(state) + ) if FEATURE_ON_OFF in feature_list: name = self.generate_service_name(FEATURE_ON_OFF) @@ -213,7 +218,7 @@ def update_state(self, new_state): self.chars[FEATURE_PLAY_STOP].set_value(hk_state) if self.chars[FEATURE_TOGGLE_MUTE]: - current_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) + current_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) _LOGGER.debug( '%s: Set current state for "toggle_mute" to %s', self.entity_id, @@ -239,9 +244,7 @@ def __init__(self, *args): # Add additional characteristics if volume or input selection supported self.chars_tv = [] self.chars_speaker = [] - features = self.hass.states.get(self.entity_id).attributes.get( - ATTR_SUPPORTED_FEATURES, 0 - ) + features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) if features & (SUPPORT_PLAY | SUPPORT_PAUSE): self.chars_tv.append(CHAR_REMOTE_KEY) @@ -252,7 +255,8 @@ def __init__(self, *args): if features & SUPPORT_VOLUME_SET: self.chars_speaker.append(CHAR_VOLUME) - if features & SUPPORT_SELECT_SOURCE: + source_list = state.attributes.get(ATTR_INPUT_SOURCE_LIST, []) + if source_list and features & SUPPORT_SELECT_SOURCE: self.support_select_source = True serv_tv = self.add_preload_service(SERV_TELEVISION, self.chars_tv) @@ -297,9 +301,7 @@ def __init__(self, *args): ) if self.support_select_source: - self.sources = self.hass.states.get(self.entity_id).attributes.get( - ATTR_INPUT_SOURCE_LIST, [] - ) + self.sources = source_list self.char_input_source = serv_tv.configure_char( CHAR_ACTIVE_IDENTIFIER, setter_callback=self.set_input_source ) @@ -379,14 +381,13 @@ def update_state(self, new_state): hk_state = 0 if current_state not in ("None", STATE_OFF, STATE_UNKNOWN): hk_state = 1 - _LOGGER.debug("%s: Set current active state to %s", self.entity_id, hk_state) if self.char_active.value != hk_state: self.char_active.set_value(hk_state) # Set mute state if CHAR_VOLUME_SELECTOR in self.chars_speaker: - current_mute_state = new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED) + current_mute_state = bool(new_state.attributes.get(ATTR_MEDIA_VOLUME_MUTED)) _LOGGER.debug( "%s: Set current mute state to %s", self.entity_id, current_mute_state, ) @@ -394,20 +395,16 @@ def update_state(self, new_state): self.char_mute.set_value(current_mute_state) # Set active input - if self.support_select_source: + if self.support_select_source and self.sources: source_name = new_state.attributes.get(ATTR_INPUT_SOURCE) - if self.sources: - _LOGGER.debug( - "%s: Set current input to %s", self.entity_id, source_name + _LOGGER.debug("%s: Set current input to %s", self.entity_id, source_name) + if source_name in self.sources: + index = self.sources.index(source_name) + if self.char_input_source.value != index: + self.char_input_source.set_value(index) + else: + _LOGGER.warning( + "%s: Sources out of sync. Restart Home Assistant", self.entity_id, ) - if source_name in self.sources: - index = self.sources.index(source_name) - if self.char_input_source.value != index: - self.char_input_source.set_value(index) - else: - _LOGGER.warning( - "%s: Sources out of sync. Restart Home Assistant", - self.entity_id, - ) - if self.char_input_source.value != 0: - self.char_input_source.set_value(0) + if self.char_input_source.value != 0: + self.char_input_source.set_value(0) diff --git a/homeassistant/components/homekit/util.py b/homeassistant/components/homekit/util.py index 3ccf73d39250b6..0dc821492b08f7 100644 --- a/homeassistant/components/homekit/util.py +++ b/homeassistant/components/homekit/util.py @@ -1,6 +1,7 @@ """Collection of useful functions for the HomeKit component.""" from collections import OrderedDict, namedtuple import io +import ipaddress import logging import os import secrets @@ -23,11 +24,28 @@ import homeassistant.util.temperature as temp_util from .const import ( + CONF_AUDIO_MAP, + CONF_AUDIO_PACKET_SIZE, CONF_FEATURE, CONF_FEATURE_LIST, CONF_LINKED_BATTERY_SENSOR, CONF_LOW_BATTERY_THRESHOLD, + CONF_MAX_FPS, + CONF_MAX_HEIGHT, + CONF_MAX_WIDTH, + CONF_STREAM_ADDRESS, + CONF_STREAM_SOURCE, + CONF_SUPPORT_AUDIO, + CONF_VIDEO_MAP, + CONF_VIDEO_PACKET_SIZE, + DEFAULT_AUDIO_MAP, + DEFAULT_AUDIO_PACKET_SIZE, DEFAULT_LOW_BATTERY_THRESHOLD, + DEFAULT_MAX_FPS, + DEFAULT_MAX_HEIGHT, + DEFAULT_MAX_WIDTH, + DEFAULT_VIDEO_MAP, + DEFAULT_VIDEO_PACKET_SIZE, DOMAIN, FEATURE_ON_OFF, FEATURE_PLAY_PAUSE, @@ -62,6 +80,25 @@ {vol.Optional(CONF_FEATURE_LIST, default=None): cv.ensure_list} ) +CAMERA_SCHEMA = BASIC_INFO_SCHEMA.extend( + { + vol.Optional(CONF_STREAM_ADDRESS): vol.All(ipaddress.ip_address, cv.string), + vol.Optional(CONF_STREAM_SOURCE): cv.string, + vol.Optional(CONF_SUPPORT_AUDIO, default=False): cv.boolean, + vol.Optional(CONF_MAX_WIDTH, default=DEFAULT_MAX_WIDTH): cv.positive_int, + vol.Optional(CONF_MAX_HEIGHT, default=DEFAULT_MAX_HEIGHT): cv.positive_int, + vol.Optional(CONF_MAX_FPS, default=DEFAULT_MAX_FPS): cv.positive_int, + vol.Optional(CONF_AUDIO_MAP, default=DEFAULT_AUDIO_MAP): cv.string, + vol.Optional(CONF_VIDEO_MAP, default=DEFAULT_VIDEO_MAP): cv.string, + vol.Optional( + CONF_AUDIO_PACKET_SIZE, default=DEFAULT_AUDIO_PACKET_SIZE + ): cv.positive_int, + vol.Optional( + CONF_VIDEO_PACKET_SIZE, default=DEFAULT_VIDEO_PACKET_SIZE + ): cv.positive_int, + } +) + CODE_SCHEMA = BASIC_INFO_SCHEMA.extend( {vol.Optional(ATTR_CODE, default=None): vol.Any(None, cv.string)} ) @@ -101,6 +138,40 @@ ) +HOMEKIT_CHAR_TRANSLATIONS = { + 0: " ", # nul + 10: " ", # nl + 13: " ", # cr + 33: "-", # ! + 34: " ", # " + 36: "-", # $ + 37: "-", # % + 40: "-", # ( + 41: "-", # ) + 42: "-", # * + 43: "-", # + + 47: "-", # / + 58: "-", # : + 59: "-", # ; + 60: "-", # < + 61: "-", # = + 62: "-", # > + 63: "-", # ? + 64: "-", # @ + 91: "-", # [ + 92: "-", # \ + 93: "-", # ] + 94: "-", # ^ + 95: " ", # _ + 96: "-", # ` + 123: "-", # { + 124: "-", # | + 125: "-", # } + 126: "-", # ~ + 127: "-", # del +} + + def validate_entity_config(values): """Validate config entry for CONF_ENTITY.""" if not isinstance(values, dict): @@ -128,6 +199,9 @@ def validate_entity_config(values): feature_list[key] = params config[CONF_FEATURE_LIST] = feature_list + elif domain == "camera": + config = CAMERA_SCHEMA(config) + elif domain == "switch": config = SWITCH_TYPE_SCHEMA(config) @@ -138,8 +212,8 @@ def validate_entity_config(values): return entities -def validate_media_player_features(state, feature_list): - """Validate features for media players.""" +def get_media_player_features(state): + """Determine features for media players.""" features = state.attributes.get(ATTR_SUPPORTED_FEATURES, 0) supported_modes = [] @@ -153,6 +227,20 @@ def validate_media_player_features(state, feature_list): supported_modes.append(FEATURE_PLAY_STOP) if features & media_player.const.SUPPORT_VOLUME_MUTE: supported_modes.append(FEATURE_TOGGLE_MUTE) + return supported_modes + + +def validate_media_player_features(state, feature_list): + """Validate features for media players.""" + supported_modes = get_media_player_features(state) + + if not supported_modes: + _LOGGER.error("%s does not support any media_player features", state.entity_id) + return False + + if not feature_list: + # Auto detected + return True error_list = [] for feature in feature_list: @@ -160,7 +248,9 @@ def validate_media_player_features(state, feature_list): error_list.append(feature) if error_list: - _LOGGER.error("%s does not support features: %s", state.entity_id, error_list) + _LOGGER.error( + "%s does not support media_player features: %s", state.entity_id, error_list + ) return False return True @@ -252,6 +342,16 @@ def convert_to_float(state): return None +def cleanup_name_for_homekit(name): + """Ensure the name of the device will not crash homekit.""" + # + # This is not a security measure. + # + # UNICODE_EMOJI is also not allowed but that + # likely isn't a problem + return name.translate(HOMEKIT_CHAR_TRANSLATIONS) + + def temperature_to_homekit(temperature, unit): """Convert temperature to Celsius for HomeKit.""" return round(temp_util.convert(temperature, unit, TEMP_CELSIUS), 1) diff --git a/homeassistant/components/homekit_controller/translations/es-419.json b/homeassistant/components/homekit_controller/translations/es-419.json index f3a084e7545e7b..a10c6eaa04fe46 100644 --- a/homeassistant/components/homekit_controller/translations/es-419.json +++ b/homeassistant/components/homekit_controller/translations/es-419.json @@ -1,10 +1,19 @@ { "config": { "abort": { + "accessory_not_found_error": "No se puede agregar el emparejamiento ya que el dispositivo ya no se puede encontrar.", "already_configured": "El accesorio ya est\u00e1 configurado con este controlador.", - "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo." + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", + "already_paired": "Este accesorio ya est\u00e1 emparejado con otro dispositivo. Por favor, reinicie el accesorio y vuelva a intentarlo.", + "ignored_model": "El soporte de HomeKit para este modelo est\u00e1 bloqueado ya que hay disponible una caracter\u00edstica m\u00e1s completa de integraci\u00f3n nativa.", + "invalid_config_entry": "Este dispositivo se muestra como listo para emparejar, pero ya hay una entrada de configuraci\u00f3n conflictiva en Home Assistant que primero debe eliminarse.", + "no_devices": "No se encontraron dispositivos no emparejados" }, "error": { + "authentication_error": "C\u00f3digo de HomeKit incorrecto. Por favor revisalo e int\u00e9ntalo de nuevo.", + "busy_error": "El dispositivo se neg\u00f3 a agregar el emparejamiento ya que ya se est\u00e1 emparejando con otro controlador.", + "max_peers_error": "El dispositivo se neg\u00f3 a agregar emparejamiento ya que no tiene almacenamiento de emparejamiento libre.", + "max_tries_error": "El dispositivo se neg\u00f3 a agregar el emparejamiento ya que recibi\u00f3 m\u00e1s de 100 intentos de autenticaci\u00f3n fallidos.", "pairing_failed": "Se produjo un error no controlado al intentar vincularse con este dispositivo. Esto puede ser una falla temporal o su dispositivo puede no ser compatible actualmente.", "unable_to_pair": "No se puede vincular, por favor intente nuevamente.", "unknown_error": "El dispositivo inform\u00f3 un error desconocido. Vinculaci\u00f3n fallida." diff --git a/homeassistant/components/homekit_controller/translations/zh-Hans.json b/homeassistant/components/homekit_controller/translations/zh-Hans.json index 4b58e4f9fb6fe2..a8360161c6a97a 100644 --- a/homeassistant/components/homekit_controller/translations/zh-Hans.json +++ b/homeassistant/components/homekit_controller/translations/zh-Hans.json @@ -3,6 +3,7 @@ "abort": { "accessory_not_found_error": "\u65e0\u6cd5\u6dfb\u52a0\u914d\u5bf9\uff0c\u56e0\u4e3a\u65e0\u6cd5\u518d\u627e\u5230\u8bbe\u5907\u3002", "already_configured": "\u914d\u4ef6\u5df2\u901a\u8fc7\u6b64\u63a7\u5236\u5668\u914d\u7f6e\u5b8c\u6210\u3002", + "already_in_progress": "\u6b64\u8bbe\u5907\u7684\u914d\u7f6e\u6d41\u7a0b\u5df2\u5728\u8fdb\u884c\u4e2d\u3002", "already_paired": "\u6b64\u914d\u4ef6\u5df2\u4e0e\u53e6\u4e00\u53f0\u8bbe\u5907\u914d\u5bf9\u3002\u8bf7\u91cd\u7f6e\u914d\u4ef6\uff0c\u7136\u540e\u91cd\u8bd5\u3002", "ignored_model": "HomeKit \u5bf9\u6b64\u8bbe\u5907\u7684\u652f\u6301\u5df2\u88ab\u963b\u6b62\uff0c\u56e0\u4e3a\u6709\u529f\u80fd\u66f4\u5b8c\u6574\u7684\u539f\u751f\u96c6\u6210\u53ef\u4ee5\u4f7f\u7528\u3002", "invalid_config_entry": "\u6b64\u8bbe\u5907\u5df2\u51c6\u5907\u597d\u914d\u5bf9\uff0c\u4f46\u662f Home Assistant \u4e2d\u5b58\u5728\u4e0e\u4e4b\u51b2\u7a81\u7684\u914d\u7f6e\uff0c\u5fc5\u987b\u5148\u5c06\u5176\u5220\u9664\u3002", diff --git a/homeassistant/components/homematicip_cloud/translations/es-419.json b/homeassistant/components/homematicip_cloud/translations/es-419.json index a853d7677c8e63..1b743f1c51f0ad 100644 --- a/homeassistant/components/homematicip_cloud/translations/es-419.json +++ b/homeassistant/components/homematicip_cloud/translations/es-419.json @@ -8,7 +8,8 @@ "error": { "invalid_pin": "PIN no v\u00e1lido, por favor intente de nuevo.", "press_the_button": "Por favor, presione el bot\u00f3n azul.", - "register_failed": "No se pudo registrar, por favor intente de nuevo." + "register_failed": "No se pudo registrar, por favor intente de nuevo.", + "timeout_button": "Tiempo de espera del bot\u00f3n azul, intente nuevamente." }, "step": { "init": { @@ -20,7 +21,8 @@ "title": "Elija el punto de acceso HomematicIP" }, "link": { - "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)" + "description": "Presione el bot\u00f3n azul en el punto de acceso y el bot\u00f3n enviar para registrar HomematicIP con Home Assistant. \n\n ! [Ubicaci\u00f3n del bot\u00f3n en el puente] (/static/images/config_flows/config_homematicip_cloud.png)", + "title": "Enlazar Punto de acceso" } } } diff --git a/homeassistant/components/homematicip_cloud/translations/es.json b/homeassistant/components/homematicip_cloud/translations/es.json index a017a5a1df7ba6..b1d1ea64e3fb36 100644 --- a/homeassistant/components/homematicip_cloud/translations/es.json +++ b/homeassistant/components/homematicip_cloud/translations/es.json @@ -21,7 +21,7 @@ "title": "Elegir punto de acceso HomematicIP" }, "link": { - "description": "Pulsa el bot\u00f3n azul en el punto de acceso y el bot\u00f3n de env\u00edo para registrar HomematicIP en Home Assistant.\n\n![Ubicaci\u00f3n del bot\u00f3n en el puente](/static/images/config_flows/config_homematicip_cloud.png)", + "description": "Pulsa el bot\u00f3n azul en el punto de acceso y el bot\u00f3n de env\u00edo para registrar HomematicIP en Home Assistant.\n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_flows/config_homematicip_cloud.png)", "title": "Enlazar punto de acceso" } } diff --git a/homeassistant/components/huawei_lte/translations/es-419.json b/homeassistant/components/huawei_lte/translations/es-419.json new file mode 100644 index 00000000000000..a00f805c9fa111 --- /dev/null +++ b/homeassistant/components/huawei_lte/translations/es-419.json @@ -0,0 +1,42 @@ +{ + "config": { + "abort": { + "already_configured": "Este dispositivo ya ha sido configurado", + "already_in_progress": "Este dispositivo ya est\u00e1 siendo configurado", + "not_huawei_lte": "No es un dispositivo Huawei LTE" + }, + "error": { + "connection_failed": "La conexi\u00f3n fall\u00f3", + "connection_timeout": "El tiempo de conexi\u00f3n expir\u00f3", + "incorrect_password": "Contrase\u00f1a incorrecta", + "incorrect_username": "Nombre de usuario incorrecto", + "incorrect_username_or_password": "Nombre de usuario o contrase\u00f1a incorrecta", + "invalid_url": "URL invalida", + "login_attempts_exceeded": "Se han excedido los intentos de inicio de sesi\u00f3n m\u00e1ximos. Vuelva a intentarlo m\u00e1s tarde", + "response_error": "Error desconocido del dispositivo", + "unknown_connection_error": "Error desconocido al conectarse al dispositivo" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "url": "URL", + "username": "Nombre de usuario" + }, + "description": "Ingrese los detalles de acceso del dispositivo. Especificar nombre de usuario y contrase\u00f1a es opcional, pero habilita el soporte para m\u00e1s funciones de integraci\u00f3n. Por otro lado, el uso de una conexi\u00f3n autorizada puede causar problemas para acceder a la interfaz web del dispositivo desde fuera de Home Assistant mientras la integraci\u00f3n est\u00e1 activa y viceversa.", + "title": "Configurar Huawei LTE" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "name": "Nombre del servicio de notificaci\u00f3n (el cambio requiere reiniciar)", + "recipient": "Destinatarios de notificaciones por SMS", + "track_new_devices": "Rastrear nuevos dispositivos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hue/light.py b/homeassistant/components/hue/light.py index 7b5b7e6e804a14..c9d543dba94cf7 100644 --- a/homeassistant/components/hue/light.py +++ b/homeassistant/components/hue/light.py @@ -259,6 +259,9 @@ def brightness(self): else: bri = self.light.state.get("bri") + if bri is None: + return bri + return hue_brightness_to_hass(bri) @property diff --git a/homeassistant/components/hue/translations/es-419.json b/homeassistant/components/hue/translations/es-419.json index 8b2b90456bd84f..ec81c24e2cb5f0 100644 --- a/homeassistant/components/hue/translations/es-419.json +++ b/homeassistant/components/hue/translations/es-419.json @@ -3,6 +3,7 @@ "abort": { "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", "already_configured": "El puente ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en progreso.", "cannot_connect": "No se puede conectar al puente", "discover_timeout": "Incapaz de descubrir puentes Hue", "no_bridges": "No se descubrieron puentes Philips Hue", @@ -17,8 +18,34 @@ "init": { "data": { "host": "Host" - } + }, + "title": "Seleccione el puente Hue" + }, + "link": { + "description": "Presione el bot\u00f3n en el puente para registrar Philips Hue con Home Assistant. \n\n![Location of button on bridge](/static/images/config_philips_hue.jpg)", + "title": "Enlazar Hub" } } + }, + "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": "Aumentar intensidad", + "double_buttons_1_3": "Botones primero y tercero", + "double_buttons_2_4": "Botones segundo y cuarto", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "remote_button_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_short_press": "Se presion\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_short_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_double_button_long_press": "Ambos \"{subtype}\" sueltos despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_double_button_short_press": "Ambos \"{subtype}\" sueltos" + } } } \ No newline at end of file diff --git a/homeassistant/components/hue/translations/es.json b/homeassistant/components/hue/translations/es.json index ddaad5c424a45a..f47625604859fa 100644 --- a/homeassistant/components/hue/translations/es.json +++ b/homeassistant/components/hue/translations/es.json @@ -1,13 +1,13 @@ { "config": { "abort": { - "all_configured": "Todos los puentes Philips Hue ya est\u00e1n configurados", - "already_configured": "El puente ya esta configurado", - "already_in_progress": "El flujo de configuraci\u00f3n para el puente ya est\u00e1 en curso.", - "cannot_connect": "No se puede conectar al puente", - "discover_timeout": "No se han descubierto puentes Philips Hue", - "no_bridges": "No se han descubierto puentes Philips Hue.", - "not_hue_bridge": "No es un puente Hue", + "all_configured": "Ya se han configurado todas las pasarelas Philips Hue", + "already_configured": "La pasarela ya esta configurada", + "already_in_progress": "La configuraci\u00f3n del flujo para la pasarela ya est\u00e1 en curso.", + "cannot_connect": "No se puede conectar a la pasarela", + "discover_timeout": "Imposible encontrar pasarelas Philips Hue", + "no_bridges": "No se han encontrado pasarelas Philips Hue.", + "not_hue_bridge": "No es una pasarela Hue", "unknown": "Se produjo un error desconocido" }, "error": { @@ -19,10 +19,10 @@ "data": { "host": "Host" }, - "title": "Elige el puente de Hue" + "title": "Elige la pasarela Hue" }, "link": { - "description": "Presione el bot\u00f3n en el puente para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en el puente](/static/images/config_philips_hue.jpg)", + "description": "Presione el bot\u00f3n en la pasarela para registrar Philips Hue con Home Assistant. \n\n![Ubicaci\u00f3n del bot\u00f3n en la pasarela](/static/images/config_philips_hue.jpg)", "title": "Link Hub" } } diff --git a/homeassistant/components/hunterdouglas_powerview/cover.py b/homeassistant/components/hunterdouglas_powerview/cover.py index 8364b3273ca13e..e14142677e34a2 100644 --- a/homeassistant/components/hunterdouglas_powerview/cover.py +++ b/homeassistant/components/hunterdouglas_powerview/cover.py @@ -161,7 +161,7 @@ async def async_stop_cover(self, **kwargs): self._async_update_from_command(await self._shade.stop()) await self._async_force_refresh_state() - async def set_cover_position(self, **kwargs): + async def async_set_cover_position(self, **kwargs): """Move the shade to a specific position.""" if ATTR_POSITION not in kwargs: return diff --git a/homeassistant/components/hunterdouglas_powerview/translations/es-419.json b/homeassistant/components/hunterdouglas_powerview/translations/es-419.json new file mode 100644 index 00000000000000..c4a26d7d35d97a --- /dev/null +++ b/homeassistant/components/hunterdouglas_powerview/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "link": { + "description": "\u00bfDesea configurar {name} ({host})?", + "title": "Conectar a Powerview Hub" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP" + }, + "title": "Conectar a Powerview Hub" + } + } + }, + "title": "Hunter Douglas PowerView" +} \ No newline at end of file diff --git a/homeassistant/components/iaqualink/translations/es-419.json b/homeassistant/components/iaqualink/translations/es-419.json index 170c2851d0801b..9d43bfa7e57db2 100644 --- a/homeassistant/components/iaqualink/translations/es-419.json +++ b/homeassistant/components/iaqualink/translations/es-419.json @@ -1,12 +1,19 @@ { "config": { + "abort": { + "already_setup": "Solo puede configurar una \u00fanica conexi\u00f3n iAqualink." + }, + "error": { + "connection_failure": "No se puede conectar a iAqualink. Verifice su nombre de usuario y contrase\u00f1a." + }, "step": { "user": { "data": { "password": "Contrase\u00f1a", "username": "Nombre de usuario / direcci\u00f3n de correo electr\u00f3nico" }, - "description": "Por favor, Ingrese el nombre de usuario y la contrase\u00f1a para su cuenta de iAqualink." + "description": "Por favor, Ingrese el nombre de usuario y la contrase\u00f1a para su cuenta de iAqualink.", + "title": "Conectar a iAqualink" } } } diff --git a/homeassistant/components/icloud/account.py b/homeassistant/components/icloud/account.py index e0d9a6086055a3..d039b270bb8591 100644 --- a/homeassistant/components/icloud/account.py +++ b/homeassistant/components/icloud/account.py @@ -186,7 +186,7 @@ def update_devices(self) -> None: DEVICE_STATUS_CODES.get(list(api_devices)[0][DEVICE_STATUS]) == "pending" and not self._retried_fetch ): - _LOGGER.warning("Pending devices, trying again in 15s") + _LOGGER.debug("Pending devices, trying again in 15s") self._fetch_interval = 0.25 self._retried_fetch = True else: diff --git a/homeassistant/components/icloud/manifest.json b/homeassistant/components/icloud/manifest.json index 2b8bc2fccaeda3..40b58cbf2d0ae0 100644 --- a/homeassistant/components/icloud/manifest.json +++ b/homeassistant/components/icloud/manifest.json @@ -3,6 +3,6 @@ "name": "Apple iCloud", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/icloud", - "requirements": ["pyicloud==0.9.6.1"], + "requirements": ["pyicloud==0.9.7"], "codeowners": ["@Quentame"] } diff --git a/homeassistant/components/icloud/translations/es-419.json b/homeassistant/components/icloud/translations/es-419.json new file mode 100644 index 00000000000000..90fca1454cfdbf --- /dev/null +++ b/homeassistant/components/icloud/translations/es-419.json @@ -0,0 +1,38 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada", + "no_device": "Ninguno de sus dispositivos tiene activado \"Buscar mi iPhone\"" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su correo electr\u00f3nico y contrase\u00f1a", + "send_verification_code": "Error al enviar el c\u00f3digo de verificaci\u00f3n", + "validate_verification_code": "No se pudo verificar su c\u00f3digo de verificaci\u00f3n, elija un dispositivo de confianza y comience la verificaci\u00f3n nuevamente" + }, + "step": { + "trusted_device": { + "data": { + "trusted_device": "Dispositivo de confianza" + }, + "description": "Selecciona tu dispositivo de confianza", + "title": "Dispositivo de confianza iCloud" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Correo electr\u00f3nico", + "with_family": "Con la familia" + }, + "description": "Ingrese sus credenciales", + "title": "Credenciales de iCloud" + }, + "verification_code": { + "data": { + "verification_code": "C\u00f3digo de verificaci\u00f3n" + }, + "description": "Ingrese el c\u00f3digo de verificaci\u00f3n que acaba de recibir de iCloud", + "title": "C\u00f3digo de verificaci\u00f3n de iCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ifttt/translations/es-419.json b/homeassistant/components/ifttt/translations/es-419.json index 097ecad5b792ee..4b648d64279cda 100644 --- a/homeassistant/components/ifttt/translations/es-419.json +++ b/homeassistant/components/ifttt/translations/es-419.json @@ -9,7 +9,8 @@ }, "step": { "user": { - "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?" + "description": "\u00bfEst\u00e1s seguro de que quieres configurar IFTTT?", + "title": "Configurar el Applet de webhook IFTTT" } } } diff --git a/homeassistant/components/influxdb/__init__.py b/homeassistant/components/influxdb/__init__.py index 922a0197cf1600..0d1999e0d7bee0 100644 --- a/homeassistant/components/influxdb/__init__.py +++ b/homeassistant/components/influxdb/__init__.py @@ -17,6 +17,7 @@ CONF_HOST, CONF_INCLUDE, CONF_PASSWORD, + CONF_PATH, CONF_PORT, CONF_SSL, CONF_USERNAME, @@ -83,6 +84,7 @@ } ), vol.Optional(CONF_DB_NAME, default=DEFAULT_DATABASE): cv.string, + vol.Optional(CONF_PATH): cv.string, vol.Optional(CONF_PORT): cv.port, vol.Optional(CONF_SSL): cv.boolean, vol.Optional(CONF_RETRY_COUNT, default=0): cv.positive_int, @@ -131,6 +133,9 @@ def setup(hass, config): if CONF_HOST in conf: kwargs["host"] = conf[CONF_HOST] + if CONF_PATH in conf: + kwargs["path"] = conf[CONF_PATH] + if CONF_PORT in conf: kwargs["port"] = conf[CONF_PORT] diff --git a/homeassistant/components/ios/translations/no.json b/homeassistant/components/ios/translations/no.json index 5645bd91e2ab10..826df14b1e35c8 100644 --- a/homeassistant/components/ios/translations/no.json +++ b/homeassistant/components/ios/translations/no.json @@ -5,7 +5,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 konfigurere Home Assistant iOS-komponenten?", + "description": "\u00d8nsker du \u00e5 sette opp Home Assistant iOS-komponenten?", "title": "Home Assistant iOS" } } diff --git a/homeassistant/components/ipma/translations/es-419.json b/homeassistant/components/ipma/translations/es-419.json index 7b319c66a49a93..a3f83c150e7073 100644 --- a/homeassistant/components/ipma/translations/es-419.json +++ b/homeassistant/components/ipma/translations/es-419.json @@ -8,6 +8,7 @@ "data": { "latitude": "Latitud", "longitude": "Longitud", + "mode": "Modo", "name": "Nombre" }, "description": "Instituto Portugu\u00eas do Mar e Atmosfera", diff --git a/homeassistant/components/ipp/translations/es-419.json b/homeassistant/components/ipp/translations/es-419.json new file mode 100644 index 00000000000000..eb9c21eb28c377 --- /dev/null +++ b/homeassistant/components/ipp/translations/es-419.json @@ -0,0 +1,34 @@ +{ + "config": { + "abort": { + "already_configured": "Esta impresora ya est\u00e1 configurada.", + "connection_error": "No se pudo conectar a la impresora.", + "connection_upgrade": "No se pudo conectar a la impresora debido a que se requiere una actualizaci\u00f3n de la conexi\u00f3n.", + "ipp_error": "Error de IPP encontrado.", + "ipp_version_error": "La versi\u00f3n IPP no es compatible con la impresora.", + "parse_error": "Error al analizar la respuesta de la impresora." + }, + "error": { + "connection_error": "No se pudo conectar a la impresora.", + "connection_upgrade": "No se pudo conectar a la impresora. Intente nuevamente 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": "Configure su impresora a trav\u00e9s del Protocolo de impresi\u00f3n de Internet (IPP) para integrarse con Home Assistant.", + "title": "Enlace su impresora" + }, + "zeroconf_confirm": { + "description": "\u00bfDesea agregar la impresora llamada `{name}` a Home Assistant?", + "title": "Impresora descubierta" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/ipp/translations/no.json b/homeassistant/components/ipp/translations/no.json index 10ab960272b136..fa5e4c86a891f5 100644 --- a/homeassistant/components/ipp/translations/no.json +++ b/homeassistant/components/ipp/translations/no.json @@ -22,7 +22,7 @@ "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.", + "description": "Sett opp skriveren din via Internet Printing Protocol (IPP) for \u00e5 integrere med Home Assistant.", "title": "Koble til skriveren din" }, "zeroconf_confirm": { diff --git a/homeassistant/components/iqvia/manifest.json b/homeassistant/components/iqvia/manifest.json index 154cdd43c657e4..0acbecddf8d1bf 100644 --- a/homeassistant/components/iqvia/manifest.json +++ b/homeassistant/components/iqvia/manifest.json @@ -3,6 +3,6 @@ "name": "IQVIA", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/iqvia", - "requirements": ["numpy==1.18.2", "pyiqvia==0.2.1"], + "requirements": ["numpy==1.18.4", "pyiqvia==0.2.1"], "codeowners": ["@bachya"] } diff --git a/homeassistant/components/iqvia/translations/es-419.json b/homeassistant/components/iqvia/translations/es-419.json index b107e1bb696df0..21b5273bfba1b7 100644 --- a/homeassistant/components/iqvia/translations/es-419.json +++ b/homeassistant/components/iqvia/translations/es-419.json @@ -1,13 +1,16 @@ { "config": { "error": { + "identifier_exists": "C\u00f3digo postal ya registrado", "invalid_zip_code": "El c\u00f3digo postal no es v\u00e1lido" }, "step": { "user": { "data": { "zip_code": "C\u00f3digo postal" - } + }, + "description": "Complete su c\u00f3digo postal de EE. UU. o Canad\u00e1.", + "title": "IQVIA" } } } diff --git a/homeassistant/components/islamic_prayer_times/translations/es-419.json b/homeassistant/components/islamic_prayer_times/translations/es-419.json new file mode 100644 index 00000000000000..2a5105a405466c --- /dev/null +++ b/homeassistant/components/islamic_prayer_times/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "one_instance_allowed": "Solo una instancia es necesaria." + }, + "step": { + "user": { + "description": "\u00bfDesea establecer tiempos de oraci\u00f3n isl\u00e1mica?", + "title": "Establecer tiempos de oraci\u00f3n isl\u00e1mica" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "calculation_method": "M\u00e9todo de c\u00e1lculo de la oraci\u00f3n" + } + } + } + }, + "title": "Tiempos de oraci\u00f3n isl\u00e1mica" +} \ No newline at end of file diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index f0766c4e4f9b47..8076debc2d1d6e 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -1,10 +1,7 @@ """Support the ISY-994 controllers.""" -from collections import namedtuple -import logging from urllib.parse import urlparse import PyISY -from PyISY.Nodes import Group import voluptuous as vol from homeassistant.const import ( @@ -12,29 +9,27 @@ CONF_PASSWORD, CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, - UNIT_PERCENTAGE, ) from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv, discovery -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.typing import ConfigType, Dict - -_LOGGER = logging.getLogger(__name__) - -DOMAIN = "isy994" - -CONF_IGNORE_STRING = "ignore_string" -CONF_SENSOR_STRING = "sensor_string" -CONF_ENABLE_CLIMATE = "enable_climate" -CONF_TLS_VER = "tls" - -DEFAULT_IGNORE_STRING = "{IGNORE ME}" -DEFAULT_SENSOR_STRING = "sensor" - -KEY_ACTIONS = "actions" -KEY_FOLDER = "folder" -KEY_MY_PROGRAMS = "My Programs" -KEY_STATUS = "status" +from homeassistant.helpers.typing import ConfigType + +from .const import ( + _LOGGER, + CONF_ENABLE_CLIMATE, + CONF_IGNORE_STRING, + CONF_SENSOR_STRING, + CONF_TLS_VER, + DEFAULT_IGNORE_STRING, + DEFAULT_SENSOR_STRING, + DOMAIN, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_WEATHER, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, +) +from .helpers import _categorize_nodes, _categorize_programs, _categorize_weather CONFIG_SCHEMA = vol.Schema( { @@ -57,353 +52,18 @@ extra=vol.ALLOW_EXTRA, ) -# Do not use the Home Assistant consts for the states here - we're matching -# exact API responses, not using them for Home Assistant states -NODE_FILTERS = { - "binary_sensor": { - "uom": [], - "states": [], - "node_def_id": ["BinaryAlarm", "BinaryAlarm_ADV"], - "insteon_type": ["16."], # Does a startswith() match; include the dot - }, - "sensor": { - # This is just a more-readable way of including MOST uoms between 1-100 - # (Remember that range() is non-inclusive of the stop value) - "uom": ( - ["1"] - + list(map(str, range(3, 11))) - + list(map(str, range(12, 51))) - + list(map(str, range(52, 66))) - + list(map(str, range(69, 78))) - + ["79"] - + list(map(str, range(82, 97))) - ), - "states": [], - "node_def_id": ["IMETER_SOLO"], - "insteon_type": ["9.0.", "9.7."], - }, - "lock": { - "uom": ["11"], - "states": ["locked", "unlocked"], - "node_def_id": ["DoorLock"], - "insteon_type": ["15."], - }, - "fan": { - "uom": [], - "states": ["off", "low", "med", "high"], - "node_def_id": ["FanLincMotor"], - "insteon_type": ["1.46."], - }, - "cover": { - "uom": ["97"], - "states": ["open", "closed", "closing", "opening", "stopped"], - "node_def_id": [], - "insteon_type": [], - }, - "light": { - "uom": ["51"], - "states": ["on", "off", UNIT_PERCENTAGE], - "node_def_id": [ - "DimmerLampSwitch", - "DimmerLampSwitch_ADV", - "DimmerSwitchOnly", - "DimmerSwitchOnly_ADV", - "DimmerLampOnly", - "BallastRelayLampSwitch", - "BallastRelayLampSwitch_ADV", - "RemoteLinc2", - "RemoteLinc2_ADV", - "KeypadDimmer", - "KeypadDimmer_ADV", - ], - "insteon_type": ["1."], - }, - "switch": { - "uom": ["2", "78"], - "states": ["on", "off"], - "node_def_id": [ - "OnOffControl", - "RelayLampSwitch", - "RelayLampSwitch_ADV", - "RelaySwitchOnlyPlusQuery", - "RelaySwitchOnlyPlusQuery_ADV", - "RelayLampOnly", - "RelayLampOnly_ADV", - "KeypadButton", - "KeypadButton_ADV", - "EZRAIN_Input", - "EZRAIN_Output", - "EZIO2x4_Input", - "EZIO2x4_Input_ADV", - "BinaryControl", - "BinaryControl_ADV", - "AlertModuleSiren", - "AlertModuleSiren_ADV", - "AlertModuleArmed", - "Siren", - "Siren_ADV", - "X10", - "KeypadRelay", - "KeypadRelay_ADV", - ], - "insteon_type": ["2.", "9.10.", "9.11.", "113."], - }, -} - -SUPPORTED_DOMAINS = [ - "binary_sensor", - "sensor", - "lock", - "fan", - "cover", - "light", - "switch", -] -SUPPORTED_PROGRAM_DOMAINS = ["binary_sensor", "lock", "fan", "cover", "switch"] - -# ISY Scenes are more like Switches than Home Assistant Scenes -# (they can turn off, and report their state) -SCENE_DOMAIN = "switch" - -ISY994_NODES = "isy994_nodes" -ISY994_WEATHER = "isy994_weather" -ISY994_PROGRAMS = "isy994_programs" - -WeatherNode = namedtuple("WeatherNode", ("status", "name", "uom")) - - -def _check_for_node_def(hass: HomeAssistant, node, single_domain: str = None) -> bool: - """Check if the node matches the node_def_id for any domains. - - This is only present on the 5.0 ISY firmware, and is the most reliable - way to determine a device's type. - """ - if not hasattr(node, "node_def_id") or node.node_def_id is None: - # Node doesn't have a node_def (pre 5.0 firmware most likely) - return False - - node_def_id = node.node_def_id - - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_def_id in NODE_FILTERS[domain]["node_def_id"]: - hass.data[ISY994_NODES][domain].append(node) - return True - - _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) - return False - - -def _check_for_insteon_type( - hass: HomeAssistant, node, single_domain: str = None -) -> bool: - """Check if the node matches the Insteon type for any domains. - - This is for (presumably) every version of the ISY firmware, but only - works for Insteon device. "Node Server" (v5+) and Z-Wave and others will - not have a type. - """ - if not hasattr(node, "type") or node.type is None: - # Node doesn't have a type (non-Insteon device most likely) - return False - - device_type = node.type - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if any( - [ - device_type.startswith(t) - for t in set(NODE_FILTERS[domain]["insteon_type"]) - ] - ): - - # Hacky special-case just for FanLinc, which has a light module - # as one of its nodes. Note that this special-case is not necessary - # on ISY 5.x firmware as it uses the superior NodeDefs method - if domain == "fan" and int(node.nid[-1]) == 1: - hass.data[ISY994_NODES]["light"].append(node) - return True - - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_uom_id( - hass: HomeAssistant, node, single_domain: str = None, uom_list: list = None -) -> bool: - """Check if a node's uom matches any of the domains uom filter. - - This is used for versions of the ISY firmware that report uoms as a single - ID. We can often infer what type of device it is by that ID. - """ - if not hasattr(node, "uom") or node.uom is None: - # Node doesn't have a uom (Scenes for example) - return False - - node_uom = set(map(str.lower, node.uom)) - - if uom_list: - if node_uom.intersection(uom_list): - hass.data[ISY994_NODES][single_domain].append(node) - return True - else: - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_uom.intersection(NODE_FILTERS[domain]["uom"]): - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _check_for_states_in_uom( - hass: HomeAssistant, node, single_domain: str = None, states_list: list = None -) -> bool: - """Check if a list of uoms matches two possible filters. - - This is for versions of the ISY firmware that report uoms as a list of all - possible "human readable" states. This filter passes if all of the possible - states fit inside the given filter. - """ - if not hasattr(node, "uom") or node.uom is None: - # Node doesn't have a uom (Scenes for example) - return False - - node_uom = set(map(str.lower, node.uom)) - - if states_list: - if node_uom == set(states_list): - hass.data[ISY994_NODES][single_domain].append(node) - return True - else: - domains = SUPPORTED_DOMAINS if not single_domain else [single_domain] - for domain in domains: - if node_uom == set(NODE_FILTERS[domain]["states"]): - hass.data[ISY994_NODES][domain].append(node) - return True - - return False - - -def _is_sensor_a_binary_sensor(hass: HomeAssistant, node) -> bool: - """Determine if the given sensor node should be a binary_sensor.""" - if _check_for_node_def(hass, node, single_domain="binary_sensor"): - return True - if _check_for_insteon_type(hass, node, single_domain="binary_sensor"): - return True - - # For the next two checks, we're providing our own set of uoms that - # represent on/off devices. This is because we can only depend on these - # checks in the context of already knowing that this is definitely a - # sensor device. - if _check_for_uom_id( - hass, node, single_domain="binary_sensor", uom_list=["2", "78"] - ): - return True - if _check_for_states_in_uom( - hass, node, single_domain="binary_sensor", states_list=["on", "off"] - ): - return True - - return False - - -def _categorize_nodes( - hass: HomeAssistant, nodes, ignore_identifier: str, sensor_identifier: str -) -> None: - """Sort the nodes to their proper domains.""" - for (path, node) in nodes: - ignored = ignore_identifier in path or ignore_identifier in node.name - if ignored: - # Don't import this node as a device at all - continue - - if isinstance(node, Group): - hass.data[ISY994_NODES][SCENE_DOMAIN].append(node) - continue - - if sensor_identifier in path or sensor_identifier in node.name: - # User has specified to treat this as a sensor. First we need to - # determine if it should be a binary_sensor. - if _is_sensor_a_binary_sensor(hass, node): - continue - - hass.data[ISY994_NODES]["sensor"].append(node) - continue - - # We have a bunch of different methods for determining the device type, - # each of which works with different ISY firmware versions or device - # family. The order here is important, from most reliable to least. - if _check_for_node_def(hass, node): - continue - if _check_for_insteon_type(hass, node): - continue - if _check_for_uom_id(hass, node): - continue - if _check_for_states_in_uom(hass, node): - continue - - -def _categorize_programs(hass: HomeAssistant, programs: dict) -> None: - """Categorize the ISY994 programs.""" - for domain in SUPPORTED_PROGRAM_DOMAINS: - try: - folder = programs[KEY_MY_PROGRAMS][f"HA.{domain}"] - except KeyError: - pass - else: - for dtype, _, node_id in folder.children: - if dtype != KEY_FOLDER: - continue - entity_folder = folder[node_id] - try: - status = entity_folder[KEY_STATUS] - assert status.dtype == "program", "Not a program" - if domain != "binary_sensor": - actions = entity_folder[KEY_ACTIONS] - assert actions.dtype == "program", "Not a program" - else: - actions = None - except (AttributeError, KeyError, AssertionError): - _LOGGER.warning( - "Program entity '%s' not loaded due " - "to invalid folder structure.", - entity_folder.name, - ) - continue - - entity = (entity_folder.name, status, actions) - hass.data[ISY994_PROGRAMS][domain].append(entity) - - -def _categorize_weather(hass: HomeAssistant, climate) -> None: - """Categorize the ISY994 weather data.""" - climate_attrs = dir(climate) - weather_nodes = [ - WeatherNode( - getattr(climate, attr), - attr.replace("_", " "), - getattr(climate, f"{attr}_units"), - ) - for attr in climate_attrs - if f"{attr}_units" in climate_attrs - ] - hass.data[ISY994_WEATHER].extend(weather_nodes) - def setup(hass: HomeAssistant, config: ConfigType) -> bool: """Set up the ISY 994 platform.""" hass.data[ISY994_NODES] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_NODES][domain] = [] + for platform in SUPPORTED_PLATFORMS: + hass.data[ISY994_NODES][platform] = [] hass.data[ISY994_WEATHER] = [] hass.data[ISY994_PROGRAMS] = {} - for domain in SUPPORTED_DOMAINS: - hass.data[ISY994_PROGRAMS][domain] = [] + for platform in SUPPORTED_PROGRAM_PLATFORMS: + hass.data[ISY994_PROGRAMS][platform] = [] isy_config = config.get(DOMAIN) @@ -452,86 +112,8 @@ def stop(event: object) -> None: hass.bus.listen_once(EVENT_HOMEASSISTANT_STOP, stop) # Load platforms for the devices in the ISY controller that we support. - for component in SUPPORTED_DOMAINS: - discovery.load_platform(hass, component, DOMAIN, {}, config) + for platform in SUPPORTED_PLATFORMS: + discovery.load_platform(hass, platform, DOMAIN, {}, config) isy.auto_update = True return True - - -class ISYDevice(Entity): - """Representation of an ISY994 device.""" - - _attrs = {} - _name: str = None - - def __init__(self, node) -> None: - """Initialize the insteon device.""" - self._node = node - self._change_handler = None - self._control_handler = None - - async def async_added_to_hass(self) -> None: - """Subscribe to the node change events.""" - self._change_handler = self._node.status.subscribe("changed", self.on_update) - - if hasattr(self._node, "controlEvents"): - self._control_handler = self._node.controlEvents.subscribe(self.on_control) - - def on_update(self, event: object) -> None: - """Handle the update event from the ISY994 Node.""" - self.schedule_update_ha_state() - - def on_control(self, event: object) -> None: - """Handle a control event from the ISY994 Node.""" - self.hass.bus.fire( - "isy994_control", {"entity_id": self.entity_id, "control": event} - ) - - @property - def unique_id(self) -> str: - """Get the unique identifier of the device.""" - # pylint: disable=protected-access - if hasattr(self._node, "_id"): - return self._node._id - - return None - - @property - def name(self) -> str: - """Get the name of the device.""" - return self._name or str(self._node.name) - - @property - def should_poll(self) -> bool: - """No polling required since we're using the subscription.""" - return False - - @property - def value(self) -> int: - """Get the current value of the device.""" - # pylint: disable=protected-access - return self._node.status._val - - def is_unknown(self) -> bool: - """Get whether or not the value of this Entity's node is unknown. - - PyISY reports unknown values as -inf - """ - return self.value == -1 * float("inf") - - @property - def state(self): - """Return the state of the ISY device.""" - if self.is_unknown(): - return None - return super().state - - @property - def device_state_attributes(self) -> Dict: - """Get the state attributes for the device.""" - attr = {} - if hasattr(self._node, "aux_properties"): - for name, val in self._node.aux_properties.items(): - attr[name] = f"{val.get('value')} {val.get('uom')}" - return attr diff --git a/homeassistant/components/isy994/binary_sensor.py b/homeassistant/components/isy994/binary_sensor.py index a96f2f44fdbb9b..a7c9d1b7943cf1 100644 --- a/homeassistant/components/isy994/binary_sensor.py +++ b/homeassistant/components/isy994/binary_sensor.py @@ -1,24 +1,20 @@ """Support for ISY994 binary sensors.""" from datetime import timedelta -import logging from typing import Callable -from homeassistant.components.binary_sensor import DOMAIN, BinarySensorEntity +from homeassistant.components.binary_sensor import ( + DOMAIN as BINARY_SENSOR, + BinarySensorEntity, +) from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.core import callback from homeassistant.helpers.event import async_track_point_in_utc_time from homeassistant.helpers.typing import ConfigType from homeassistant.util import dt as dt_util -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -ISY_DEVICE_TYPES = { - "moisture": ["16.8", "16.13", "16.14"], - "opening": ["16.9", "16.6", "16.7", "16.2", "16.17", "16.20", "16.21"], - "motion": ["16.1", "16.4", "16.5", "16.3"], -} +from . import ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, ISY_BIN_SENS_DEVICE_TYPES +from .entity import ISYNodeEntity, ISYProgramEntity def setup_platform( @@ -29,7 +25,7 @@ def setup_platform( devices_by_nid = {} child_nodes = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][BINARY_SENSOR]: if node.parent_node is None: device = ISYBinarySensorEntity(node) devices.append(device) @@ -69,8 +65,8 @@ def setup_platform( device = ISYBinarySensorEntity(node) devices.append(device) - for name, status, _ in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYBinarySensorProgram(name, status)) + for name, status, _ in hass.data[ISY994_PROGRAMS][BINARY_SENSOR]: + devices.append(ISYBinarySensorProgramEntity(name, status)) add_entities(devices) @@ -83,7 +79,7 @@ def _detect_device_type(node) -> str: return None split_type = device_type.split(".") - for device_class, ids in ISY_DEVICE_TYPES.items(): + for device_class, ids in ISY_BIN_SENS_DEVICE_TYPES.items(): if f"{split_type[0]}.{split_type[1]}" in ids: return device_class @@ -95,7 +91,7 @@ def _is_val_unknown(val): return val == -1 * float("inf") -class ISYBinarySensorEntity(ISYDevice, BinarySensorEntity): +class ISYBinarySensorEntity(ISYNodeEntity, BinarySensorEntity): """Representation of an ISY994 binary sensor device. Often times, a single device is represented by multiple nodes in the ISY, @@ -253,7 +249,7 @@ def device_class(self) -> str: return self._device_class_from_type -class ISYBinarySensorHeartbeat(ISYDevice, BinarySensorEntity): +class ISYBinarySensorHeartbeat(ISYNodeEntity, BinarySensorEntity): """Representation of the battery state of an ISY994 sensor.""" def __init__(self, node, parent_device) -> None: @@ -353,18 +349,13 @@ def device_state_attributes(self): return attr -class ISYBinarySensorProgram(ISYDevice, BinarySensorEntity): +class ISYBinarySensorProgramEntity(ISYProgramEntity, BinarySensorEntity): """Representation of an ISY994 binary sensor program. This does not need all of the subnode logic in the device version of binary sensors. """ - def __init__(self, name, node) -> None: - """Initialize the ISY994 binary sensor program.""" - super().__init__(node) - self._name = name - @property def is_on(self) -> bool: """Get whether the ISY994 binary sensor device is on.""" diff --git a/homeassistant/components/isy994/const.py b/homeassistant/components/isy994/const.py new file mode 100644 index 00000000000000..ee50a4ef0df0cb --- /dev/null +++ b/homeassistant/components/isy994/const.py @@ -0,0 +1,479 @@ +"""Constants for the ISY994 Platform.""" +import logging + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_FAN, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + FAN_AUTO, + FAN_HIGH, + FAN_MEDIUM, + FAN_ON, + 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_BOOST, +) +from homeassistant.components.cover import DOMAIN as COVER +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +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 ( + CONCENTRATION_PARTS_PER_MILLION, + DEGREE, + ENERGY_KILO_WATT_HOUR, + FREQUENCY_HERTZ, + LENGTH_CENTIMETERS, + LENGTH_FEET, + LENGTH_INCHES, + LENGTH_KILOMETERS, + LENGTH_METERS, + MASS_KILOGRAMS, + MASS_POUNDS, + POWER_WATT, + PRESSURE_INHG, + SERVICE_LOCK, + SERVICE_UNLOCK, + SPEED_KILOMETERS_PER_HOUR, + SPEED_METERS_PER_SECOND, + SPEED_MILES_PER_HOUR, + STATE_CLOSED, + STATE_CLOSING, + STATE_LOCKED, + STATE_OFF, + STATE_ON, + STATE_OPEN, + STATE_OPENING, + STATE_PROBLEM, + STATE_UNKNOWN, + STATE_UNLOCKED, + TEMP_CELSIUS, + TEMP_FAHRENHEIT, + TEMP_KELVIN, + TIME_DAYS, + TIME_HOURS, + TIME_MILLISECONDS, + TIME_MINUTES, + TIME_MONTHS, + TIME_SECONDS, + TIME_YEARS, + UNIT_PERCENTAGE, + UV_INDEX, + VOLT, + VOLUME_GALLONS, + VOLUME_LITERS, +) + +_LOGGER = logging.getLogger(__package__) + +DOMAIN = "isy994" + +MANUFACTURER = "Universal Devices, Inc" + +CONF_IGNORE_STRING = "ignore_string" +CONF_SENSOR_STRING = "sensor_string" +CONF_ENABLE_CLIMATE = "enable_climate" +CONF_TLS_VER = "tls" + +DEFAULT_IGNORE_STRING = "{IGNORE ME}" +DEFAULT_SENSOR_STRING = "sensor" +DEFAULT_TLS_VERSION = 1.1 + +KEY_ACTIONS = "actions" +KEY_FOLDER = "folder" +KEY_MY_PROGRAMS = "My Programs" +KEY_STATUS = "status" + +SUPPORTED_PLATFORMS = [BINARY_SENSOR, SENSOR, LOCK, FAN, COVER, LIGHT, SWITCH] +SUPPORTED_PROGRAM_PLATFORMS = [BINARY_SENSOR, LOCK, FAN, COVER, SWITCH] + +SUPPORTED_BIN_SENS_CLASSES = ["moisture", "opening", "motion", "climate"] + +# ISY Scenes are more like Switches than Home Assistant Scenes +# (they can turn off, and report their state) +ISY_GROUP_PLATFORM = SWITCH + +ISY994_ISY = "isy" +ISY994_NODES = "isy994_nodes" +ISY994_WEATHER = "isy994_weather" +ISY994_PROGRAMS = "isy994_programs" + +# Do not use the Home Assistant consts for the states here - we're matching exact API +# responses, not using them for Home Assistant states +NODE_FILTERS = { + BINARY_SENSOR: { + "uom": [], + "states": [], + "node_def_id": [ + "BinaryAlarm", + "BinaryAlarm_ADV", + "BinaryControl", + "BinaryControl_ADV", + "EZIO2x4_Input", + "EZRAIN_Input", + "OnOffControl", + "OnOffControl_ADV", + ], + "insteon_type": [ + "7.0.", + "7.13.", + "16.", + ], # Does a startswith() match; include the dot + }, + SENSOR: { + # This is just a more-readable way of including MOST uoms between 1-100 + # (Remember that range() is non-inclusive of the stop value) + "uom": ( + ["1"] + + list(map(str, range(3, 11))) + + list(map(str, range(12, 51))) + + list(map(str, range(52, 66))) + + list(map(str, range(69, 78))) + + ["79"] + + list(map(str, range(82, 97))) + ), + "states": [], + "node_def_id": ["IMETER_SOLO", "EZIO2x4_Input_ADV"], + "insteon_type": ["9.0.", "9.7."], + }, + LOCK: { + "uom": ["11"], + "states": ["locked", "unlocked"], + "node_def_id": ["DoorLock"], + "insteon_type": ["15.", "4.64."], + }, + FAN: { + "uom": [], + "states": ["off", "low", "med", "high"], + "node_def_id": ["FanLincMotor"], + "insteon_type": ["1.46."], + }, + COVER: { + "uom": ["97"], + "states": ["open", "closed", "closing", "opening", "stopped"], + "node_def_id": [], + "insteon_type": [], + }, + LIGHT: { + "uom": ["51"], + "states": ["on", "off", "%"], + "node_def_id": [ + "BallastRelayLampSwitch", + "BallastRelayLampSwitch_ADV", + "DimmerLampOnly", + "DimmerLampSwitch", + "DimmerLampSwitch_ADV", + "DimmerSwitchOnly", + "DimmerSwitchOnly_ADV", + "KeypadDimmer", + "KeypadDimmer_ADV", + ], + "insteon_type": ["1."], + }, + SWITCH: { + "uom": ["2", "78"], + "states": ["on", "off"], + "node_def_id": [ + "AlertModuleArmed", + "AlertModuleSiren", + "AlertModuleSiren_ADV", + "EZIO2x4_Output", + "EZRAIN_Output", + "KeypadButton", + "KeypadButton_ADV", + "KeypadRelay", + "KeypadRelay_ADV", + "RelayLampOnly", + "RelayLampOnly_ADV", + "RelayLampSwitch", + "RelayLampSwitch_ADV", + "RelaySwitchOnlyPlusQuery", + "RelaySwitchOnlyPlusQuery_ADV", + "RemoteLinc2", + "RemoteLinc2_ADV", + "Siren", + "Siren_ADV", + "X10", + ], + "insteon_type": ["0.16.", "2.", "7.3.255.", "9.10.", "9.11.", "113."], + }, +} + +UOM_FRIENDLY_NAME = { + "1": "A", + "3": f"btu/{TIME_HOURS}", + "4": TEMP_CELSIUS, + "5": LENGTH_CENTIMETERS, + "6": "ft³", + "7": f"ft³/{TIME_MINUTES}", + "8": "m³", + "9": TIME_DAYS, + "10": TIME_DAYS, + "12": "dB", + "13": "dB A", + "14": DEGREE, + "16": "macroseismic", + "17": TEMP_FAHRENHEIT, + "18": LENGTH_FEET, + "19": TIME_HOURS, + "20": TIME_HOURS, + "21": "%AH", + "22": "%RH", + "23": PRESSURE_INHG, + "24": f"{LENGTH_INCHES}/{TIME_HOURS}", + "25": "index", + "26": TEMP_KELVIN, + "27": "keyword", + "28": MASS_KILOGRAMS, + "29": "kV", + "30": "kW", + "31": "kPa", + "32": SPEED_KILOMETERS_PER_HOUR, + "33": ENERGY_KILO_WATT_HOUR, + "34": "liedu", + "35": VOLUME_LITERS, + "36": "lx", + "37": "mercalli", + "38": LENGTH_METERS, + "39": f"{LENGTH_METERS}³/{TIME_HOURS}", + "40": SPEED_METERS_PER_SECOND, + "41": "mA", + "42": TIME_MILLISECONDS, + "43": "mV", + "44": TIME_MINUTES, + "45": TIME_MINUTES, + "46": f"mm/{TIME_HOURS}", + "47": TIME_MONTHS, + "48": SPEED_MILES_PER_HOUR, + "49": SPEED_METERS_PER_SECOND, + "50": "Ω", + "51": UNIT_PERCENTAGE, + "52": MASS_POUNDS, + "53": "pf", + "54": CONCENTRATION_PARTS_PER_MILLION, + "55": "pulse count", + "57": TIME_SECONDS, + "58": TIME_SECONDS, + "59": "S/m", + "60": "m_b", + "61": "M_L", + "62": "M_w", + "63": "M_S", + "64": "shindo", + "65": "SML", + "69": VOLUME_GALLONS, + "71": UV_INDEX, + "72": VOLT, + "73": POWER_WATT, + "74": f"{POWER_WATT}/{LENGTH_METERS}²", + "75": "weekday", + "76": DEGREE, + "77": TIME_YEARS, + "82": "mm", + "83": LENGTH_KILOMETERS, + "85": "Ω", + "86": "kΩ", + "87": f"{LENGTH_METERS}³/{LENGTH_METERS}³", + "88": "Water activity", + "89": "RPM", + "90": FREQUENCY_HERTZ, + "91": DEGREE, + "92": f"{DEGREE} South", + "101": f"{DEGREE} (x2)", + "102": "kWs", + "103": "$", + "104": "¢", + "105": LENGTH_INCHES, + "106": "mm/day", +} + +UOM_TO_STATES = { + "11": { # Deadbolt Status + 0: STATE_UNLOCKED, + 100: STATE_LOCKED, + 101: STATE_UNKNOWN, + 102: STATE_PROBLEM, + }, + "15": { # Door Lock Alarm + 1: "master code changed", + 2: "tamper code entry limit", + 3: "escutcheon removed", + 4: "key/manually locked", + 5: "locked by touch", + 6: "key/manually unlocked", + 7: "remote locking jammed bolt", + 8: "remotely locked", + 9: "remotely unlocked", + 10: "deadbolt jammed", + 11: "battery too low to operate", + 12: "critical low battery", + 13: "low battery", + 14: "automatically locked", + 15: "automatic locking jammed bolt", + 16: "remotely power cycled", + 17: "lock handling complete", + 19: "user deleted", + 20: "user added", + 21: "duplicate pin", + 22: "jammed bolt by locking with keypad", + 23: "locked by keypad", + 24: "unlocked by keypad", + 25: "keypad attempt outside schedule", + 26: "hardware failure", + 27: "factory reset", + }, + "66": { # Thermostat Heat/Cool State + 0: CURRENT_HVAC_IDLE, + 1: CURRENT_HVAC_HEAT, + 2: CURRENT_HVAC_COOL, + 3: CURRENT_HVAC_FAN, + 4: CURRENT_HVAC_HEAT, # Pending Heat + 5: CURRENT_HVAC_COOL, # Pending Cool + # >6 defined in ISY but not implemented, leaving for future expanision. + 6: CURRENT_HVAC_IDLE, + 7: CURRENT_HVAC_HEAT, + 8: CURRENT_HVAC_HEAT, + 9: CURRENT_HVAC_COOL, + 10: CURRENT_HVAC_HEAT, + 11: CURRENT_HVAC_HEAT, + }, + "67": { # Thermostat Mode + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_AUTO, + 4: PRESET_BOOST, + 5: "resume", + 6: HVAC_MODE_FAN_ONLY, + 7: "furnace", + 8: HVAC_MODE_DRY, + 9: "moist air", + 10: "auto changeover", + 11: "energy save heat", + 12: "energy save cool", + 13: PRESET_AWAY, + 14: HVAC_MODE_AUTO, + 15: HVAC_MODE_AUTO, + 16: HVAC_MODE_AUTO, + }, + "68": { # Thermostat Fan Mode + 0: FAN_AUTO, + 1: FAN_ON, + 2: FAN_HIGH, # Auto High + 3: FAN_HIGH, + 4: FAN_MEDIUM, # Auto Medium + 5: FAN_MEDIUM, + 6: "circulation", + 7: "humidity circulation", + }, + "78": {0: STATE_OFF, 100: STATE_ON}, # 0-Off 100-On + "79": {0: STATE_OPEN, 100: STATE_CLOSED}, # 0-Open 100-Close + "80": { # Thermostat Fan Run State + 0: STATE_OFF, + 1: STATE_ON, + 2: "on high", + 3: "on medium", + 4: "circulation", + 5: "humidity circulation", + 6: "right/left circulation", + 7: "up/down circulation", + 8: "quiet circulation", + }, + "84": {0: SERVICE_LOCK, 1: SERVICE_UNLOCK}, # Secure Mode + "93": { # Power Management Alarm + 1: "power applied", + 2: "ac mains disconnected", + 3: "ac mains reconnected", + 4: "surge detection", + 5: "volt drop or drift", + 6: "over current detected", + 7: "over voltage detected", + 8: "over load detected", + 9: "load error", + 10: "replace battery soon", + 11: "replace battery now", + 12: "battery is charging", + 13: "battery is fully charged", + 14: "charge battery soon", + 15: "charge battery now", + }, + "94": { # Appliance Alarm + 1: "program started", + 2: "program in progress", + 3: "program completed", + 4: "replace main filter", + 5: "failure to set target temperature", + 6: "supplying water", + 7: "water supply failure", + 8: "boiling", + 9: "boiling failure", + 10: "washing", + 11: "washing failure", + 12: "rinsing", + 13: "rinsing failure", + 14: "draining", + 15: "draining failure", + 16: "spinning", + 17: "spinning failure", + 18: "drying", + 19: "drying failure", + 20: "fan failure", + 21: "compressor failure", + }, + "95": { # Home Health Alarm + 1: "leaving bed", + 2: "sitting on bed", + 3: "lying on bed", + 4: "posture changed", + 5: "sitting on edge of bed", + }, + "96": { # VOC Level + 1: "clean", + 2: "slightly polluted", + 3: "moderately polluted", + 4: "highly polluted", + }, + "97": { # Barrier Status + **{ + 0: STATE_CLOSED, + 100: STATE_OPEN, + 101: STATE_UNKNOWN, + 102: "stopped", + 103: STATE_CLOSING, + 104: STATE_OPENING, + }, + **{ + b: f"{b} %" for a, b in enumerate(list(range(1, 100))) + }, # 1-99 are percentage open + }, + "98": { # Insteon Thermostat Mode + 0: HVAC_MODE_OFF, + 1: HVAC_MODE_HEAT, + 2: HVAC_MODE_COOL, + 3: HVAC_MODE_HEAT_COOL, + 4: HVAC_MODE_FAN_ONLY, + 5: HVAC_MODE_AUTO, # Program Auto + 6: HVAC_MODE_AUTO, # Program Heat-Set @ Local Device Only + 7: HVAC_MODE_AUTO, # Program Cool-Set @ Local Device Only + }, + "99": {7: FAN_ON, 8: FAN_AUTO}, # Insteon Thermostat Fan Mode +} + +ISY_BIN_SENS_DEVICE_TYPES = { + "moisture": ["16.8.", "16.13.", "16.14."], + "opening": ["16.9.", "16.6.", "16.7.", "16.2.", "16.17.", "16.20.", "16.21."], + "motion": ["16.1.", "16.4.", "16.5.", "16.3.", "16.22."], + "climate": ["5.11.", "5.10."], +} + +# TEMPORARY CONSTANTS -- REMOVE AFTER PyISYv2 IS AVAILABLE +ISY_VALUE_UNKNOWN = -1 * float("inf") diff --git a/homeassistant/components/isy994/cover.py b/homeassistant/components/isy994/cover.py index 38688db21d272b..1661bfe37abfd2 100644 --- a/homeassistant/components/isy994/cover.py +++ b/homeassistant/components/isy994/cover.py @@ -1,28 +1,13 @@ """Support for ISY994 covers.""" -import logging from typing import Callable -from homeassistant.components.cover import DOMAIN, CoverEntity -from homeassistant.const import ( - STATE_CLOSED, - STATE_CLOSING, - STATE_OPEN, - STATE_OPENING, - STATE_UNKNOWN, -) +from homeassistant.components.cover import DOMAIN as COVER, CoverEntity +from homeassistant.const import STATE_CLOSED, STATE_OPEN from homeassistant.helpers.typing import ConfigType -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -VALUE_TO_STATE = { - 0: STATE_CLOSED, - 101: STATE_UNKNOWN, - 102: "stopped", - 103: STATE_CLOSING, - 104: STATE_OPENING, -} +from . import ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER, UOM_TO_STATES +from .entity import ISYNodeEntity, ISYProgramEntity def setup_platform( @@ -30,16 +15,16 @@ def setup_platform( ): """Set up the ISY994 cover platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][COVER]: devices.append(ISYCoverEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYCoverProgram(name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][COVER]: + devices.append(ISYCoverProgramEntity(name, status, actions)) add_entities(devices) -class ISYCoverEntity(ISYDevice, CoverEntity): +class ISYCoverEntity(ISYNodeEntity, CoverEntity): """Representation of an ISY994 cover device.""" @property @@ -59,7 +44,8 @@ def state(self) -> str: """Get the state of the ISY994 cover device.""" if self.is_unknown(): return None - return VALUE_TO_STATE.get(self.value, STATE_OPEN) + # TEMPORARY: Cast value to int until PyISYv2. + return UOM_TO_STATES["97"].get(int(self.value), STATE_OPEN) def open_cover(self, **kwargs) -> None: """Send the open cover command to the ISY994 cover device.""" @@ -72,15 +58,9 @@ def close_cover(self, **kwargs) -> None: _LOGGER.error("Unable to close the cover") -class ISYCoverProgram(ISYCoverEntity): +class ISYCoverProgramEntity(ISYProgramEntity, CoverEntity): """Representation of an ISY994 cover program.""" - def __init__(self, name: str, node: object, actions: object) -> None: - """Initialize the ISY994 cover program.""" - super().__init__(node) - self._name = name - self._actions = actions - @property def state(self) -> str: """Get the state of the ISY994 cover program.""" diff --git a/homeassistant/components/isy994/entity.py b/homeassistant/components/isy994/entity.py new file mode 100644 index 00000000000000..f592de6c9b2f02 --- /dev/null +++ b/homeassistant/components/isy994/entity.py @@ -0,0 +1,96 @@ +"""Representation of ISYEntity Types.""" + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.typing import Dict + + +class ISYEntity(Entity): + """Representation of an ISY994 device.""" + + _attrs = {} + _name: str = None + + def __init__(self, node) -> None: + """Initialize the insteon device.""" + self._node = node + self._change_handler = None + self._control_handler = None + + async def async_added_to_hass(self) -> None: + """Subscribe to the node change events.""" + self._change_handler = self._node.status.subscribe("changed", self.on_update) + + if hasattr(self._node, "controlEvents"): + self._control_handler = self._node.controlEvents.subscribe(self.on_control) + + def on_update(self, event: object) -> None: + """Handle the update event from the ISY994 Node.""" + self.schedule_update_ha_state() + + def on_control(self, event: object) -> None: + """Handle a control event from the ISY994 Node.""" + self.hass.bus.fire( + "isy994_control", {"entity_id": self.entity_id, "control": event} + ) + + @property + def unique_id(self) -> str: + """Get the unique identifier of the device.""" + # pylint: disable=protected-access + if hasattr(self._node, "_id"): + return self._node._id + + return None + + @property + def name(self) -> str: + """Get the name of the device.""" + return self._name or str(self._node.name) + + @property + def should_poll(self) -> bool: + """No polling required since we're using the subscription.""" + return False + + @property + def value(self) -> int: + """Get the current value of the device.""" + # pylint: disable=protected-access + return self._node.status._val + + def is_unknown(self) -> bool: + """Get whether or not the value of this Entity's node is unknown. + + PyISY reports unknown values as -inf + """ + return self.value == -1 * float("inf") + + @property + def state(self): + """Return the state of the ISY device.""" + if self.is_unknown(): + return None + return super().state + + +class ISYNodeEntity(ISYEntity): + """Representation of a ISY Nodebase (Node/Group) entity.""" + + @property + def device_state_attributes(self) -> Dict: + """Get the state attributes for the device.""" + attr = {} + if hasattr(self._node, "aux_properties"): + for name, val in self._node.aux_properties.items(): + attr[name] = f"{val.get('value')} {val.get('uom')}" + return attr + + +class ISYProgramEntity(ISYEntity): + """Representation of an ISY994 program base.""" + + def __init__(self, name: str, status, actions=None) -> None: + """Initialize the ISY994 program-based entity.""" + super().__init__(status) + self._name = name + self._actions = actions diff --git a/homeassistant/components/isy994/fan.py b/homeassistant/components/isy994/fan.py index b42e8cda755f8d..2315610dcf8e0d 100644 --- a/homeassistant/components/isy994/fan.py +++ b/homeassistant/components/isy994/fan.py @@ -1,9 +1,8 @@ """Support for ISY994 fans.""" -import logging from typing import Callable from homeassistant.components.fan import ( - DOMAIN, + DOMAIN as FAN, SPEED_HIGH, SPEED_LOW, SPEED_MEDIUM, @@ -13,9 +12,9 @@ ) from homeassistant.helpers.typing import ConfigType -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from . import ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER +from .entity import ISYNodeEntity, ISYProgramEntity VALUE_TO_STATE = { 0: SPEED_OFF, @@ -37,16 +36,16 @@ def setup_platform( """Set up the ISY994 fan platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYFanDevice(node)) + for node in hass.data[ISY994_NODES][FAN]: + devices.append(ISYFanEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYFanProgram(name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][FAN]: + devices.append(ISYFanProgramEntity(name, status, actions)) add_entities(devices) -class ISYFanDevice(ISYDevice, FanEntity): +class ISYFanEntity(ISYNodeEntity, FanEntity): """Representation of an ISY994 fan device.""" @property @@ -82,14 +81,20 @@ def supported_features(self) -> int: return SUPPORT_SET_SPEED -class ISYFanProgram(ISYFanDevice): +class ISYFanProgramEntity(ISYProgramEntity, FanEntity): """Representation of an ISY994 fan program.""" - def __init__(self, name: str, node, actions) -> None: - """Initialize the ISY994 fan program.""" - super().__init__(node) - self._name = name - self._actions = actions + @property + def speed(self) -> str: + """Return the current speed.""" + # TEMPORARY: Cast value to int until PyISYv2. + return VALUE_TO_STATE.get(int(self.value)) + + @property + def is_on(self) -> bool: + """Get if the fan is on.""" + # TEMPORARY: Cast value to int until PyISYv2. + return int(self.value) != 0 def turn_off(self, **kwargs) -> None: """Send the turn on command to ISY994 fan program.""" @@ -100,8 +105,3 @@ def turn_on(self, speed: str = None, **kwargs) -> None: """Send the turn off command to ISY994 fan program.""" if not self._actions.runElse(): _LOGGER.error("Unable to turn on the fan") - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return 0 diff --git a/homeassistant/components/isy994/helpers.py b/homeassistant/components/isy994/helpers.py new file mode 100644 index 00000000000000..4f6bba6b6592ec --- /dev/null +++ b/homeassistant/components/isy994/helpers.py @@ -0,0 +1,249 @@ +"""Sorting helpers for ISY994 device classifications.""" +from collections import namedtuple + +from PyISY.Nodes import Group + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR +from homeassistant.components.fan import DOMAIN as FAN +from homeassistant.components.light import DOMAIN as LIGHT +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.helpers.typing import HomeAssistantType + +from .const import ( + _LOGGER, + ISY994_NODES, + ISY994_PROGRAMS, + ISY994_WEATHER, + ISY_GROUP_PLATFORM, + KEY_ACTIONS, + KEY_FOLDER, + KEY_MY_PROGRAMS, + KEY_STATUS, + NODE_FILTERS, + SUPPORTED_PLATFORMS, + SUPPORTED_PROGRAM_PLATFORMS, +) + +WeatherNode = namedtuple("WeatherNode", ("status", "name", "uom")) + + +def _check_for_node_def( + hass: HomeAssistantType, node, single_platform: str = None +) -> bool: + """Check if the node matches the node_def_id for any platforms. + + This is only present on the 5.0 ISY firmware, and is the most reliable + way to determine a device's type. + """ + if not hasattr(node, "node_def_id") or node.node_def_id is None: + # Node doesn't have a node_def (pre 5.0 firmware most likely) + return False + + node_def_id = node.node_def_id + + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_def_id in NODE_FILTERS[platform]["node_def_id"]: + hass.data[ISY994_NODES][platform].append(node) + return True + + _LOGGER.warning("Unsupported node: %s, type: %s", node.name, node.type) + return False + + +def _check_for_insteon_type( + hass: HomeAssistantType, node, single_platform: str = None +) -> bool: + """Check if the node matches the Insteon type for any platforms. + + This is for (presumably) every version of the ISY firmware, but only + works for Insteon device. "Node Server" (v5+) and Z-Wave and others will + not have a type. + """ + if not hasattr(node, "type") or node.type is None: + # Node doesn't have a type (non-Insteon device most likely) + return False + + device_type = node.type + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if any( + [ + device_type.startswith(t) + for t in set(NODE_FILTERS[platform]["insteon_type"]) + ] + ): + + # Hacky special-case just for FanLinc, which has a light module + # as one of its nodes. Note that this special-case is not necessary + # on ISY 5.x firmware as it uses the superior NodeDefs method + if platform == FAN and int(node.nid[-1]) == 1: + hass.data[ISY994_NODES][LIGHT].append(node) + return True + + hass.data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_uom_id( + hass: HomeAssistantType, node, single_platform: str = None, uom_list: list = None +) -> bool: + """Check if a node's uom matches any of the platforms uom filter. + + This is used for versions of the ISY firmware that report uoms as a single + ID. We can often infer what type of device it is by that ID. + """ + if not hasattr(node, "uom") or node.uom is None: + # Node doesn't have a uom (Scenes for example) + return False + + node_uom = set(map(str.lower, node.uom)) + + if uom_list: + if node_uom.intersection(uom_list): + hass.data[ISY994_NODES][single_platform].append(node) + return True + else: + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom.intersection(NODE_FILTERS[platform]["uom"]): + hass.data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _check_for_states_in_uom( + hass: HomeAssistantType, node, single_platform: str = None, states_list: list = None +) -> bool: + """Check if a list of uoms matches two possible filters. + + This is for versions of the ISY firmware that report uoms as a list of all + possible "human readable" states. This filter passes if all of the possible + states fit inside the given filter. + """ + if not hasattr(node, "uom") or node.uom is None: + # Node doesn't have a uom (Scenes for example) + return False + + node_uom = set(map(str.lower, node.uom)) + + if states_list: + if node_uom == set(states_list): + hass.data[ISY994_NODES][single_platform].append(node) + return True + else: + platforms = SUPPORTED_PLATFORMS if not single_platform else [single_platform] + for platform in platforms: + if node_uom == set(NODE_FILTERS[platform]["states"]): + hass.data[ISY994_NODES][platform].append(node) + return True + + return False + + +def _is_sensor_a_binary_sensor(hass: HomeAssistantType, node) -> bool: + """Determine if the given sensor node should be a binary_sensor.""" + if _check_for_node_def(hass, node, single_platform=BINARY_SENSOR): + return True + if _check_for_insteon_type(hass, node, single_platform=BINARY_SENSOR): + return True + + # For the next two checks, we're providing our own set of uoms that + # represent on/off devices. This is because we can only depend on these + # checks in the context of already knowing that this is definitely a + # sensor device. + if _check_for_uom_id( + hass, node, single_platform=BINARY_SENSOR, uom_list=["2", "78"] + ): + return True + if _check_for_states_in_uom( + hass, node, single_platform=BINARY_SENSOR, states_list=["on", "off"] + ): + return True + + return False + + +def _categorize_nodes( + hass: HomeAssistantType, nodes, ignore_identifier: str, sensor_identifier: str +) -> None: + """Sort the nodes to their proper platforms.""" + for (path, node) in nodes: + ignored = ignore_identifier in path or ignore_identifier in node.name + if ignored: + # Don't import this node as a device at all + continue + + if isinstance(node, Group): + hass.data[ISY994_NODES][ISY_GROUP_PLATFORM].append(node) + continue + + if sensor_identifier in path or sensor_identifier in node.name: + # User has specified to treat this as a sensor. First we need to + # determine if it should be a binary_sensor. + if _is_sensor_a_binary_sensor(hass, node): + continue + + hass.data[ISY994_NODES][SENSOR].append(node) + continue + + # We have a bunch of different methods for determining the device type, + # each of which works with different ISY firmware versions or device + # family. The order here is important, from most reliable to least. + if _check_for_node_def(hass, node): + continue + if _check_for_insteon_type(hass, node): + continue + if _check_for_uom_id(hass, node): + continue + if _check_for_states_in_uom(hass, node): + continue + + +def _categorize_programs(hass: HomeAssistantType, programs: dict) -> None: + """Categorize the ISY994 programs.""" + for platform in SUPPORTED_PROGRAM_PLATFORMS: + try: + folder = programs[KEY_MY_PROGRAMS][f"HA.{platform}"] + except KeyError: + continue + for dtype, _, node_id in folder.children: + if dtype != KEY_FOLDER: + continue + entity_folder = folder[node_id] + try: + status = entity_folder[KEY_STATUS] + assert status.dtype == "program", "Not a program" + if platform != BINARY_SENSOR: + actions = entity_folder[KEY_ACTIONS] + assert actions.dtype == "program", "Not a program" + else: + actions = None + except (AttributeError, KeyError, AssertionError): + _LOGGER.warning( + "Program entity '%s' not loaded due " + "to invalid folder structure.", + entity_folder.name, + ) + continue + + entity = (entity_folder.name, status, actions) + hass.data[ISY994_PROGRAMS][platform].append(entity) + + +def _categorize_weather(hass: HomeAssistantType, climate) -> None: + """Categorize the ISY994 weather data.""" + climate_attrs = dir(climate) + weather_nodes = [ + WeatherNode( + getattr(climate, attr), + attr.replace("_", " "), + getattr(climate, f"{attr}_units"), + ) + for attr in climate_attrs + if f"{attr}_units" in climate_attrs + ] + hass.data[ISY994_WEATHER].extend(weather_nodes) diff --git a/homeassistant/components/isy994/light.py b/homeassistant/components/isy994/light.py index 7ae8d1c76f8176..e5f35bc62fb7a9 100644 --- a/homeassistant/components/isy994/light.py +++ b/homeassistant/components/isy994/light.py @@ -1,14 +1,17 @@ """Support for ISY994 lights.""" -import logging from typing import Callable -from homeassistant.components.light import DOMAIN, SUPPORT_BRIGHTNESS, LightEntity +from homeassistant.components.light import ( + DOMAIN as LIGHT, + SUPPORT_BRIGHTNESS, + LightEntity, +) from homeassistant.helpers.restore_state import RestoreEntity from homeassistant.helpers.typing import ConfigType -from . import ISY994_NODES, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from . import ISY994_NODES +from .const import _LOGGER +from .entity import ISYNodeEntity ATTR_LAST_BRIGHTNESS = "last_brightness" @@ -18,13 +21,13 @@ def setup_platform( ): """Set up the ISY994 light platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYLightDevice(node)) + for node in hass.data[ISY994_NODES][LIGHT]: + devices.append(ISYLightEntity(node)) add_entities(devices) -class ISYLightDevice(ISYDevice, LightEntity, RestoreEntity): +class ISYLightEntity(ISYNodeEntity, LightEntity, RestoreEntity): """Representation of an ISY994 light device.""" def __init__(self, node) -> None: diff --git a/homeassistant/components/isy994/lock.py b/homeassistant/components/isy994/lock.py index 807027d4610509..b1e94cadac899b 100644 --- a/homeassistant/components/isy994/lock.py +++ b/homeassistant/components/isy994/lock.py @@ -1,14 +1,13 @@ """Support for ISY994 locks.""" -import logging from typing import Callable -from homeassistant.components.lock import DOMAIN, LockEntity +from homeassistant.components.lock import DOMAIN as LOCK, LockEntity from homeassistant.const import STATE_LOCKED, STATE_UNKNOWN, STATE_UNLOCKED from homeassistant.helpers.typing import ConfigType -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from . import ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER +from .entity import ISYNodeEntity, ISYProgramEntity VALUE_TO_STATE = {0: STATE_UNLOCKED, 100: STATE_LOCKED} @@ -18,16 +17,16 @@ def setup_platform( ): """Set up the ISY994 lock platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: - devices.append(ISYLockDevice(node)) + for node in hass.data[ISY994_NODES][LOCK]: + devices.append(ISYLockEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYLockProgram(name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][LOCK]: + devices.append(ISYLockProgramEntity(name, status, actions)) add_entities(devices) -class ISYLockDevice(ISYDevice, LockEntity): +class ISYLockEntity(ISYNodeEntity, LockEntity): """Representation of an ISY994 lock device.""" def __init__(self, node) -> None: @@ -70,15 +69,9 @@ def unlock(self, **kwargs) -> None: self._node.update(0.5) -class ISYLockProgram(ISYLockDevice): +class ISYLockProgramEntity(ISYProgramEntity, LockEntity): """Representation of a ISY lock program.""" - def __init__(self, name: str, node, actions) -> None: - """Initialize the lock.""" - super().__init__(node) - self._name = name - self._actions = actions - @property def is_locked(self) -> bool: """Return true if the device is locked.""" diff --git a/homeassistant/components/isy994/manifest.json b/homeassistant/components/isy994/manifest.json index 083f25808fbc9a..88de7db824cd57 100644 --- a/homeassistant/components/isy994/manifest.json +++ b/homeassistant/components/isy994/manifest.json @@ -3,5 +3,5 @@ "name": "Universal Devices ISY994", "documentation": "https://www.home-assistant.io/integrations/isy994", "requirements": ["PyISY==1.1.2"], - "codeowners": ["@bdraco"] + "codeowners": ["@bdraco", "@shbatm"] } diff --git a/homeassistant/components/isy994/sensor.py b/homeassistant/components/isy994/sensor.py index 1252d0ef53b4b0..bf787582910b87 100644 --- a/homeassistant/components/isy994/sensor.py +++ b/homeassistant/components/isy994/sensor.py @@ -1,252 +1,13 @@ """Support for ISY994 sensors.""" -import logging from typing import Callable -from homeassistant.components.sensor import DOMAIN -from homeassistant.const import ( - CONCENTRATION_PARTS_PER_MILLION, - DEGREE, - FREQUENCY_HERTZ, - LENGTH_CENTIMETERS, - LENGTH_KILOMETERS, - LENGTH_METERS, - MASS_KILOGRAMS, - POWER_WATT, - SPEED_KILOMETERS_PER_HOUR, - SPEED_METERS_PER_SECOND, - SPEED_MILES_PER_HOUR, - TEMP_CELSIUS, - TEMP_FAHRENHEIT, - TIME_DAYS, - TIME_HOURS, - TIME_MILLISECONDS, - TIME_MINUTES, - TIME_MONTHS, - TIME_SECONDS, - TIME_YEARS, - UNIT_PERCENTAGE, - UV_INDEX, - VOLT, -) +from homeassistant.components.sensor import DOMAIN as SENSOR +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.helpers.typing import ConfigType -from . import ISY994_NODES, ISY994_WEATHER, ISYDevice - -_LOGGER = logging.getLogger(__name__) - -UOM_FRIENDLY_NAME = { - "1": "amp", - "3": f"btu/{TIME_HOURS}", - "4": TEMP_CELSIUS, - "5": LENGTH_CENTIMETERS, - "6": "ft³", - "7": f"ft³/{TIME_MINUTES}", - "8": "m³", - "9": TIME_DAYS, - "10": TIME_DAYS, - "12": "dB", - "13": "dB A", - "14": DEGREE, - "16": "macroseismic", - "17": TEMP_FAHRENHEIT, - "18": "ft", - "19": TIME_HOURS, - "20": TIME_HOURS, - "21": "abs. humidity (%)", - "22": "rel. humidity (%)", - "23": "inHg", - "24": "in/hr", - "25": "index", - "26": "K", - "27": "keyword", - "28": MASS_KILOGRAMS, - "29": "kV", - "30": "kW", - "31": "kPa", - "32": SPEED_KILOMETERS_PER_HOUR, - "33": "kWH", - "34": "liedu", - "35": "l", - "36": "lx", - "37": "mercalli", - "38": LENGTH_METERS, - "39": "m³/hr", - "40": SPEED_METERS_PER_SECOND, - "41": "mA", - "42": TIME_MILLISECONDS, - "43": "mV", - "44": TIME_MINUTES, - "45": TIME_MINUTES, - "46": "mm/hr", - "47": TIME_MONTHS, - "48": SPEED_MILES_PER_HOUR, - "49": SPEED_METERS_PER_SECOND, - "50": "ohm", - "51": UNIT_PERCENTAGE, - "52": "lb", - "53": "power factor", - "54": CONCENTRATION_PARTS_PER_MILLION, - "55": "pulse count", - "57": TIME_SECONDS, - "58": TIME_SECONDS, - "59": "seimens/m", - "60": "body wave magnitude scale", - "61": "Ricter scale", - "62": "moment magnitude scale", - "63": "surface wave magnitude scale", - "64": "shindo", - "65": "SML", - "69": "gal", - "71": UV_INDEX, - "72": VOLT, - "73": POWER_WATT, - "74": f"{POWER_WATT}/m²", - "75": "weekday", - "76": f"Wind Direction ({DEGREE})", - "77": TIME_YEARS, - "82": "mm", - "83": LENGTH_KILOMETERS, - "85": "ohm", - "86": "kOhm", - "87": "m³/m³", - "88": "Water activity", - "89": "RPM", - "90": FREQUENCY_HERTZ, - "91": f"{DEGREE} (Relative to North)", - "92": f"{DEGREE} (Relative to South)", -} - -UOM_TO_STATES = { - "11": {"0": "unlocked", "100": "locked", "102": "jammed"}, - "15": { - "1": "master code changed", - "2": "tamper code entry limit", - "3": "escutcheon removed", - "4": "key/manually locked", - "5": "locked by touch", - "6": "key/manually unlocked", - "7": "remote locking jammed bolt", - "8": "remotely locked", - "9": "remotely unlocked", - "10": "deadbolt jammed", - "11": "battery too low to operate", - "12": "critical low battery", - "13": "low battery", - "14": "automatically locked", - "15": "automatic locking jammed bolt", - "16": "remotely power cycled", - "17": "lock handling complete", - "19": "user deleted", - "20": "user added", - "21": "duplicate pin", - "22": "jammed bolt by locking with keypad", - "23": "locked by keypad", - "24": "unlocked by keypad", - "25": "keypad attempt outside schedule", - "26": "hardware failure", - "27": "factory reset", - }, - "66": { - "0": "idle", - "1": "heating", - "2": "cooling", - "3": "fan only", - "4": "pending heat", - "5": "pending cool", - "6": "vent", - "7": "aux heat", - "8": "2nd stage heating", - "9": "2nd stage cooling", - "10": "2nd stage aux heat", - "11": "3rd stage aux heat", - }, - "67": { - "0": "off", - "1": "heat", - "2": "cool", - "3": "auto", - "4": "aux/emergency heat", - "5": "resume", - "6": "fan only", - "7": "furnace", - "8": "dry air", - "9": "moist air", - "10": "auto changeover", - "11": "energy save heat", - "12": "energy save cool", - "13": "away", - }, - "68": { - "0": "auto", - "1": "on", - "2": "auto high", - "3": "high", - "4": "auto medium", - "5": "medium", - "6": "circulation", - "7": "humidity circulation", - }, - "93": { - "1": "power applied", - "2": "ac mains disconnected", - "3": "ac mains reconnected", - "4": "surge detection", - "5": "volt drop or drift", - "6": "over current detected", - "7": "over voltage detected", - "8": "over load detected", - "9": "load error", - "10": "replace battery soon", - "11": "replace battery now", - "12": "battery is charging", - "13": "battery is fully charged", - "14": "charge battery soon", - "15": "charge battery now", - }, - "94": { - "1": "program started", - "2": "program in progress", - "3": "program completed", - "4": "replace main filter", - "5": "failure to set target temperature", - "6": "supplying water", - "7": "water supply failure", - "8": "boiling", - "9": "boiling failure", - "10": "washing", - "11": "washing failure", - "12": "rinsing", - "13": "rinsing failure", - "14": "draining", - "15": "draining failure", - "16": "spinning", - "17": "spinning failure", - "18": "drying", - "19": "drying failure", - "20": "fan failure", - "21": "compressor failure", - }, - "95": { - "1": "leaving bed", - "2": "sitting on bed", - "3": "lying on bed", - "4": "posture changed", - "5": "sitting on edge of bed", - }, - "96": { - "1": "clean", - "2": "slightly polluted", - "3": "moderately polluted", - "4": "highly polluted", - }, - "97": { - "0": "closed", - "100": "open", - "102": "stopped", - "103": "closing", - "104": "opening", - }, -} +from . import ISY994_NODES, ISY994_WEATHER +from .const import _LOGGER, UOM_FRIENDLY_NAME, UOM_TO_STATES +from .entity import ISYEntity, ISYNodeEntity def setup_platform( @@ -255,9 +16,9 @@ def setup_platform( """Set up the ISY994 sensor platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][SENSOR]: _LOGGER.debug("Loading %s", node.name) - devices.append(ISYSensorDevice(node)) + devices.append(ISYSensorEntity(node)) for node in hass.data[ISY994_WEATHER]: devices.append(ISYWeatherDevice(node)) @@ -265,7 +26,7 @@ def setup_platform( add_entities(devices) -class ISYSensorDevice(ISYDevice): +class ISYSensorEntity(ISYNodeEntity): """Representation of an ISY994 sensor device.""" @property @@ -289,8 +50,9 @@ def state(self) -> str: if len(self._node.uom) == 1: if self._node.uom[0] in UOM_TO_STATES: states = UOM_TO_STATES.get(self._node.uom[0]) - if self.value in states: - return states.get(self.value) + # TEMPORARY: Cast value to int until PyISYv2. + if int(self.value) in states: + return states.get(int(self.value)) elif self._node.prec and self._node.prec != [0]: str_val = str(self.value) int_prec = int(self._node.prec) @@ -316,7 +78,8 @@ def unit_of_measurement(self) -> str: return raw_units -class ISYWeatherDevice(ISYDevice): +# Depreciated, not renaming. Will be removed in next PR. +class ISYWeatherDevice(ISYEntity): """Representation of an ISY994 weather device.""" @property diff --git a/homeassistant/components/isy994/switch.py b/homeassistant/components/isy994/switch.py index d5339960282b7d..e87ed846fd9425 100644 --- a/homeassistant/components/isy994/switch.py +++ b/homeassistant/components/isy994/switch.py @@ -1,13 +1,12 @@ """Support for ISY994 switches.""" -import logging from typing import Callable -from homeassistant.components.switch import DOMAIN, SwitchEntity +from homeassistant.components.switch import DOMAIN as SWITCH, SwitchEntity from homeassistant.helpers.typing import ConfigType -from . import ISY994_NODES, ISY994_PROGRAMS, ISYDevice - -_LOGGER = logging.getLogger(__name__) +from . import ISY994_NODES, ISY994_PROGRAMS +from .const import _LOGGER +from .entity import ISYNodeEntity, ISYProgramEntity def setup_platform( @@ -15,17 +14,17 @@ def setup_platform( ): """Set up the ISY994 switch platform.""" devices = [] - for node in hass.data[ISY994_NODES][DOMAIN]: + for node in hass.data[ISY994_NODES][SWITCH]: if not node.dimmable: - devices.append(ISYSwitchDevice(node)) + devices.append(ISYSwitchEntity(node)) - for name, status, actions in hass.data[ISY994_PROGRAMS][DOMAIN]: - devices.append(ISYSwitchProgram(name, status, actions)) + for name, status, actions in hass.data[ISY994_PROGRAMS][SWITCH]: + devices.append(ISYSwitchProgramEntity(name, status, actions)) add_entities(devices) -class ISYSwitchDevice(ISYDevice, SwitchEntity): +class ISYSwitchEntity(ISYNodeEntity, SwitchEntity): """Representation of an ISY994 switch device.""" @property @@ -44,15 +43,9 @@ def turn_on(self, **kwargs) -> None: _LOGGER.debug("Unable to turn on switch.") -class ISYSwitchProgram(ISYSwitchDevice): +class ISYSwitchProgramEntity(ISYProgramEntity, SwitchEntity): """A representation of an ISY994 program switch.""" - def __init__(self, name: str, node, actions) -> None: - """Initialize the ISY994 switch program.""" - super().__init__(node) - self._name = name - self._actions = actions - @property def is_on(self) -> bool: """Get whether the ISY994 switch program is on.""" @@ -67,3 +60,8 @@ def turn_off(self, **kwargs) -> None: """Send the turn off command to the ISY994 switch program.""" if not self._actions.runElse(): _LOGGER.error("Unable to turn off switch") + + @property + def icon(self) -> str: + """Get the icon for programs.""" + return "mdi:script-text-outline" # Matches isy program icon diff --git a/homeassistant/components/izone/translations/es-419.json b/homeassistant/components/izone/translations/es-419.json new file mode 100644 index 00000000000000..b645c427e3e2db --- /dev/null +++ b/homeassistant/components/izone/translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "no_devices_found": "No se encontraron dispositivos iZone en la red.", + "single_instance_allowed": "Solo es necesaria una \u00fanica configuraci\u00f3n de iZone." + }, + "step": { + "confirm": { + "description": "\u00bfDesea configurar iZone?", + "title": "iZone" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/izone/translations/no.json b/homeassistant/components/izone/translations/no.json index 49c806aa5642f2..854805948ee390 100644 --- a/homeassistant/components/izone/translations/no.json +++ b/homeassistant/components/izone/translations/no.json @@ -6,7 +6,7 @@ }, "step": { "confirm": { - "description": "Vil du konfigurere iZone?", + "description": "Vil du \u00e5 sette opp iZone?", "title": "" } } diff --git a/homeassistant/components/konnected/translations/es-419.json b/homeassistant/components/konnected/translations/es-419.json new file mode 100644 index 00000000000000..a63ba501960727 --- /dev/null +++ b/homeassistant/components/konnected/translations/es-419.json @@ -0,0 +1,108 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ya est\u00e1 en progreso.", + "not_konn_panel": "No es un dispositivo Konnected.io reconocido", + "unknown": "Se produjo un error desconocido" + }, + "error": { + "cannot_connect": "No se puede conectar a un Panel Konnected en {host}: {port}" + }, + "step": { + "confirm": { + "description": "Modelo: {model} \nID: {id} \nHost: {host} \nPuerto: {port} \n\nPuede configurar el comportamiento de IO y del panel en la configuraci\u00f3n del Panel de alarmas conectadas.", + "title": "Dispositivo Konnected listo" + }, + "import_confirm": { + "description": "Se ha descubierto un Panel de alarma conectado con ID {id} en configuration.yaml. Este flujo le permitir\u00e1 importarlo a una entrada de configuraci\u00f3n.", + "title": "Importar dispositivo Konnected" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP del dispositivo Konnected", + "port": "Puerto de dispositivo Konnected" + }, + "description": "Ingrese la informaci\u00f3n del host para su Panel Konnected.", + "title": "Descubrir el dispositivo Konnected" + } + } + }, + "options": { + "abort": { + "not_konn_panel": "No es un dispositivo Konnected.io reconocido" + }, + "error": { + "bad_host": "URL de host de API de sobrescritura no v\u00e1lida" + }, + "step": { + "options_binary": { + "data": { + "inverse": "Invertir el estado abierto/cerrado", + "name": "Nombre (opcional)", + "type": "Tipo de sensor binario" + }, + "description": "Seleccione las opciones para el sensor binario conectado a {zone}", + "title": "Configurar sensor binario" + }, + "options_digital": { + "data": { + "name": "Nombre (opcional)", + "poll_interval": "Intervalo de sondeo (minutos) (opcional)", + "type": "Tipo de sensor" + }, + "description": "Seleccione las opciones para el sensor digital conectado a {zone}", + "title": "Configurar sensor digital" + }, + "options_io": { + "data": { + "1": "Zona 1", + "2": "Zona 2", + "3": "Zona 3", + "4": "Zona 4", + "5": "Zona 5", + "6": "Zona 6", + "7": "Zona 7", + "out": "SALIDA" + }, + "description": "Descubierto un {model} en {host} . Seleccione la configuraci\u00f3n base de cada E/S a continuaci\u00f3n: seg\u00fan la E/S, puede permitir sensores binarios (contactos de apertura/cierre), sensores digitales (dht y ds18b20) o salidas conmutables. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S" + }, + "options_io_ext": { + "data": { + "10": "Zona 10", + "11": "Zona 11", + "12": "Zona 12", + "8": "Zona 8", + "9": "Zona 9", + "alarm1": "ALARMA1", + "alarm2_out2": "SALIDA2/ALARMA2", + "out1": "SALIDA1" + }, + "description": "Seleccione la configuraci\u00f3n de las E/S restantes a continuaci\u00f3n. Podr\u00e1 configurar opciones detalladas en los pr\u00f3ximos pasos.", + "title": "Configurar E/S extendida" + }, + "options_misc": { + "data": { + "api_host": "Sobrescribir URL de host de API (opcional)", + "blink": "Parpadea el LED del panel cuando se env\u00eda un cambio de estado", + "override_api_host": "Sobrescribir la URL predeterminada del panel de host de la API de Home Assistant" + }, + "description": "Seleccione el comportamiento deseado para su panel", + "title": "Configurar Misc" + }, + "options_switch": { + "data": { + "activation": "Salida cuando est\u00e1 encendido", + "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)" + }, + "description": "Seleccione las opciones de salida para {zone}: state {state}", + "title": "Configurar salida conmutable" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/konnected/translations/fr.json b/homeassistant/components/konnected/translations/fr.json index 566c623c80cef9..0cc9cf06896107 100644 --- a/homeassistant/components/konnected/translations/fr.json +++ b/homeassistant/components/konnected/translations/fr.json @@ -6,8 +6,12 @@ "not_konn_panel": "Non reconnu comme appareil Konnected.io", "unknown": "Une erreur inconnue s'est produite" }, + "error": { + "cannot_connect": "Impossible de se connecter \u00e0 Konnected Panel sur {host} : {port}" + }, "step": { "confirm": { + "description": "Model: {model} \n ID: {id} \n Host: {host} \n Port: {port} \n\nVous pouvez configurer le comportement des E/S et du panneau dans les param\u00e8tres de Konnected Alarm Panel.", "title": "Appareil Konnected pr\u00eat" }, "import_confirm": { @@ -15,12 +19,18 @@ }, "user": { "data": { - "host": "Adresse IP de l\u2019appareil Konnected" - } + "host": "Adresse IP de l\u2019appareil Konnected", + "port": "Port de l'appareil Konnected" + }, + "description": "Veuillez saisir les informations de l\u2019h\u00f4te de votre panneau Konnected.", + "title": "D\u00e9couverte d\u2019appareil Konnected" } } }, "options": { + "abort": { + "not_konn_panel": "Non reconnu comme appareil Konnected.io" + }, "error": { "one": "Vide", "other": "Vide" diff --git a/homeassistant/components/life360/config_flow.py b/homeassistant/components/life360/config_flow.py index e0ea645b197f71..e24bb3639b66be 100644 --- a/homeassistant/components/life360/config_flow.py +++ b/homeassistant/components/life360/config_flow.py @@ -47,8 +47,8 @@ async def async_step_user(self, user_input=None): try: # pylint: disable=no-value-for-parameter vol.Email()(self._username) - authorization = self._api.get_authorization( - self._username, self._password + authorization = await self.hass.async_add_executor_job( + self._api.get_authorization, self._username, self._password ) except vol.Invalid: errors[CONF_USERNAME] = "invalid_username" @@ -89,7 +89,9 @@ async def async_step_import(self, user_input): username = user_input[CONF_USERNAME] password = user_input[CONF_PASSWORD] try: - authorization = self._api.get_authorization(username, password) + authorization = await self.hass.async_add_executor_job( + self._api.get_authorization, username, password + ) except LoginError: _LOGGER.error("Invalid credentials for %s", username) return self.async_abort(reason="invalid_credentials") diff --git a/homeassistant/components/life360/strings.json b/homeassistant/components/life360/strings.json index 22d3f52c63b813..a4f4cdfed95799 100644 --- a/homeassistant/components/life360/strings.json +++ b/homeassistant/components/life360/strings.json @@ -10,7 +10,7 @@ "error": { "invalid_username": "Invalid username", "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured", + "user_already_configured": "[%key:common::config_flow::abort::already_configured_account%]", "unexpected": "Unexpected error communicating with Life360 server" }, "create_entry": { @@ -18,7 +18,7 @@ }, "abort": { "invalid_credentials": "Invalid credentials", - "user_already_configured": "Account has already been configured" + "user_already_configured": "[%key:common::config_flow::abort::already_configured_account%]" } } } diff --git a/homeassistant/components/life360/translations/es-419.json b/homeassistant/components/life360/translations/es-419.json index 33d8d59d080a00..627f5b8f198c04 100644 --- a/homeassistant/components/life360/translations/es-419.json +++ b/homeassistant/components/life360/translations/es-419.json @@ -1,8 +1,12 @@ { "config": { "abort": { + "invalid_credentials": "Credenciales no v\u00e1lidas", "user_already_configured": "La cuenta ya ha sido configurada" }, + "create_entry": { + "default": "Para establecer opciones avanzadas, consulte [Documentaci\u00f3n de Life360] ({docs_url})." + }, "error": { "invalid_credentials": "Credenciales no v\u00e1lidas", "invalid_username": "Nombre de usuario inv\u00e1lido", @@ -15,6 +19,7 @@ "password": "Contrase\u00f1a", "username": "Nombre de usuario" }, + "description": "Para establecer opciones avanzadas, consulte [Documentaci\u00f3n de Life360] ( {docs_url} ). \n Es posible que desee hacer eso antes de agregar cuentas.", "title": "Informaci\u00f3n de la cuenta Life360" } } diff --git a/homeassistant/components/light/translations/es-419.json b/homeassistant/components/light/translations/es-419.json index a36bd06e27e4dd..f8939b689cfbc6 100644 --- a/homeassistant/components/light/translations/es-419.json +++ b/homeassistant/components/light/translations/es-419.json @@ -1,5 +1,13 @@ { "device_automation": { + "action_type": { + "brightness_decrease": "Disminuya el brillo de {entity_name}", + "brightness_increase": "Aumenta el brillo de {entity_name}", + "flash": "Destello {entity_name}", + "toggle": "Alternar {entity_name}", + "turn_off": "Desactivar {entity_name}", + "turn_on": "Activar {entity_name}" + }, "condition_type": { "is_off": "{entity_name} est\u00e1 apagada", "is_on": "{entity_name} est\u00e1 encendida" diff --git a/homeassistant/components/light/translations/no.json b/homeassistant/components/light/translations/no.json index b9bd83ba19073a..77ac84c444002a 100644 --- a/homeassistant/components/light/translations/no.json +++ b/homeassistant/components/light/translations/no.json @@ -3,7 +3,7 @@ "action_type": { "brightness_decrease": "Reduser lysstyrken p\u00e5 {entity_name}", "brightness_increase": "\u00d8k lysstyrken p\u00e5 {entity_name}", - "flash": "", + "flash": "Flash {entity_name}", "toggle": "Veksle {entity_name}", "turn_off": "Sl\u00e5 av {entity_name}", "turn_on": "Sl\u00e5 p\u00e5 {entity_name}" diff --git a/homeassistant/components/linky/translations/es-419.json b/homeassistant/components/linky/translations/es-419.json index ff559803e06cbd..58e44695fc8995 100644 --- a/homeassistant/components/linky/translations/es-419.json +++ b/homeassistant/components/linky/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, "error": { "access": "No se pudo acceder a Enedis.fr, compruebe su conexi\u00f3n a Internet.", "enedis": "Enedis.fr respondi\u00f3 con un error: vuelva a intentarlo m\u00e1s tarde (normalmente no entre las 11 p.m. y las 2 a.m.)", diff --git a/homeassistant/components/local_ip/translations/es-419.json b/homeassistant/components/local_ip/translations/es-419.json new file mode 100644 index 00000000000000..ba120a1ce14ac2 --- /dev/null +++ b/homeassistant/components/local_ip/translations/es-419.json @@ -0,0 +1,16 @@ +{ + "config": { + "abort": { + "single_instance_allowed": "Solo se permite una \u00fanica configuraci\u00f3n de IP local." + }, + "step": { + "user": { + "data": { + "name": "Nombre del sensor" + }, + "title": "Direcci\u00f3n IP local" + } + } + }, + "title": "Direcci\u00f3n IP local" +} \ No newline at end of file diff --git a/homeassistant/components/lock/translations/es-419.json b/homeassistant/components/lock/translations/es-419.json index ff9210f96fd57d..09b0932a014639 100644 --- a/homeassistant/components/lock/translations/es-419.json +++ b/homeassistant/components/lock/translations/es-419.json @@ -1,4 +1,19 @@ { + "device_automation": { + "action_type": { + "lock": "Bloquear {entity_name}", + "open": "Abrir {entity_name}", + "unlock": "Desbloquear {entity_name}" + }, + "condition_type": { + "is_locked": "{entity_name} est\u00e1 bloqueado", + "is_unlocked": "{entity_name} est\u00e1 desbloqueado" + }, + "trigger_type": { + "locked": "{entity_name} bloqueado", + "unlocked": "{entity_name} desbloqueado" + } + }, "state": { "_": { "locked": "Cerrado", diff --git a/homeassistant/components/logi_circle/translations/es-419.json b/homeassistant/components/logi_circle/translations/es-419.json index 8cc68833875204..4f512189b32fd6 100644 --- a/homeassistant/components/logi_circle/translations/es-419.json +++ b/homeassistant/components/logi_circle/translations/es-419.json @@ -3,22 +3,27 @@ "abort": { "already_setup": "Solo puede configurar una sola cuenta de Logi Circle.", "external_error": "Se produjo una excepci\u00f3n de otro flujo.", - "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo." + "external_setup": "Logi Circle se configur\u00f3 correctamente desde otro flujo.", + "no_flows": "Debe configurar Logi Circle antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/logi_circle/)." }, "create_entry": { "default": "Autenticado con \u00e9xito con Logi Circle." }, "error": { - "auth_error": "Autorizaci\u00f3n de API fallida." + "auth_error": "Autorizaci\u00f3n de API fallida.", + "auth_timeout": "La autorizaci\u00f3n agot\u00f3 el tiempo de espera al solicitar el token de acceso.", + "follow_link": "Siga el enlace y autent\u00edquese antes de presionar Enviar." }, "step": { "auth": { + "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceda a su cuenta de Logi Circle, luego regrese y presione Enviar continuaci\u00f3n. \n\n[Enlace] ({authorization_url})", "title": "Autenticar con Logi Circle" }, "user": { "data": { "flow_impl": "Proveedor" }, + "description": "Elija a trav\u00e9s del proveedor de autenticaci\u00f3n que desea autenticar con Logi Circle.", "title": "Proveedor de autenticaci\u00f3n" } } diff --git a/homeassistant/components/lutron_caseta/translations/es-419.json b/homeassistant/components/lutron_caseta/translations/es-419.json new file mode 100644 index 00000000000000..970d722fe4cc37 --- /dev/null +++ b/homeassistant/components/lutron_caseta/translations/es-419.json @@ -0,0 +1,3 @@ +{ + "title": "Lutron Cas\u00e9ta" +} \ No newline at end of file diff --git a/homeassistant/components/media_extractor/manifest.json b/homeassistant/components/media_extractor/manifest.json index fd1d2172873d89..28780bb2a8690f 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.24"], + "requirements": ["youtube_dl==2020.05.03"], "dependencies": ["media_player"], "codeowners": [], "quality_scale": "internal" diff --git a/homeassistant/components/media_player/translations/es-419.json b/homeassistant/components/media_player/translations/es-419.json index 667d4af550e14f..518ca4453f0496 100644 --- a/homeassistant/components/media_player/translations/es-419.json +++ b/homeassistant/components/media_player/translations/es-419.json @@ -1,4 +1,13 @@ { + "device_automation": { + "condition_type": { + "is_idle": "{entity_name} est\u00e1 inactivo", + "is_off": "{entity_name} est\u00e1 apagado", + "is_on": "{entity_name} est\u00e1 encendido", + "is_paused": "{entity_name} est\u00e1 en pausa", + "is_playing": "{entity_name} est\u00e1 reproduciendo" + } + }, "state": { "_": { "idle": "Inactivo", diff --git a/homeassistant/components/melcloud/translations/es-419.json b/homeassistant/components/melcloud/translations/es-419.json new file mode 100644 index 00000000000000..0ee2674c8d903e --- /dev/null +++ b/homeassistant/components/melcloud/translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Integraci\u00f3n de MELCloud ya configurada para este correo electr\u00f3nico. El token de acceso se ha actualizado." + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a de MELCloud.", + "username": "Correo electr\u00f3nico utilizado para iniciar sesi\u00f3n en MELCloud." + }, + "description": "Con\u00e9ctese usando su cuenta MELCloud.", + "title": "Conectar con MELCloud" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/melcloud/translations/fr.json b/homeassistant/components/melcloud/translations/fr.json index 0385113787c49e..a567058297e9ca 100644 --- a/homeassistant/components/melcloud/translations/fr.json +++ b/homeassistant/components/melcloud/translations/fr.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "L'int\u00e9gration MELCloud est d\u00e9j\u00e0 configur\u00e9e pour cet e-mail. Le jeton d'acc\u00e8s a \u00e9t\u00e9 actualis\u00e9." + }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", "invalid_auth": "Authentification non valide", diff --git a/homeassistant/components/meteo_france/translations/es-419.json b/homeassistant/components/meteo_france/translations/es-419.json new file mode 100644 index 00000000000000..471b965a8244d5 --- /dev/null +++ b/homeassistant/components/meteo_france/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "La ciudad ya est\u00e1 configurada", + "unknown": "Error desconocido: vuelva a intentarlo m\u00e1s tarde" + }, + "step": { + "user": { + "data": { + "city": "Ciudad" + }, + "description": "Ingrese el c\u00f3digo postal (solo para Francia, recomendado) o el nombre de la ciudad", + "title": "M\u00e9t\u00e9o-Francia" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/microsoft_face/__init__.py b/homeassistant/components/microsoft_face/__init__.py index 16b25e4f85de46..208cecf6d3b945 100644 --- a/homeassistant/components/microsoft_face/__init__.py +++ b/homeassistant/components/microsoft_face/__init__.py @@ -189,7 +189,9 @@ async def async_face_person(service): binary=True, ) except HomeAssistantError as err: - _LOGGER.error("Can't delete person '%s' with error: %s", p_id, err) + _LOGGER.error( + "Can't add an image of a person '%s' with error: %s", p_id, err + ) hass.services.async_register( DOMAIN, SERVICE_FACE_PERSON, async_face_person, schema=SCHEMA_FACE_SERVICE diff --git a/homeassistant/components/mikrotik/translations/es-419.json b/homeassistant/components/mikrotik/translations/es-419.json new file mode 100644 index 00000000000000..1b464bed3930a4 --- /dev/null +++ b/homeassistant/components/mikrotik/translations/es-419.json @@ -0,0 +1,36 @@ +{ + "config": { + "abort": { + "already_configured": "Mikrotik ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "Conexi\u00f3n fallida", + "name_exists": "El nombre existe", + "wrong_credentials": "Credenciales incorrectas" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario", + "verify_ssl": "Usar SSL" + }, + "title": "Configurar el Router Mikrotik" + } + } + }, + "options": { + "step": { + "device_tracker": { + "data": { + "arp_ping": "Habilitar ping ARP", + "detection_time": "Considere el intervalo de inicio", + "force_dhcp": "Escaneo forzado utilizando DHCP" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/mikrotik/translations/no.json b/homeassistant/components/mikrotik/translations/no.json index 6fa745a8b57ed9..1e528fa4986a25 100644 --- a/homeassistant/components/mikrotik/translations/no.json +++ b/homeassistant/components/mikrotik/translations/no.json @@ -18,7 +18,7 @@ "username": "Brukernavn", "verify_ssl": "Bruk ssl" }, - "title": "Konfigurere Mikrotik-ruter" + "title": "Sett opp Mikrotik-ruter" } } }, diff --git a/homeassistant/components/minecraft_server/translations/es-419.json b/homeassistant/components/minecraft_server/translations/es-419.json new file mode 100644 index 00000000000000..46a3ee9a029299 --- /dev/null +++ b/homeassistant/components/minecraft_server/translations/es-419.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "Error al conectar con el servidor. Verifique el host y el puerto e intente nuevamente. Tambi\u00e9n aseg\u00farese de que est\u00e9 ejecutando al menos Minecraft versi\u00f3n 1.7 en su servidor.", + "invalid_ip": "La direcci\u00f3n IP no es v\u00e1lida (no se pudo determinar la direcci\u00f3n MAC). Por favor corr\u00edjalo e intente nuevamente.", + "invalid_port": "El puerto debe estar en el rango de 1024 a 65535. Corr\u00edjalo e int\u00e9ntelo de nuevo." + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre" + }, + "description": "Configure su instancia de Minecraft Server para permitir el monitoreo.", + "title": "Vincule su servidor Minecraft" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/minecraft_server/translations/fr.json b/homeassistant/components/minecraft_server/translations/fr.json index f2bb4dfa3ded19..44bd8230b4a138 100644 --- a/homeassistant/components/minecraft_server/translations/fr.json +++ b/homeassistant/components/minecraft_server/translations/fr.json @@ -3,12 +3,18 @@ "abort": { "already_configured": "L'h\u00f4te est d\u00e9j\u00e0 configur\u00e9." }, + "error": { + "cannot_connect": "\u00c9chec de connexion au serveur. Veuillez v\u00e9rifier l'h\u00f4te et le port et r\u00e9essayer. Assurez-vous \u00e9galement que vous ex\u00e9cutez au moins Minecraft version 1.7 sur votre serveur.", + "invalid_ip": "L'adresse IP n'est pas valide (l'adresse MAC n'a pas pu \u00eatre d\u00e9termin\u00e9e). Veuillez le corriger et r\u00e9essayer.", + "invalid_port": "Le port doit \u00eatre compris entre 1024 et 65535. Veuillez le corriger et r\u00e9essayer." + }, "step": { "user": { "data": { "host": "H\u00f4te", "name": "Nom" }, + "description": "Configurez votre instance Minecraft Server pour permettre la surveillance.", "title": "Reliez votre serveur Minecraft" } } diff --git a/homeassistant/components/minecraft_server/translations/no.json b/homeassistant/components/minecraft_server/translations/no.json index bc3bb2955add7b..4d2ecc6dbaabc2 100644 --- a/homeassistant/components/minecraft_server/translations/no.json +++ b/homeassistant/components/minecraft_server/translations/no.json @@ -14,7 +14,7 @@ "host": "Vert", "name": "Navn" }, - "description": "Konfigurer Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", + "description": "Sett opp Minecraft Server-forekomsten slik at den kan overv\u00e5kes.", "title": "Link din Minecraft Server" } } diff --git a/homeassistant/components/mobile_app/translations/es-419.json b/homeassistant/components/mobile_app/translations/es-419.json index cb666a7d72d2e7..0ad6fb613f9249 100644 --- a/homeassistant/components/mobile_app/translations/es-419.json +++ b/homeassistant/components/mobile_app/translations/es-419.json @@ -5,6 +5,7 @@ }, "step": { "confirm": { + "description": "\u00bfDesea configurar el componente de la aplicaci\u00f3n m\u00f3vil?", "title": "Aplicaci\u00f3n movil" } } diff --git a/homeassistant/components/mobile_app/translations/no.json b/homeassistant/components/mobile_app/translations/no.json index 860d455d582517..131e007751c983 100644 --- a/homeassistant/components/mobile_app/translations/no.json +++ b/homeassistant/components/mobile_app/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "install_app": "\u00c5pne mobilappen for \u00e5 konfigurere integrasjonen med hjemmevirksomheten. Se [docs]({apps_url}) for en liste over kompatible apper." + "install_app": "\u00c5pne mobilappen for \u00e5 sette opp integrasjonen med Home Assistant. Se [dokumentasjon]({apps_url}) for en liste over kompatible apper." }, "step": { "confirm": { diff --git a/homeassistant/components/monoprice/__init__.py b/homeassistant/components/monoprice/__init__.py index 37593f6828e325..d5173d40d8949a 100644 --- a/homeassistant/components/monoprice/__init__.py +++ b/homeassistant/components/monoprice/__init__.py @@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryNotReady -from .const import DOMAIN +from .const import DOMAIN, MONOPRICE_OBJECT, UNDO_UPDATE_LISTENER PLATFORMS = ["media_player"] @@ -28,12 +28,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): 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 - entry.add_update_listener(_update_listener) + undo_listener = entry.add_update_listener(_update_listener) + + hass.data.setdefault(DOMAIN, {})[entry.entry_id] = { + MONOPRICE_OBJECT: monoprice, + UNDO_UPDATE_LISTENER: undo_listener, + } for component in PLATFORMS: hass.async_create_task( @@ -54,6 +58,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): ) ) + if unload_ok: + hass.data[DOMAIN][entry.entry_id][UNDO_UPDATE_LISTENER]() + hass.data[DOMAIN].pop(entry.entry_id) + return unload_ok diff --git a/homeassistant/components/monoprice/config_flow.py b/homeassistant/components/monoprice/config_flow.py index cbabc65a54b014..6c6bc87bf28df2 100644 --- a/homeassistant/components/monoprice/config_flow.py +++ b/homeassistant/components/monoprice/config_flow.py @@ -99,7 +99,9 @@ def async_get_options_flow(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)]) + key = vol.Optional( + source, description={"suggested_value": previous_sources[str(index)]} + ) else: key = vol.Optional(source) diff --git a/homeassistant/components/monoprice/const.py b/homeassistant/components/monoprice/const.py index ea4667a77ffe6c..180ec452831026 100644 --- a/homeassistant/components/monoprice/const.py +++ b/homeassistant/components/monoprice/const.py @@ -13,3 +13,6 @@ SERVICE_SNAPSHOT = "snapshot" SERVICE_RESTORE = "restore" + +MONOPRICE_OBJECT = "monoprice_object" +UNDO_UPDATE_LISTENER = "update_update_listener" diff --git a/homeassistant/components/monoprice/manifest.json b/homeassistant/components/monoprice/manifest.json index c88673b2855375..93cebc9d885462 100644 --- a/homeassistant/components/monoprice/manifest.json +++ b/homeassistant/components/monoprice/manifest.json @@ -3,6 +3,6 @@ "name": "Monoprice 6-Zone Amplifier", "documentation": "https://www.home-assistant.io/integrations/monoprice", "requirements": ["pymonoprice==0.3"], - "codeowners": ["@etsinko"], + "codeowners": ["@etsinko", "@OnFreund"], "config_flow": true } diff --git a/homeassistant/components/monoprice/media_player.py b/homeassistant/components/monoprice/media_player.py index 855d1b41f9886b..985cd88e47f7b4 100644 --- a/homeassistant/components/monoprice/media_player.py +++ b/homeassistant/components/monoprice/media_player.py @@ -16,7 +16,13 @@ from homeassistant.const import CONF_PORT, STATE_OFF, STATE_ON from homeassistant.helpers import config_validation as cv, entity_platform, service -from .const import CONF_SOURCES, DOMAIN, SERVICE_RESTORE, SERVICE_SNAPSHOT +from .const import ( + CONF_SOURCES, + DOMAIN, + MONOPRICE_OBJECT, + SERVICE_RESTORE, + SERVICE_SNAPSHOT, +) _LOGGER = logging.getLogger(__name__) @@ -58,7 +64,7 @@ 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] + monoprice = hass.data[DOMAIN][config_entry.entry_id][MONOPRICE_OBJECT] sources = _get_sources(config_entry) diff --git a/homeassistant/components/monoprice/translations/es-419.json b/homeassistant/components/monoprice/translations/es-419.json new file mode 100644 index 00000000000000..66c7d5417ce35c --- /dev/null +++ b/homeassistant/components/monoprice/translations/es-419.json @@ -0,0 +1,40 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "port": "Puerto serial", + "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" + } + } + }, + "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/moon/translations/sensor.es-419.json b/homeassistant/components/moon/translations/sensor.es-419.json index 107d3e46404216..98242bed3ffbd8 100644 --- a/homeassistant/components/moon/translations/sensor.es-419.json +++ b/homeassistant/components/moon/translations/sensor.es-419.json @@ -3,7 +3,12 @@ "moon__phase": { "first_quarter": "Cuarto creciente", "full_moon": "Luna llena", - "last_quarter": "Cuarto menguante" + "last_quarter": "Cuarto menguante", + "new_moon": "Luna nueva", + "waning_crescent": "Luna menguante", + "waning_gibbous": "Luna menguante gibosa", + "waxing_crescent": "Luna creciente", + "waxing_gibbous": "Luna creciente gibosa" } } } \ No newline at end of file diff --git a/homeassistant/components/mqtt/abbreviations.py b/homeassistant/components/mqtt/abbreviations.py index e4262a7c548246..4b310fb19ecff7 100644 --- a/homeassistant/components/mqtt/abbreviations.py +++ b/homeassistant/components/mqtt/abbreviations.py @@ -159,6 +159,7 @@ "temp_lo_stat_t": "temperature_low_state_topic", "temp_stat_tpl": "temperature_state_template", "temp_stat_t": "temperature_state_topic", + "temp_unit": "temperature_unit", "tilt_clsd_val": "tilt_closed_value", "tilt_cmd_t": "tilt_command_topic", "tilt_inv_stat": "tilt_invert_state", diff --git a/homeassistant/components/mqtt/light/schema_basic.py b/homeassistant/components/mqtt/light/schema_basic.py index 2af63311e4731d..e3405a02c6e213 100644 --- a/homeassistant/components/mqtt/light/schema_basic.py +++ b/homeassistant/components/mqtt/light/schema_basic.py @@ -732,11 +732,13 @@ async def async_turn_on(self, **kwargs): ATTR_BRIGHTNESS in kwargs and self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC] is not None ): - percent_bright = float(kwargs[ATTR_BRIGHTNESS]) / 255 + brightness_normalized = kwargs[ATTR_BRIGHTNESS] / 255 brightness_scale = self._config[CONF_BRIGHTNESS_SCALE] device_brightness = min( - round(percent_bright * brightness_scale), brightness_scale + round(brightness_normalized * brightness_scale), brightness_scale ) + # Make sure the brightness is not rounded down to 0 + device_brightness = max(device_brightness, 1) mqtt.async_publish( self.hass, self._topic[CONF_BRIGHTNESS_COMMAND_TOPIC], diff --git a/homeassistant/components/mqtt/light/schema_json.py b/homeassistant/components/mqtt/light/schema_json.py index ef311cbe8a703b..69bbab3970da2c 100644 --- a/homeassistant/components/mqtt/light/schema_json.py +++ b/homeassistant/components/mqtt/light/schema_json.py @@ -428,9 +428,7 @@ async def async_turn_on(self, **kwargs): if self._brightness is not None: brightness = 255 else: - brightness = kwargs.get( - ATTR_BRIGHTNESS, self._brightness if self._brightness else 255 - ) + brightness = kwargs.get(ATTR_BRIGHTNESS, 255) rgb = color_util.color_hsv_to_RGB( hs_color[0], hs_color[1], brightness / 255 * 100 ) @@ -461,11 +459,14 @@ async def async_turn_on(self, **kwargs): message["transition"] = kwargs[ATTR_TRANSITION] if ATTR_BRIGHTNESS in kwargs and self._brightness is not None: - message["brightness"] = int( - kwargs[ATTR_BRIGHTNESS] - / float(DEFAULT_BRIGHTNESS_SCALE) - * self._config[CONF_BRIGHTNESS_SCALE] + brightness_normalized = kwargs[ATTR_BRIGHTNESS] / DEFAULT_BRIGHTNESS_SCALE + brightness_scale = self._config[CONF_BRIGHTNESS_SCALE] + device_brightness = min( + round(brightness_normalized * brightness_scale), brightness_scale ) + # Make sure the brightness is not rounded down to 0 + device_brightness = max(device_brightness, 1) + message["brightness"] = device_brightness if self._optimistic: self._brightness = kwargs[ATTR_BRIGHTNESS] diff --git a/homeassistant/components/mqtt/translations/es-419.json b/homeassistant/components/mqtt/translations/es-419.json index af0bd912bc7359..afb55aca1ce1f6 100644 --- a/homeassistant/components/mqtt/translations/es-419.json +++ b/homeassistant/components/mqtt/translations/es-419.json @@ -26,5 +26,27 @@ "title": "MQTT Broker a trav\u00e9s del complemento Hass.io" } } + }, + "device_automation": { + "trigger_subtype": { + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "turn_off": "Apagar", + "turn_on": "Encender" + }, + "trigger_type": { + "button_double_press": "\"{subtype}\" pulsado 2 veces", + "button_long_press": "\"{subtype}\" pulsado continuamente", + "button_long_release": "Se solt\u00f3 \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "button_quadruple_press": "\"{subtype}\" pulsado 4 veces", + "button_quintuple_press": "\"{subtype}\" pulsado 5 veces", + "button_short_press": "\"{subtype}\" presionado", + "button_short_release": "\"{subtype}\" soltado", + "button_triple_press": "\"{subtype}\" pulsado 3 veces" + } } } \ No newline at end of file diff --git a/homeassistant/components/myq/translations/es-419.json b/homeassistant/components/myq/translations/es-419.json new file mode 100644 index 00000000000000..3d2f7a6ea719ce --- /dev/null +++ b/homeassistant/components/myq/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "MyQ ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a MyQ Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/neato/translations/es-419.json b/homeassistant/components/neato/translations/es-419.json new file mode 100644 index 00000000000000..a6a684597f6b64 --- /dev/null +++ b/homeassistant/components/neato/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Ya est\u00e1 configurado", + "invalid_credentials": "Credenciales no v\u00e1lidas" + }, + "create_entry": { + "default": "Consulte [Documentaci\u00f3n de Neato] ({docs_url})." + }, + "error": { + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unexpected_error": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario", + "vendor": "Vendedor" + }, + "description": "Consulte [Documentaci\u00f3n de Neato] ({docs_url}).", + "title": "Informaci\u00f3n de cuenta de Neato" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nest/translations/es-419.json b/homeassistant/components/nest/translations/es-419.json index d816813855c120..f7e19809f6b3d7 100644 --- a/homeassistant/components/nest/translations/es-419.json +++ b/homeassistant/components/nest/translations/es-419.json @@ -3,10 +3,13 @@ "abort": { "already_setup": "Solo puedes configurar una sola cuenta Nest.", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "no_flows": "Debe configurar Nest antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/nest/)." }, "error": { + "internal_error": "C\u00f3digo de validaci\u00f3n de error interno", "invalid_code": "Codigo invalido", + "timeout": "Tiempo de espera agotado para validar el c\u00f3digo", "unknown": "Error desconocido al validar el c\u00f3digo" }, "step": { diff --git a/homeassistant/components/netatmo/strings.json b/homeassistant/components/netatmo/strings.json index e25ca1e58495e4..2d41f560cff524 100644 --- a/homeassistant/components/netatmo/strings.json +++ b/homeassistant/components/netatmo/strings.json @@ -1,13 +1,17 @@ { "config": { "step": { - "pick_implementation": { "title": "Pick Authentication Method" } + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } }, "abort": { - "already_setup": "You can only configure one Netatmo account.", - "authorize_url_timeout": "Timeout generating authorize url.", - "missing_configuration": "The Netatmo component is not configured. Please follow the documentation." + "already_setup": "[%key:common::config_flow::abort::single_instance_allowed%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]" }, - "create_entry": { "default": "Successfully authenticated with Netatmo." } + "create_entry": { + "default": "[%key:common::config_flow::create_entry::authenticated%]" + } } } diff --git a/homeassistant/components/netatmo/translations/es-419.json b/homeassistant/components/netatmo/translations/es-419.json new file mode 100644 index 00000000000000..d6f993e830e834 --- /dev/null +++ b/homeassistant/components/netatmo/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta de Netatmo.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente Netatmo no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Netatmo." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nexia/translations/es-419.json b/homeassistant/components/nexia/translations/es-419.json new file mode 100644 index 00000000000000..e2f04c7d4b4dd7 --- /dev/null +++ b/homeassistant/components/nexia/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "Esta casa de nexia ya est\u00e1 configurada" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a mynexia.com" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/notion/__init__.py b/homeassistant/components/notion/__init__.py index 5e8450ffdea0a5..77ab68d8e70317 100644 --- a/homeassistant/components/notion/__init__.py +++ b/homeassistant/components/notion/__init__.py @@ -196,7 +196,14 @@ async def async_update(self): results = await asyncio.gather(*tasks.values(), return_exceptions=True) for attr, result in zip(tasks, results): if isinstance(result, NotionError): - _LOGGER.error("There was an error while updating %s: %s", attr, result) + _LOGGER.error( + "There was a Notion error while updating %s: %s", attr, result + ) + continue + if isinstance(result, Exception): + _LOGGER.error( + "There was an unknown error while updating %s: %s", attr, result + ) continue holding_pen = getattr(self, attr) diff --git a/homeassistant/components/notion/translations/es-419.json b/homeassistant/components/notion/translations/es-419.json index 6e0e7eb538f909..a34d03562618d1 100644 --- a/homeassistant/components/notion/translations/es-419.json +++ b/homeassistant/components/notion/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Este nombre de usuario ya est\u00e1 en uso." + }, "error": { "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/nuheat/translations/es-419.json b/homeassistant/components/nuheat/translations/es-419.json new file mode 100644 index 00000000000000..88e786c8b906cd --- /dev/null +++ b/homeassistant/components/nuheat/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_configured": "El termostato ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\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 ID num\u00e9rico de su termostato iniciando sesi\u00f3n en https://MyNuHeat.com y seleccionando su (s) termostato (s).", + "title": "Con\u00e9ctese a NuHeat" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nut/translations/es-419.json b/homeassistant/components/nut/translations/es-419.json new file mode 100644 index 00000000000000..b33bc854b23f87 --- /dev/null +++ b/homeassistant/components/nut/translations/es-419.json @@ -0,0 +1,46 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "resources": { + "data": { + "resources": "Recursos" + }, + "title": "Seleccione los recursos para monitorear" + }, + "ups": { + "data": { + "alias": "Alias", + "resources": "Recursos" + }, + "title": "Seleccione el UPS para monitorear" + }, + "user": { + "data": { + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese al servidor NUT" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "resources": "Recursos", + "scan_interval": "Intervalo de escaneo (segundos)" + }, + "description": "Seleccione los Recursos del sensor." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/nws/translations/es-419.json b/homeassistant/components/nws/translations/es-419.json new file mode 100644 index 00000000000000..a44b2899e3d4b1 --- /dev/null +++ b/homeassistant/components/nws/translations/es-419.json @@ -0,0 +1,23 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "Clave API (correo electr\u00f3nico)", + "latitude": "Latitud", + "longitude": "Longitud", + "station": "C\u00f3digo de estaci\u00f3n METAR" + }, + "description": "Si no se especifica un c\u00f3digo de estaci\u00f3n METAR, la latitud y la longitud se utilizar\u00e1n para encontrar la estaci\u00f3n m\u00e1s cercana.", + "title": "Con\u00e9ctese al Servicio Meteorol\u00f3gico Nacional" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/de.json b/homeassistant/components/onvif/translations/de.json new file mode 100644 index 00000000000000..2a8dcb1b67b5a7 --- /dev/null +++ b/homeassistant/components/onvif/translations/de.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Das ONVIF-Ger\u00e4t ist bereits konfiguriert.", + "already_in_progress": "Der Konfigurationsfluss f\u00fcr das ONVIF-Ger\u00e4t wird bereits ausgef\u00fchrt.", + "no_h264": "Es waren keine H264-Streams verf\u00fcgbar. \u00dcberpr\u00fcfen Sie die Profilkonfiguration auf Ihrem Ger\u00e4t.", + "no_mac": "Die eindeutige ID f\u00fcr das ONVIF-Ger\u00e4t konnte nicht konfiguriert werden.", + "onvif_error": "Fehler beim Einrichten des ONVIF-Ger\u00e4ts. \u00dcberpr\u00fcfen Sie die Protokolle auf weitere Informationen." + }, + "error": { + "connection_failed": "Es konnte keine Verbindung zum ONVIF-Dienst mit den angegebenen Anmeldeinformationen hergestellt werden." + }, + "step": { + "auth": { + "data": { + "password": "Passwort", + "username": "Benutzername" + }, + "title": "Konfigurieren Sie die Authentifizierung" + }, + "configure_profile": { + "data": { + "include": "Kameraentit\u00e4t erstellen" + }, + "description": "Kameraentit\u00e4t f\u00fcr {profile} mit {resolution} Aufl\u00f6sung erstellen?", + "title": "Profile konfigurieren" + }, + "device": { + "data": { + "host": "W\u00e4hlen Sie das erkannte ONVIF-Ger\u00e4t aus" + }, + "title": "W\u00e4hlen Sie ONVIF-Ger\u00e4t" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Port" + }, + "title": "Konfigurieren Sie das ONVIF-Ger\u00e4t" + }, + "user": { + "description": "Wenn Sie auf Senden klicken, durchsuchen wir Ihr Netzwerk nach ONVIF-Ger\u00e4ten, die Profil S unterst\u00fctzen. \n\nEinige Hersteller haben begonnen, ONVIF standardm\u00e4\u00dfig zu deaktivieren. Stellen Sie sicher, dass ONVIF in der Konfiguration Ihrer Kamera aktiviert ist.", + "title": "ONVIF-Ger\u00e4tekonfiguration" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Zus\u00e4tzliche FFMPEG-Argumente", + "rtsp_transport": "RTSP-Transportmechanismus" + }, + "title": "ONVIF-Ger\u00e4teoptionen" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/es-419.json b/homeassistant/components/onvif/translations/es-419.json new file mode 100644 index 00000000000000..823f1d158801da --- /dev/null +++ b/homeassistant/components/onvif/translations/es-419.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ONVIF ya est\u00e1 configurado.", + "already_in_progress": "El flujo de configuraci\u00f3n para el dispositivo ONVIF ya est\u00e1 en progreso.", + "no_h264": "No hab\u00eda transmisiones H264 disponibles. Verifique la configuraci\u00f3n del perfil en su dispositivo.", + "no_mac": "No se pudo configurar una identificaci\u00f3n \u00fanica para el dispositivo ONVIF.", + "onvif_error": "Error al configurar el dispositivo ONVIF. Consulte los registros para obtener m\u00e1s informaci\u00f3n." + }, + "error": { + "connection_failed": "No se pudo conectar al servicio ONVIF con las credenciales proporcionadas." + }, + "step": { + "auth": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Configurar autenticaci\u00f3n" + }, + "configure_profile": { + "data": { + "include": "Crear entidad de c\u00e1mara" + }, + "description": "\u00bfCrear entidad de c\u00e1mara para {profile} con una {resolution}?", + "title": "Configurar perfiles" + }, + "device": { + "data": { + "host": "Seleccione el dispositivo ONVIF descubierto" + }, + "title": "Seleccionar dispositivo ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "title": "Configurar dispositivo ONVIF" + }, + "user": { + "description": "Al hacer clic en enviar, buscaremos en su red dispositivos ONVIF que admitan Profile S. \n\nAlgunos fabricantes han comenzado a deshabilitar ONVIF por defecto. Aseg\u00farese de que ONVIF est\u00e9 habilitado en la configuraci\u00f3n de su c\u00e1mara.", + "title": "Configuraci\u00f3n del dispositivo ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argumentos adicionales de FFMPEG", + "rtsp_transport": "Mecanismo de transporte RTSP" + }, + "title": "Opciones de dispositivo ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/onvif/translations/fr.json b/homeassistant/components/onvif/translations/fr.json index 38036b65517314..b383aad3532f7b 100644 --- a/homeassistant/components/onvif/translations/fr.json +++ b/homeassistant/components/onvif/translations/fr.json @@ -38,7 +38,7 @@ "title": "Configurer l\u2019appareil ONVIF" }, "user": { - "description": "En cliquant sur soumettre, nous rechercherons sur votre r\u00e9seau, des \u00e9quipements ONVIF qui supporte le Profile S.\n\nCertains constructeurs ont commenc\u00e9 \u00e0 d\u00e9sactiver ONvif par d\u00e9faut. Assurez vous que ONVIF est activ\u00e9 dans la configuration de votre cam\u00e9ra", + "description": "En cliquant sur soumettre, nous rechercherons sur votre r\u00e9seau, des \u00e9quipements ONVIF qui supporte le Profile S.\n\nCertains constructeurs ont commenc\u00e9 \u00e0 d\u00e9sactiver ONvif par d\u00e9faut. Assurez-vous qu\u2019ONVIF est activ\u00e9 dans la configuration de votre cam\u00e9ra", "title": "Configuration de l'appareil ONVIF" } } diff --git a/homeassistant/components/onvif/translations/it.json b/homeassistant/components/onvif/translations/it.json new file mode 100644 index 00000000000000..689babb357c82f --- /dev/null +++ b/homeassistant/components/onvif/translations/it.json @@ -0,0 +1,58 @@ +{ + "config": { + "abort": { + "already_configured": "Il dispositivo ONVIF \u00e8 gi\u00e0 configurato.", + "already_in_progress": "Il flusso di configurazione per il dispositivo ONVIF \u00e8 gi\u00e0 in corso.", + "no_h264": "Non c'erano flussi H264 disponibili. Controllare la configurazione del profilo sul dispositivo.", + "no_mac": "Impossibile configurare l'ID univoco per il dispositivo ONVIF.", + "onvif_error": "Errore durante la configurazione del dispositivo ONVIF. Controllare i registri per ulteriori informazioni." + }, + "error": { + "connection_failed": "Impossibile connettersi al servizio ONVIF con le credenziali fornite." + }, + "step": { + "auth": { + "data": { + "password": "Password", + "username": "Nome utente" + }, + "title": "Configurare l'autenticazione" + }, + "configure_profile": { + "data": { + "include": "Crea entit\u00e0 telecamera" + }, + "description": "Creare un'entit\u00e0 telecamera per {profile} alla risoluzione {resolution}?", + "title": "Configurare i Profili" + }, + "device": { + "data": { + "host": "Seleziona il dispositivo ONVIF rilevato" + }, + "title": "Selezionare il dispositivo ONVIF" + }, + "manual_input": { + "data": { + "host": "Host", + "port": "Porta" + }, + "title": "Configurare il dispositivo ONVIF" + }, + "user": { + "description": "Facendo clic su Invia, cercheremo nella tua rete i dispositivi ONVIF che supportano il profilo S. \n\nAlcuni produttori hanno iniziato a disabilitare ONVIF per impostazione predefinita. Assicurati che ONVIF sia abilitato nella configurazione della tua telecamera.", + "title": "Configurazione del dispositivo ONVIF" + } + } + }, + "options": { + "step": { + "onvif_devices": { + "data": { + "extra_arguments": "Argomenti FFMPEG aggiuntivi", + "rtsp_transport": "Meccanismo di trasporto RTSP" + }, + "title": "Opzioni dispositivo ONVIF" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/opencv/manifest.json b/homeassistant/components/opencv/manifest.json index ed54c7d05ee718..37398c61686d98 100644 --- a/homeassistant/components/opencv/manifest.json +++ b/homeassistant/components/opencv/manifest.json @@ -2,6 +2,6 @@ "domain": "opencv", "name": "OpenCV", "documentation": "https://www.home-assistant.io/integrations/opencv", - "requirements": ["numpy==1.18.2", "opencv-python-headless==4.2.0.32"], + "requirements": ["numpy==1.18.4", "opencv-python-headless==4.2.0.32"], "codeowners": [] } diff --git a/homeassistant/components/opentherm_gw/translations/es-419.json b/homeassistant/components/opentherm_gw/translations/es-419.json new file mode 100644 index 00000000000000..9338998d377d6d --- /dev/null +++ b/homeassistant/components/opentherm_gw/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "error": { + "already_configured": "Gateway ya configurado", + "id_exists": "La identificaci\u00f3n de la puerta ya existe", + "serial_error": "Error al conectarse al dispositivo", + "timeout": "Tiempo de intento de conexi\u00f3n agotado" + }, + "step": { + "init": { + "data": { + "device": "Ruta o URL", + "id": "Identificaci\u00f3n", + "name": "Nombre" + }, + "title": "OpenTherm Gateway" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "floor_temperature": "Temperatura del piso", + "precision": "Precisi\u00f3n" + }, + "description": "Opciones para OpenTherm Gateway" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/panasonic_viera/translations/es-419.json b/homeassistant/components/panasonic_viera/translations/es-419.json new file mode 100644 index 00000000000000..d396fc58856746 --- /dev/null +++ b/homeassistant/components/panasonic_viera/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Esta televisi\u00f3n Panasonic Viera ya est\u00e1 configurada.", + "not_connected": "Se perdi\u00f3 la conexi\u00f3n remota con su televisi\u00f3n Panasonic Viera. Consulte los registros para obtener m\u00e1s informaci\u00f3n.", + "unknown": "Un error desconocido ocurri\u00f3. Consulte los registros para obtener m\u00e1s informaci\u00f3n." + }, + "error": { + "invalid_pin_code": "El c\u00f3digo PIN que ingres\u00f3 no es v\u00e1lido", + "not_connected": "No se pudo establecer una conexi\u00f3n remota con su televisi\u00f3n Panasonic Viera" + }, + "step": { + "pairing": { + "data": { + "pin": "PIN" + }, + "description": "Ingrese el PIN que se muestra en su televisi\u00f3n", + "title": "Emparejamiento" + }, + "user": { + "data": { + "host": "Direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Ingrese la direcci\u00f3n IP de su Panasonic Viera TV", + "title": "Configurar su televisi\u00f3n" + } + } + }, + "title": "Panasonic Viera" +} \ No newline at end of file diff --git a/homeassistant/components/plex/config_flow.py b/homeassistant/components/plex/config_flow.py index a314aba0ecdf22..ff6231b0586e2b 100644 --- a/homeassistant/components/plex/config_flow.py +++ b/homeassistant/components/plex/config_flow.py @@ -11,7 +11,14 @@ from homeassistant import config_entries from homeassistant.components.http.view import HomeAssistantView from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL +from homeassistant.const import ( + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_TOKEN, + CONF_URL, + CONF_VERIFY_SSL, +) from homeassistant.core import callback from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv @@ -19,14 +26,18 @@ from .const import ( # pylint: disable=unused-import AUTH_CALLBACK_NAME, AUTH_CALLBACK_PATH, + AUTOMATIC_SETUP_STRING, CONF_CLIENT_IDENTIFIER, CONF_IGNORE_NEW_SHARED_USERS, CONF_MONITORED_USERS, CONF_SERVER, CONF_SERVER_IDENTIFIER, CONF_USE_EPISODE_ART, + DEFAULT_PORT, + DEFAULT_SSL, DEFAULT_VERIFY_SSL, DOMAIN, + MANUAL_SETUP_STRING, PLEX_SERVER_CONFIG, SERVERS, X_PLEX_DEVICE_NAME, @@ -68,14 +79,77 @@ def __init__(self): self.plexauth = None self.token = None self.client_id = None + self._manual = False - async def async_step_user(self, user_input=None): + async def async_step_user(self, user_input=None, errors=None): """Handle a flow initialized by the user.""" - return self.async_show_form(step_id="start_website_auth") + if user_input is not None: + return await self.async_step_plex_website_auth() + if self.show_advanced_options: + return await self.async_step_user_advanced(errors=errors) + return self.async_show_form(step_id="user", errors=errors) - async def async_step_start_website_auth(self, user_input=None): - """Show a form before starting external authentication.""" - return await self.async_step_plex_website_auth() + async def async_step_user_advanced(self, user_input=None, errors=None): + """Handle an advanced mode flow initialized by the user.""" + if user_input is not None: + if user_input.get("setup_method") == MANUAL_SETUP_STRING: + self._manual = True + return await self.async_step_manual_setup() + return await self.async_step_plex_website_auth() + + data_schema = vol.Schema( + { + vol.Required("setup_method", default=AUTOMATIC_SETUP_STRING): vol.In( + [AUTOMATIC_SETUP_STRING, MANUAL_SETUP_STRING] + ) + } + ) + return self.async_show_form( + step_id="user_advanced", data_schema=data_schema, errors=errors + ) + + async def async_step_manual_setup(self, user_input=None, errors=None): + """Begin manual configuration.""" + if user_input is not None and errors is None: + user_input.pop(CONF_URL, None) + host = user_input.get(CONF_HOST) + if host: + port = user_input[CONF_PORT] + prefix = "https" if user_input.get(CONF_SSL) else "http" + user_input[CONF_URL] = f"{prefix}://{host}:{port}" + elif CONF_TOKEN not in user_input: + return await self.async_step_manual_setup( + user_input=user_input, errors={"base": "host_or_token"} + ) + return await self.async_step_server_validate(user_input) + + previous_input = user_input or {} + + data_schema = vol.Schema( + { + vol.Optional( + CONF_HOST, + description={"suggested_value": previous_input.get(CONF_HOST)}, + ): str, + vol.Required( + CONF_PORT, default=previous_input.get(CONF_PORT, DEFAULT_PORT) + ): int, + vol.Required( + CONF_SSL, default=previous_input.get(CONF_SSL, DEFAULT_SSL) + ): bool, + vol.Required( + CONF_VERIFY_SSL, + default=previous_input.get(CONF_VERIFY_SSL, DEFAULT_VERIFY_SSL), + ): bool, + vol.Optional( + CONF_TOKEN, + description={"suggested_value": previous_input.get(CONF_TOKEN)}, + ): str, + } + ) + return self.async_show_form( + step_id="manual_setup", data_schema=data_schema, errors=errors + ) async def async_step_server_validate(self, server_config): """Validate a provided configuration.""" @@ -95,13 +169,16 @@ async def async_step_server_validate(self, server_config): errors["base"] = "no_servers" except (plexapi.exceptions.BadRequest, plexapi.exceptions.Unauthorized): _LOGGER.error("Invalid credentials provided, config not created") - errors["base"] = "faulty_credentials" + errors[CONF_TOKEN] = "faulty_credentials" + except requests.exceptions.SSLError as error: + _LOGGER.error("SSL certificate error: [%s]", error) + errors["base"] = "ssl_error" except (plexapi.exceptions.NotFound, requests.exceptions.ConnectionError): server_identifier = ( server_config.get(CONF_URL) or plex_server.server_choice or "Unknown" ) _LOGGER.error("Plex server could not be reached: %s", server_identifier) - errors["base"] = "not_found" + errors[CONF_HOST] = "not_found" except ServerNotSpecified as available_servers: if is_importing: @@ -119,7 +196,11 @@ async def async_step_server_validate(self, server_config): if errors: if is_importing: return self.async_abort(reason="non-interactive") - return self.async_show_form(step_id="start_website_auth", errors=errors) + if self._manual: + return await self.async_step_manual_setup( + user_input=server_config, errors=errors + ) + return await self.async_step_user(errors=errors) server_id = plex_server.machine_identifier diff --git a/homeassistant/components/plex/const.py b/homeassistant/components/plex/const.py index 555454e22050f0..416c994d2bef48 100644 --- a/homeassistant/components/plex/const.py +++ b/homeassistant/components/plex/const.py @@ -39,3 +39,6 @@ X_PLEX_PLATFORM = "Home Assistant" X_PLEX_PRODUCT = "Home Assistant" X_PLEX_VERSION = __version__ + +AUTOMATIC_SETUP_STRING = "Obtain a new token from plex.tv" +MANUAL_SETUP_STRING = "Configure Plex server manually" diff --git a/homeassistant/components/plex/server.py b/homeassistant/components/plex/server.py index dc252d57410b83..e62be0244fe9f4 100644 --- a/homeassistant/components/plex/server.py +++ b/homeassistant/components/plex/server.py @@ -131,6 +131,8 @@ def _update_plexdirect_hostname(): ) _update_plexdirect_hostname() config_entry_update_needed = True + else: + raise else: raise else: diff --git a/homeassistant/components/plex/strings.json b/homeassistant/components/plex/strings.json index 962e8d35225d2a..71e3db0fdbbf81 100644 --- a/homeassistant/components/plex/strings.json +++ b/homeassistant/components/plex/strings.json @@ -1,20 +1,38 @@ { "config": { "step": { + "user": { + "title": "Plex Media Server", + "description": "Continue to [plex.tv](https://plex.tv) to link a Plex server." + }, + "user_advanced": { + "title": "Plex Media Server", + "data": { + "setup_method": "Setup method" + } + }, + "manual_setup": { + "title": "Manual Plex Configuration", + "data": { + "host": "Host (Optional if Token provided)", + "port": "Port", + "ssl": "Use SSL", + "verify_ssl": "Verify SSL certificate", + "token": "Token (Optional)" + } + }, "select_server": { "title": "Select Plex server", "description": "Multiple servers available, select one:", "data": { "server": "Server" } - }, - "start_website_auth": { - "title": "Connect Plex server", - "description": "Continue to authorize at plex.tv." } }, "error": { - "faulty_credentials": "Authorization failed", - "no_servers": "No servers linked to account", - "not_found": "Plex server not found" + "faulty_credentials": "Authorization failed, verify Token", + "host_or_token": "Must provide at least one of Host or Token", + "no_servers": "No servers linked to Plex account", + "not_found": "Plex server not found", + "ssl_error": "SSL certificate issue" }, "abort": { "all_configured": "All linked servers already configured", diff --git a/homeassistant/components/plex/translations/es-419.json b/homeassistant/components/plex/translations/es-419.json index 8fa797aec89bb7..72cd3deefbbe48 100644 --- a/homeassistant/components/plex/translations/es-419.json +++ b/homeassistant/components/plex/translations/es-419.json @@ -5,6 +5,7 @@ "already_configured": "Este servidor Plex ya est\u00e1 configurado", "already_in_progress": "Plex se est\u00e1 configurando", "invalid_import": "La configuraci\u00f3n importada no es v\u00e1lida", + "non-interactive": "Importaci\u00f3n no interactiva", "token_request_timeout": "Se agot\u00f3 el tiempo de espera para obtener el token", "unknown": "Fall\u00f3 por razones desconocidas" }, @@ -20,12 +21,21 @@ }, "description": "M\u00faltiples servidores disponibles, seleccione uno:", "title": "Seleccionar servidor Plex" + }, + "start_website_auth": { + "description": "Continuar autorizando en plex.tv.", + "title": "Conectar a servidor Plex" } } }, "options": { "step": { "plex_mp_settings": { + "data": { + "ignore_new_shared_users": "Ignorar nuevos usuarios administrados/compartidos", + "monitored_users": "Usuarios monitoreados", + "use_episode_art": "Usa el arte del episodio" + }, "description": "Opciones para reproductores multimedia Plex" } } diff --git a/homeassistant/components/point/translations/es-419.json b/homeassistant/components/point/translations/es-419.json index 2b6e51ba4a9e9e..2b177e2682556a 100644 --- a/homeassistant/components/point/translations/es-419.json +++ b/homeassistant/components/point/translations/es-419.json @@ -3,7 +3,12 @@ "abort": { "already_setup": "Solo puede configurar una cuenta Point.", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", - "external_setup": "Punto configurado con \u00e9xito desde otro flujo." + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "external_setup": "Punto configurado con \u00e9xito desde otro flujo.", + "no_flows": "Debe configurar Point antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/point/)." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Minut para sus dispositivos Point" }, "error": { "follow_link": "Por favor, siga el enlace y autent\u00edquese antes de presionar Enviar", @@ -11,7 +16,8 @@ }, "step": { "auth": { - "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceso a su cuenta de Minut, luego vuelva y presione Enviar continuaci\u00f3n. \n\n [Enlace] ( {authorization_url} )" + "description": "Siga el enlace a continuaci\u00f3n y Aceptar acceso a su cuenta de Minut, luego vuelva y presione Enviar continuaci\u00f3n. \n\n [Enlace] ( {authorization_url} )", + "title": "Autenticaci\u00f3n Point" }, "user": { "data": { diff --git a/homeassistant/components/powerwall/translations/es-419.json b/homeassistant/components/powerwall/translations/es-419.json new file mode 100644 index 00000000000000..fe8f6d061a986a --- /dev/null +++ b/homeassistant/components/powerwall/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El powerwall ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado", + "wrong_version": "Su powerwall utiliza una versi\u00f3n de software que no es compatible. Considere actualizar o informar este problema para que pueda resolverse." + }, + "step": { + "user": { + "data": { + "ip_address": "Direcci\u00f3n IP" + }, + "title": "Conectar el powerwall" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/powerwall/translations/fr.json b/homeassistant/components/powerwall/translations/fr.json index d7e0c759bbfe1e..3ddc6634557258 100644 --- a/homeassistant/components/powerwall/translations/fr.json +++ b/homeassistant/components/powerwall/translations/fr.json @@ -5,7 +5,8 @@ }, "error": { "cannot_connect": "Impossible de se connecter, veuillez r\u00e9essayer", - "unknown": "Erreur inattendue" + "unknown": "Erreur inattendue", + "wrong_version": "Votre Powerwall utilise une version logicielle qui n'est pas prise en charge. Veuillez envisager de mettre \u00e0 niveau ou de signaler ce probl\u00e8me afin qu'il puisse \u00eatre r\u00e9solu." }, "step": { "user": { diff --git a/homeassistant/components/proliphix/climate.py b/homeassistant/components/proliphix/climate.py index fc44dfba75ba4e..5dff4725ea0d42 100644 --- a/homeassistant/components/proliphix/climate.py +++ b/homeassistant/components/proliphix/climate.py @@ -4,6 +4,10 @@ from homeassistant.components.climate import PLATFORM_SCHEMA, ClimateEntity from homeassistant.components.climate.const import ( + CURRENT_HVAC_COOL, + CURRENT_HVAC_HEAT, + CURRENT_HVAC_IDLE, + CURRENT_HVAC_OFF, HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_OFF, @@ -37,6 +41,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): host = config.get(CONF_HOST) pdp = proliphix.PDP(host, username, password) + pdp.update() add_entities([ProliphixThermostat(pdp)], True) @@ -47,7 +52,7 @@ class ProliphixThermostat(ClimateEntity): def __init__(self, pdp): """Initialize the thermostat.""" self._pdp = pdp - self._name = self._pdp.name + self._name = None @property def supported_features(self): @@ -62,6 +67,7 @@ def should_poll(self): def update(self): """Update the data from the thermostat.""" self._pdp.update() + self._name = self._pdp.name @property def name(self): @@ -97,16 +103,26 @@ def target_temperature(self): """Return the temperature we try to reach.""" return self._pdp.setback + @property + def hvac_action(self): + """Return the current state of the thermostat.""" + state = self._pdp.hvac_state + if state == 1: + return CURRENT_HVAC_OFF + if state in (3, 4, 5): + return CURRENT_HVAC_HEAT + if state in (6, 7): + return CURRENT_HVAC_COOL + return CURRENT_HVAC_IDLE + @property def hvac_mode(self): """Return the current state of the thermostat.""" - state = self._pdp.hvac_mode - if state in (1, 2): - return HVAC_MODE_OFF - if state == 3: + if self._pdp.is_heating: return HVAC_MODE_HEAT - if state == 6: + if self._pdp.is_cooling: return HVAC_MODE_COOL + return HVAC_MODE_OFF @property def hvac_modes(self): diff --git a/homeassistant/components/ps4/translations/es-419.json b/homeassistant/components/ps4/translations/es-419.json index eabbc340cc8cad..c718c4469aab5d 100644 --- a/homeassistant/components/ps4/translations/es-419.json +++ b/homeassistant/components/ps4/translations/es-419.json @@ -8,7 +8,9 @@ "port_997_bind_error": "No se pudo enlazar al puerto 997." }, "error": { + "credential_timeout": "Servicio de credenciales agotado. Presione enviar para reiniciar.", "login_failed": "No se ha podido emparejar con PlayStation 4. Verifique que el PIN sea correcto.", + "no_ipaddress": "Ingrese la direcci\u00f3n IP de la PlayStation 4 que desea configurar.", "not_ready": "PlayStation 4 no est\u00e1 encendida o conectada a la red." }, "step": { @@ -28,8 +30,10 @@ }, "mode": { "data": { + "ip_address": "Direcci\u00f3n IP (dejar en blanco si se utiliza el descubrimiento autom\u00e1tico).", "mode": "Modo de configuraci\u00f3n" }, + "description": "Seleccione el modo para la configuraci\u00f3n. El campo Direcci\u00f3n IP puede dejarse en blanco si selecciona Descubrimiento autom\u00e1tico, ya que los dispositivos se descubrir\u00e1n autom\u00e1ticamente.", "title": "Playstation 4" } } diff --git a/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json b/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json new file mode 100644 index 00000000000000..ed6ab16c8b5ead --- /dev/null +++ b/homeassistant/components/pvpc_hourly_pricing/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "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 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. \n Para obtener una explicaci\u00f3n m\u00e1s precisa, visite los [documentos de integraci\u00f3n] (https://www.home-assistant.io/integrations/pvpc_hourly_pricing/). \n\nSeleccione la tarifa contratada en funci\u00f3n de la cantidad 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" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/pyload/sensor.py b/homeassistant/components/pyload/sensor.py index abcf1193ce476a..6539479d2cd08a 100644 --- a/homeassistant/components/pyload/sensor.py +++ b/homeassistant/components/pyload/sensor.py @@ -60,7 +60,6 @@ def setup_platform(hass, config, add_entities, discovery_info=None): try: pyloadapi = PyLoadAPI(api_url=url, username=username, password=password) - pyloadapi.update() except ( requests.exceptions.ConnectionError, requests.exceptions.HTTPError, @@ -144,17 +143,11 @@ def __init__(self, api_url, username=None, password=None): self.login = requests.post(f"{api_url}login", data=self.payload, timeout=5) self.update() - def post(self, method, params=None): + def post(self): """Send a POST request and return the response as a dict.""" - payload = {"method": method} - - if params: - payload["params"] = params - try: response = requests.post( f"{self.api_url}statusServer", - json=payload, cookies=self.login.cookies, headers=self.headers, timeout=5, @@ -170,4 +163,4 @@ def post(self, method, params=None): @Throttle(MIN_TIME_BETWEEN_UPDATES) def update(self): """Update cached response.""" - self.status = self.post("speed") + self.status = self.post() diff --git a/homeassistant/components/rachio/translations/es-419.json b/homeassistant/components/rachio/translations/es-419.json new file mode 100644 index 00000000000000..21186710e3ddc0 --- /dev/null +++ b/homeassistant/components/rachio/translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "api_key": "La clave API para la cuenta Rachio." + }, + "description": "Necesitar\u00e1 la clave API de https://app.rach.io/. Seleccione 'Configuraci\u00f3n de la cuenta y luego haga clic en' OBTENER CLAVE API '.", + "title": "Con\u00e9ctese a su dispositivo Rachio" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "manual_run_mins": "Por cu\u00e1nto tiempo, en minutos, encender una estaci\u00f3n cuando el interruptor est\u00e1 habilitado." + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/rainmachine/translations/es-419.json b/homeassistant/components/rainmachine/translations/es-419.json index 73da8c7c1d4f02..0767c509bf9bdc 100644 --- a/homeassistant/components/rainmachine/translations/es-419.json +++ b/homeassistant/components/rainmachine/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Este controlador RainMachine ya est\u00e1 configurado." + }, "error": { "identifier_exists": "Cuenta ya registrada", "invalid_credentials": "Credenciales no v\u00e1lidas" diff --git a/homeassistant/components/ring/translations/es-419.json b/homeassistant/components/ring/translations/es-419.json new file mode 100644 index 00000000000000..331769fbb4c742 --- /dev/null +++ b/homeassistant/components/ring/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "2fa": { + "data": { + "2fa": "C\u00f3digo de dos factores" + }, + "title": "Autenticaci\u00f3n de dos factores" + }, + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Iniciar sesi\u00f3n con cuenta de Ring" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roku/translations/es-419.json b/homeassistant/components/roku/translations/es-419.json new file mode 100644 index 00000000000000..40a76670fe18d4 --- /dev/null +++ b/homeassistant/components/roku/translations/es-419.json @@ -0,0 +1,25 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo Roku ya est\u00e1 configurado", + "unknown": "Error inesperado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente" + }, + "flow_title": "Roku: {name}", + "step": { + "ssdp_confirm": { + "description": "\u00bfDesea configurar {name}?", + "title": "Roku" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, + "description": "Ingrese su informaci\u00f3n de Roku.", + "title": "Roku" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/roomba/__init__.py b/homeassistant/components/roomba/__init__.py index 28092f964773e6..a9efa1f24abb3d 100644 --- a/homeassistant/components/roomba/__init__.py +++ b/homeassistant/components/roomba/__init__.py @@ -40,15 +40,18 @@ def _has_all_unique_bilds(value): return value -DEVICE_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST): str, - vol.Required(CONF_BLID): str, - vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, - vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, - vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, - }, +DEVICE_SCHEMA = vol.All( + cv.deprecated(CONF_CERT), + vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Required(CONF_BLID): str, + vol.Required(CONF_PASSWORD): str, + vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, + vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, + vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, + }, + ), ) @@ -92,7 +95,6 @@ async def async_setup_entry(hass, config_entry): address=config_entry.data[CONF_HOST], blid=config_entry.data[CONF_BLID], password=config_entry.data[CONF_PASSWORD], - cert_name=config_entry.data[CONF_CERT], continuous=config_entry.options[CONF_CONTINUOUS], delay=config_entry.options[CONF_DELAY], ) diff --git a/homeassistant/components/roomba/config_flow.py b/homeassistant/components/roomba/config_flow.py index e323150fba3220..cffbb3de4c9aaf 100644 --- a/homeassistant/components/roomba/config_flow.py +++ b/homeassistant/components/roomba/config_flow.py @@ -11,11 +11,9 @@ from . import CannotConnect, async_connect_or_timeout, async_disconnect_or_timeout from .const import ( CONF_BLID, - CONF_CERT, CONF_CONTINUOUS, CONF_DELAY, CONF_NAME, - DEFAULT_CERT, DEFAULT_CONTINUOUS, DEFAULT_DELAY, ROOMBA_SESSION, @@ -27,7 +25,6 @@ vol.Required(CONF_HOST): str, vol.Required(CONF_BLID): str, vol.Required(CONF_PASSWORD): str, - vol.Optional(CONF_CERT, default=DEFAULT_CERT): str, vol.Optional(CONF_CONTINUOUS, default=DEFAULT_CONTINUOUS): bool, vol.Optional(CONF_DELAY, default=DEFAULT_DELAY): int, } @@ -45,7 +42,6 @@ async def validate_input(hass: core.HomeAssistant, data): address=data[CONF_HOST], blid=data[CONF_BLID], password=data[CONF_PASSWORD], - cert_name=data[CONF_CERT], continuous=data[CONF_CONTINUOUS], delay=data[CONF_DELAY], ) diff --git a/homeassistant/components/roomba/manifest.json b/homeassistant/components/roomba/manifest.json index a164509bc99149..45fe9133bcabe1 100644 --- a/homeassistant/components/roomba/manifest.json +++ b/homeassistant/components/roomba/manifest.json @@ -3,7 +3,7 @@ "name": "iRobot Roomba", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/roomba", - "requirements": ["roombapy==1.5.1"], + "requirements": ["roombapy==1.5.3"], "dependencies": [], "codeowners": ["@pschmitt", "@cyr-ius", "@shenxn"] } diff --git a/homeassistant/components/roomba/strings.json b/homeassistant/components/roomba/strings.json index c15c5f5893a2c2..3dc904f2b94f3b 100644 --- a/homeassistant/components/roomba/strings.json +++ b/homeassistant/components/roomba/strings.json @@ -8,7 +8,6 @@ "host": "Hostname or IP Address", "blid": "BLID", "password": "Password", - "certificate": "Certificate", "continuous": "Continuous", "delay": "Delay" } diff --git a/homeassistant/components/roomba/translations/es-419.json b/homeassistant/components/roomba/translations/es-419.json new file mode 100644 index 00000000000000..d452c82b112f34 --- /dev/null +++ b/homeassistant/components/roomba/translations/es-419.json @@ -0,0 +1,32 @@ +{ + "config": { + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "blid": "BLID", + "certificate": "Certificado", + "continuous": "Continuo", + "delay": "Retraso", + "host": "Nombre de host o direcci\u00f3n IP", + "password": "Contrase\u00f1a" + }, + "description": "Actualmente recuperar el BLID y la contrase\u00f1a es un proceso manual. Siga los pasos descritos en la documentaci\u00f3n en: https://www.home-assistant.io/integrations/roomba/#retrieving-your-credentials", + "title": "Conectarse al dispositivo" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "continuous": "Continuo", + "delay": "Retraso" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/bridge.py b/homeassistant/components/samsungtv/bridge.py index a0f16e91cf518e..472ce894e1a05b 100644 --- a/homeassistant/components/samsungtv/bridge.py +++ b/homeassistant/components/samsungtv/bridge.py @@ -259,4 +259,6 @@ def _get_remote(self): except ConnectionFailure: self._notify_callback() raise + except WebSocketException: + self._remote = None return self._remote diff --git a/homeassistant/components/samsungtv/translations/es-419.json b/homeassistant/components/samsungtv/translations/es-419.json new file mode 100644 index 00000000000000..b35146e181e7f0 --- /dev/null +++ b/homeassistant/components/samsungtv/translations/es-419.json @@ -0,0 +1,26 @@ +{ + "config": { + "abort": { + "already_configured": "Esta televisi\u00f3n Samsung ya est\u00e1 configurado.", + "already_in_progress": "La configuraci\u00f3n de la televisi\u00f3n Samsung ya est\u00e1 en progreso.", + "auth_missing": "Home Assistant no est\u00e1 autorizado para conectarse a esta televisi\u00f3n Samsung. Verifique la configuraci\u00f3n de su televisi\u00f3n para autorizar Home Assistant.", + "not_successful": "No se puede conectar a este dispositivo de TV Samsung.", + "not_supported": "Este dispositivo Samsung TV no es compatible actualmente." + }, + "flow_title": "Televisi\u00f3n Samsung: {model}", + "step": { + "confirm": { + "description": "\u00bfDesea configurar la televisi\u00f3n Samsung {model}? Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n. Las configuraciones manuales para este televisor se sobrescribir\u00e1n.", + "title": "Samsung TV" + }, + "user": { + "data": { + "host": "Host o direcci\u00f3n IP", + "name": "Nombre" + }, + "description": "Ingrese la informaci\u00f3n de su televisor Samsung. Si nunca conect\u00f3 Home Assistant antes, deber\u00eda ver una ventana emergente en su televisor pidiendo autorizaci\u00f3n.", + "title": "Samsung TV" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/samsungtv/translations/no.json b/homeassistant/components/samsungtv/translations/no.json index 9240842ff97890..490c716665e839 100644 --- a/homeassistant/components/samsungtv/translations/no.json +++ b/homeassistant/components/samsungtv/translations/no.json @@ -7,7 +7,7 @@ "not_successful": "Kan ikke koble til denne Samsung TV-enheten.", "not_supported": "Denne Samsung TV-enhetene st\u00f8ttes forel\u00f8pig ikke." }, - "flow_title": "", + "flow_title": "Samsung TV: {model}", "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.", diff --git a/homeassistant/components/scrape/sensor.py b/homeassistant/components/scrape/sensor.py index 13d99a0cb8f1b6..4119f7e4c6b0ca 100644 --- a/homeassistant/components/scrape/sensor.py +++ b/homeassistant/components/scrape/sensor.py @@ -132,7 +132,11 @@ def update(self): if self._attr is not None: value = raw_data.select(self._select)[self._index][self._attr] else: - value = raw_data.select(self._select)[self._index].text + tag = raw_data.select(self._select)[self._index] + if tag.name in ("style", "script", "template"): + value = tag.string + else: + value = tag.text _LOGGER.debug(value) except IndexError: _LOGGER.error("Unable to extract data from HTML") diff --git a/homeassistant/components/season/translations/sensor.es-419.json b/homeassistant/components/season/translations/sensor.es-419.json index c0c3927357b8e2..6837038ff3c44b 100644 --- a/homeassistant/components/season/translations/sensor.es-419.json +++ b/homeassistant/components/season/translations/sensor.es-419.json @@ -1,9 +1,16 @@ { "state": { + "season__season": { + "autumn": "Oto\u00f1o", + "spring": "Primavera", + "summer": "Verano", + "winter": "Invierno" + }, "season__season__": { "autumn": "Oto\u00f1o", "spring": "Primavera", - "summer": "Verano" + "summer": "Verano", + "winter": "Invierno" } } } \ No newline at end of file diff --git a/homeassistant/components/sense/translations/es-419.json b/homeassistant/components/sense/translations/es-419.json new file mode 100644 index 00000000000000..7f643e13d63388 --- /dev/null +++ b/homeassistant/components/sense/translations/es-419.json @@ -0,0 +1,21 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "email": "Direcci\u00f3n de correo electr\u00f3nico", + "password": "Contrase\u00f1a" + }, + "title": "Con\u00e9ctese a su monitor de energ\u00eda Sense" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/sensor/translations/es-419.json b/homeassistant/components/sensor/translations/es-419.json index 6ed28293a90721..e724fe3a106d26 100644 --- a/homeassistant/components/sensor/translations/es-419.json +++ b/homeassistant/components/sensor/translations/es-419.json @@ -1,4 +1,15 @@ { + "device_automation": { + "trigger_type": { + "battery_level": "{entity_name} cambios de nivel de bater\u00eda", + "humidity": "{entity_name} cambios de humedad", + "illuminance": "{entity_name} cambios de iluminancia", + "pressure": "{entity_name} cambios de presi\u00f3n", + "signal_strength": "{entity_name} cambios en la intensidad de la se\u00f1al", + "temperature": "{entity_name} cambios de temperatura", + "value": "{entity_name} cambios de valor" + } + }, "state": { "_": { "off": "", diff --git a/homeassistant/components/sentry/translations/es-419.json b/homeassistant/components/sentry/translations/es-419.json new file mode 100644 index 00000000000000..7d207a3d5f296a --- /dev/null +++ b/homeassistant/components/sentry/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_configured": "Sentry ya est\u00e1 configurado" + }, + "error": { + "bad_dsn": "DSN inv\u00e1lido", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "description": "Ingrese su DSN Sentry", + "title": "Centinela" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/shopping_list/translations/es-419.json b/homeassistant/components/shopping_list/translations/es-419.json new file mode 100644 index 00000000000000..f9f1d78ea8395a --- /dev/null +++ b/homeassistant/components/shopping_list/translations/es-419.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "already_configured": "La lista de compras ya est\u00e1 configurada." + }, + "step": { + "user": { + "description": "\u00bfQuieres configurar la lista de compras?", + "title": "Lista de la compra" + } + } + }, + "title": "Lista de la compra" +} \ No newline at end of file diff --git a/homeassistant/components/simplisafe/translations/es-419.json b/homeassistant/components/simplisafe/translations/es-419.json index 135e9f843e97d0..6273cfa671b854 100644 --- a/homeassistant/components/simplisafe/translations/es-419.json +++ b/homeassistant/components/simplisafe/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Esta cuenta SimpliSafe ya est\u00e1 en uso." + }, "error": { "identifier_exists": "Cuenta ya registrada", "invalid_credentials": "Credenciales no v\u00e1lidas" @@ -7,11 +10,22 @@ "step": { "user": { "data": { + "code": "C\u00f3digo (utilizado en la interfaz de usuario de Home Assistant)", "password": "Contrase\u00f1a", "username": "Direcci\u00f3n de correo electr\u00f3nico" }, "title": "Completa tu informaci\u00f3n" } } + }, + "options": { + "step": { + "init": { + "data": { + "code": "C\u00f3digo (utilizado en la 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 4454c82c8f8fc3..730b9b810d15fa 100644 --- a/homeassistant/components/simplisafe/translations/fr.json +++ b/homeassistant/components/simplisafe/translations/fr.json @@ -10,6 +10,7 @@ "step": { "user": { "data": { + "code": "Code (utilis\u00e9 dans l'interface Home Assistant)", "password": "Mot de passe", "username": "Adresse e-mail" }, diff --git a/homeassistant/components/smartthings/climate.py b/homeassistant/components/smartthings/climate.py index e9c0e749ca8848..6ce872cdac7ab8 100644 --- a/homeassistant/components/smartthings/climate.py +++ b/homeassistant/components/smartthings/climate.py @@ -1,7 +1,8 @@ """Support for climate devices through the SmartThings cloud API.""" import asyncio +from collections.abc import Iterable import logging -from typing import Iterable, Optional, Sequence +from typing import Optional, Sequence from pysmartthings import Attribute, Capability diff --git a/homeassistant/components/smartthings/strings.json b/homeassistant/components/smartthings/strings.json index c8b938ebc6ad29..3b8e3b208a61b4 100644 --- a/homeassistant/components/smartthings/strings.json +++ b/homeassistant/components/smartthings/strings.json @@ -8,7 +8,9 @@ "pat": { "title": "Enter Personal Access Token", "description": "Please enter a SmartThings [Personal Access Token]({token_url}) that has been created per the [instructions]({component_url}). This will be used to create the Home Assistant integration within your SmartThings account.", - "data": { "access_token": "Access Token" } + "data": { + "access_token": "[%key:common::config_flow::data::access_token%]" + } }, "select_location": { "title": "Select Location", diff --git a/homeassistant/components/smartthings/translations/es-419.json b/homeassistant/components/smartthings/translations/es-419.json index d5446773700a18..a07311b833ee69 100644 --- a/homeassistant/components/smartthings/translations/es-419.json +++ b/homeassistant/components/smartthings/translations/es-419.json @@ -1,13 +1,36 @@ { "config": { + "abort": { + "invalid_webhook_url": "Home Assistant no est\u00e1 configurado correctamente para recibir actualizaciones de SmartThings. La URL del webhook no es v\u00e1lida: \n > {webhook_url} \n\nActualice su configuraci\u00f3n seg\u00fan las [instrucciones] ({component_url}), reinicie Home Assistant e intente nuevamente.", + "no_available_locations": "No hay ubicaciones SmartThings disponibles para configurar en Home Assistant." + }, "error": { "app_setup_error": "No se puede configurar el SmartApp. Por favor, int\u00e9ntelo de nuevo.", + "token_forbidden": "El token no tiene los \u00e1mbitos de OAuth necesarios.", "token_invalid_format": "El token debe estar en formato UID/GUID", "token_unauthorized": "El token no es v\u00e1lido o ya no est\u00e1 autorizado.", "webhook_error": "SmartThings no pudo validar el endpoint configurado en `base_url`. Por favor, revise los requisitos de los componentes." }, "step": { + "authorize": { + "title": "Autorizar Home Assistant" + }, + "pat": { + "data": { + "access_token": "Token de acceso" + }, + "description": "Ingrese un SmartThings [Token de acceso personal] ({token_url}) que se ha creado seg\u00fan las [instrucciones] ({component_url}). Esto se usar\u00e1 para crear la integraci\u00f3n de Home Assistant dentro de su cuenta SmartThings.", + "title": "Ingresar token de acceso personal" + }, + "select_location": { + "data": { + "location_id": "Ubicaci\u00f3n" + }, + "description": "Seleccione la ubicaci\u00f3n de SmartThings que desea agregar a Home Assistant. Luego, abriremos una nueva ventana y le pediremos que inicie sesi\u00f3n y autorice la instalaci\u00f3n de la integraci\u00f3n de Home Assistant en la ubicaci\u00f3n seleccionada.", + "title": "Seleccionar ubicaci\u00f3n" + }, "user": { + "description": "SmartThings se configurar\u00e1 para enviar actualizaciones push a Home Assistant en: \n > {webhook_url} \n\nSi esto no es correcto, actualice su configuraci\u00f3n, reinicie Home Assistant e intente nuevamente.", "title": "Ingresar token de acceso personal" } } diff --git a/homeassistant/components/solaredge/translations/es-419.json b/homeassistant/components/solaredge/translations/es-419.json new file mode 100644 index 00000000000000..7cbd7f1b5a1f55 --- /dev/null +++ b/homeassistant/components/solaredge/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "error": { + "site_exists": "Este site_id ya est\u00e1 configurado" + }, + "step": { + "user": { + "data": { + "api_key": "La clave API para este sitio", + "name": "El nombre de esta instalaci\u00f3n.", + "site_id": "La identificaci\u00f3n del sitio de SolarEdge" + }, + "title": "Definir los par\u00e1metros de API para esta instalaci\u00f3n." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/solarlog/translations/es-419.json b/homeassistant/components/solarlog/translations/es-419.json new file mode 100644 index 00000000000000..9f17072f424803 --- /dev/null +++ b/homeassistant/components/solarlog/translations/es-419.json @@ -0,0 +1,20 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "already_configured": "El dispositivo ya est\u00e1 configurado", + "cannot_connect": "No se pudo conectar, verifique la direcci\u00f3n del host" + }, + "step": { + "user": { + "data": { + "host": "El nombre de host o la direcci\u00f3n IP de su dispositivo Solar-Log", + "name": "El prefijo que se utilizar\u00e1 para sus sensores de registro solar" + }, + "title": "Define tu conexi\u00f3n Solar-Log" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/soma/translations/es-419.json b/homeassistant/components/soma/translations/es-419.json new file mode 100644 index 00000000000000..11c50fd5fb9219 --- /dev/null +++ b/homeassistant/components/soma/translations/es-419.json @@ -0,0 +1,24 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta Soma.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "connection_error": "No se pudo conectar a SOMA Connect.", + "missing_configuration": "El componente Soma no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n.", + "result_error": "SOMA Connect respondi\u00f3 con estado de error." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Soma." + }, + "step": { + "user": { + "data": { + "host": "Host", + "port": "Puerto" + }, + "description": "Ingrese la configuraci\u00f3n de conexi\u00f3n de su SOMA Connect.", + "title": "SOMA Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/somfy/translations/es-419.json b/homeassistant/components/somfy/translations/es-419.json index ea066156c713f2..3667a72315b753 100644 --- a/homeassistant/components/somfy/translations/es-419.json +++ b/homeassistant/components/somfy/translations/es-419.json @@ -1,3 +1,17 @@ { - "title": "Somfy" + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta Somfy.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "El componente Somfy no est\u00e1 configurado. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado con \u00e9xito con Somfy." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } } \ No newline at end of file diff --git a/homeassistant/components/sonos/manifest.json b/homeassistant/components/sonos/manifest.json index ee035fb59c1d21..27631cc80452c8 100644 --- a/homeassistant/components/sonos/manifest.json +++ b/homeassistant/components/sonos/manifest.json @@ -3,7 +3,7 @@ "name": "Sonos", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/sonos", - "requirements": ["pysonos==0.0.25"], + "requirements": ["pysonos==0.0.28"], "ssdp": [ { "st": "urn:schemas-upnp-org:device:ZonePlayer:1" diff --git a/homeassistant/components/sonos/media_player.py b/homeassistant/components/sonos/media_player.py index 55107101610467..6565be0c5c994f 100644 --- a/homeassistant/components/sonos/media_player.py +++ b/homeassistant/components/sonos/media_player.py @@ -31,7 +31,13 @@ SUPPORT_VOLUME_MUTE, SUPPORT_VOLUME_SET, ) -from homeassistant.const import ATTR_TIME, STATE_IDLE, STATE_PAUSED, STATE_PLAYING +from homeassistant.const import ( + ATTR_TIME, + EVENT_HOMEASSISTANT_STOP, + STATE_IDLE, + STATE_PAUSED, + STATE_PLAYING, +) from homeassistant.core import ServiceCall, callback from homeassistant.helpers import config_validation as cv, entity_platform, service from homeassistant.util.dt import utcnow @@ -99,11 +105,13 @@ class SonosData: """Storage class for platform global data.""" - def __init__(self, hass): + def __init__(self): """Initialize the data.""" self.entities = [] self.discovered = [] self.topology_condition = asyncio.Condition() + self.discovery_thread = None + self.hosts_heartbeat = None async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -116,7 +124,7 @@ 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 Sonos from a config entry.""" if DATA_SONOS not in hass.data: - hass.data[DATA_SONOS] = SonosData(hass) + hass.data[DATA_SONOS] = SonosData() config = hass.data[SONOS_DOMAIN].get("media_player", {}) _LOGGER.debug("Reached async_setup_entry, config=%s", config) @@ -125,6 +133,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if advertise_addr: pysonos.config.EVENT_ADVERTISE_IP = advertise_addr + def _stop_discovery(event): + data = hass.data[DATA_SONOS] + if data.discovery_thread: + data.discovery_thread.stop() + data.discovery_thread = None + if data.hosts_heartbeat: + data.hosts_heartbeat() + data.hosts_heartbeat = None + def _discovery(now=None): """Discover players from network or configuration.""" hosts = config.get(CONF_HOSTS) @@ -162,10 +179,12 @@ def _discovered_player(soco): _LOGGER.warning("Failed to initialize '%s'", host) _LOGGER.debug("Tested all hosts") - hass.helpers.event.call_later(DISCOVERY_INTERVAL, _discovery) + hass.data[DATA_SONOS].hosts_heartbeat = hass.helpers.event.call_later( + DISCOVERY_INTERVAL, _discovery + ) else: _LOGGER.debug("Starting discovery thread") - pysonos.discover_thread( + hass.data[DATA_SONOS].discovery_thread = pysonos.discover_thread( _discovered_player, interval=DISCOVERY_INTERVAL, interface_addr=config.get(CONF_INTERFACE_ADDR), @@ -173,6 +192,7 @@ def _discovered_player(soco): _LOGGER.debug("Adding discovery job") hass.async_add_executor_job(_discovery) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_discovery) platform = entity_platform.current_platform.get() @@ -639,17 +659,14 @@ def update_media_radio(self, variables, track_info): def update_media_music(self, update_media_position, track_info): """Update state when playing music tracks.""" self._media_duration = _timespan_secs(track_info.get("duration")) - - position_info = self.soco.avTransport.GetPositionInfo( - [("InstanceID", 0), ("Channel", "Master")] - ) - rel_time = _timespan_secs(position_info.get("RelTime")) + current_position = _timespan_secs(track_info.get("position")) # player started reporting position? - update_media_position |= rel_time is not None and self._media_position is None + if current_position is not None and self._media_position is None: + update_media_position = True # position jumped? - if rel_time is not None and self._media_position is not None: + if current_position 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() @@ -658,12 +675,13 @@ def update_media_music(self, update_media_position, track_info): calculated_position = self._media_position + time_diff - update_media_position |= abs(calculated_position - rel_time) > 1.5 + if abs(calculated_position - current_position) > 1.5: + update_media_position = True - if rel_time is None: + if current_position is None: self._clear_media_position() elif update_media_position: - self._media_position = rel_time + self._media_position = current_position self._media_position_updated_at = utcnow() self._media_image_url = track_info.get("album_art") diff --git a/homeassistant/components/spotify/manifest.json b/homeassistant/components/spotify/manifest.json index bbbfb75a5365bf..88e0c938a28c92 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.11.1"], + "requirements": ["spotipy==2.12.0"], "zeroconf": ["_spotify-connect._tcp.local."], "dependencies": ["http"], "codeowners": ["@frenck"], diff --git a/homeassistant/components/spotify/translations/es-419.json b/homeassistant/components/spotify/translations/es-419.json new file mode 100644 index 00000000000000..f9b956e47bc981 --- /dev/null +++ b/homeassistant/components/spotify/translations/es-419.json @@ -0,0 +1,17 @@ +{ + "config": { + "abort": { + "already_setup": "Solo puede configurar una cuenta de Spotify.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", + "missing_configuration": "La integraci\u00f3n de Spotify no est\u00e1 configurada. Por favor, siga la documentaci\u00f3n." + }, + "create_entry": { + "default": "Autenticado correctamente con Spotify." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/squeezebox/manifest.json b/homeassistant/components/squeezebox/manifest.json index 13c200fc46f780..ae076c88b4afed 100644 --- a/homeassistant/components/squeezebox/manifest.json +++ b/homeassistant/components/squeezebox/manifest.json @@ -3,5 +3,5 @@ "name": "Logitech Squeezebox", "documentation": "https://www.home-assistant.io/integrations/squeezebox", "codeowners": ["@rajlaud"], - "requirements": ["pysqueezebox==0.1.2"] + "requirements": ["pysqueezebox==0.1.4"] } diff --git a/homeassistant/components/squeezebox/media_player.py b/homeassistant/components/squeezebox/media_player.py index 8b27defd5e131a..7194959d990c64 100644 --- a/homeassistant/components/squeezebox/media_player.py +++ b/homeassistant/components/squeezebox/media_player.py @@ -1,5 +1,4 @@ """Support for interfacing to the Logitech SqueezeBox API.""" -import asyncio import logging import socket @@ -25,7 +24,6 @@ ) from homeassistant.const import ( ATTR_COMMAND, - ATTR_ENTITY_ID, CONF_HOST, CONF_PASSWORD, CONF_PORT, @@ -33,11 +31,19 @@ STATE_OFF, ) from homeassistant.exceptions import PlatformNotReady +from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.aiohttp_client import async_get_clientsession -import homeassistant.helpers.config_validation as cv from homeassistant.util.dt import utcnow -from .const import DOMAIN, SERVICE_CALL_METHOD, SQUEEZEBOX_MODE +from .const import SQUEEZEBOX_MODE + +SERVICE_CALL_METHOD = "call_method" +SERVICE_CALL_QUERY = "call_query" +SERVICE_SYNC = "sync" +SERVICE_UNSYNC = "unsync" + +ATTR_QUERY_RESULT = "query_result" +ATTR_SYNC_GROUP = "sync_group" _LOGGER = logging.getLogger(__name__) @@ -59,8 +65,6 @@ | SUPPORT_CLEAR_PLAYLIST ) -MEDIA_PLAYER_SCHEMA = vol.Schema({ATTR_ENTITY_ID: cv.comp_entity_ids}) - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, @@ -76,21 +80,12 @@ ATTR_PARAMETERS = "parameters" -SQUEEZEBOX_CALL_METHOD_SCHEMA = MEDIA_PLAYER_SCHEMA.extend( - { - vol.Required(ATTR_COMMAND): cv.string, - vol.Optional(ATTR_PARAMETERS): vol.All( - cv.ensure_list, vol.Length(min=1), [cv.string] - ), - } -) +ATTR_OTHER_PLAYER = "other_player" -SERVICE_TO_METHOD = { - SERVICE_CALL_METHOD: { - "method": "async_call_method", - "schema": SQUEEZEBOX_CALL_METHOD_SCHEMA, - } -} +ATTR_TO_PROPERTY = [ + ATTR_QUERY_RESULT, + ATTR_SYNC_GROUP, +] async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): @@ -141,38 +136,35 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= hass.data[DATA_SQUEEZEBOX].extend(media_players) async_add_entities(media_players) - async def async_service_handler(service): - """Map services to methods on MediaPlayerEntity.""" - method = SERVICE_TO_METHOD.get(service.service) - if not method: - return - - params = { - key: value for key, value in service.data.items() if key != "entity_id" - } - entity_ids = service.data.get("entity_id") - if entity_ids: - target_players = [ - player - for player in hass.data[DATA_SQUEEZEBOX] - if player.entity_id in entity_ids - ] - else: - target_players = hass.data[DATA_SQUEEZEBOX] - - update_tasks = [] - for player in target_players: - await getattr(player, method["method"])(**params) - update_tasks.append(player.async_update_ha_state(True)) - - if update_tasks: - await asyncio.wait(update_tasks) - - for service in SERVICE_TO_METHOD: - schema = SERVICE_TO_METHOD[service]["schema"] - hass.services.async_register( - DOMAIN, service, async_service_handler, schema=schema - ) + platform = entity_platform.current_platform.get() + + platform.async_register_entity_service( + SERVICE_CALL_METHOD, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMETERS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + }, + "async_call_method", + ) + + platform.async_register_entity_service( + SERVICE_CALL_QUERY, + { + vol.Required(ATTR_COMMAND): cv.string, + vol.Optional(ATTR_PARAMETERS): vol.All( + cv.ensure_list, vol.Length(min=1), [cv.string] + ), + }, + "async_call_query", + ) + + platform.async_register_entity_service( + SERVICE_SYNC, {vol.Required(ATTR_OTHER_PLAYER): cv.string}, "async_sync", + ) + + platform.async_register_entity_service(SERVICE_UNSYNC, None, "async_unsync") return True @@ -188,6 +180,18 @@ def __init__(self, player): """Initialize the SqueezeBox device.""" self._player = player self._last_update = None + self._query_result = {} + + @property + def device_state_attributes(self): + """Return device-specific attributes.""" + squeezebox_attr = { + attr: getattr(self, attr) + for attr in ATTR_TO_PROPERTY + if getattr(self, attr) is not None + } + + return squeezebox_attr @property def name(self): @@ -284,6 +288,21 @@ def supported_features(self): """Flag media player features that are supported.""" return SUPPORT_SQUEEZEBOX + @property + def sync_group(self): + """List players we are synced with.""" + player_ids = {p.unique_id: p.entity_id for p in self.hass.data[DATA_SQUEEZEBOX]} + sync_group = [] + for player in self._player.sync_group: + if player in player_ids: + sync_group.append(player_ids[player]) + return sync_group + + @property + def query_result(self): + """Return the result from the call_query service.""" + return self._query_result + async def async_turn_off(self): """Turn off media player.""" await self._player.async_set_power(False) @@ -366,3 +385,35 @@ async def async_call_method(self, command, parameters=None): for parameter in parameters: all_params.append(parameter) await self._player.async_query(*all_params) + + async def async_call_query(self, command, parameters=None): + """ + Call Squeezebox JSON/RPC method where we care about the result. + + Additional parameters are added to the command to form the list of + positional parameters (p0, p1..., pN) passed to JSON/RPC server. + """ + all_params = [command] + if parameters: + for parameter in parameters: + all_params.append(parameter) + self._query_result = await self._player.async_query(*all_params) + _LOGGER.debug("call_query got result %s", self._query_result) + + async def async_sync(self, other_player): + """ + Add another Squeezebox player to this player's sync group. + + If the other player is a member of a sync group, it will leave the current sync group + without asking. + """ + player_ids = {p.entity_id: p.unique_id for p in self.hass.data[DATA_SQUEEZEBOX]} + other_player_id = player_ids.get(other_player) + if other_player_id: + await self._player.async_sync(other_player_id) + else: + _LOGGER.info("Could not find player_id for %s. Not syncing.", other_player) + + async def async_unsync(self): + """Unsync this Squeezebox player.""" + await self._player.async_unsync() diff --git a/homeassistant/components/squeezebox/services.yaml b/homeassistant/components/squeezebox/services.yaml index b1768949258432..f1b9966cd937ec 100644 --- a/homeassistant/components/squeezebox/services.yaml +++ b/homeassistant/components/squeezebox/services.yaml @@ -8,5 +8,35 @@ call_method: description: Command to pass to Logitech Media Server (p0 in the CLI documentation). example: "playlist" parameters: - description: Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). + description: > + Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). example: ["loadtracks", "album.titlesearch="] +call_query: + description: > + Call a custom Squeezebox JSONRPC API. Result will be stored in 'query_result' attribute of the Squeezebox entity. + fields: + entity_id: + description: Name(s) of the Squeezebox entities where to run the API method. + example: 'media_player.squeezebox_radio' + command: + description: Command to pass to Logitech Media Server (p0 in the CLI documentation). + example: 'albums' + parameters: + description: > + Array of additional parameters to pass to Logitech Media Server (p1, ..., pN in the CLI documentation). + example: ["0", "20", "Revolver"] +sync: + description: > + Add another player to this player's sync group. If the other player is already in a sync group, it will leave it. + fields: + entity_id: + description: Name of the Squeezebox entity where to run the API method. + example: "media_player.bedroom" + other_player: + description: Name of the other Squeezebox player to link. + example: "media_player.living_room" +unsync: + description: Remove this player from its sync group. + fields: + entity_id: + description: Name of the Squeezebox entity to unsync. diff --git a/homeassistant/components/starline/translations/es-419.json b/homeassistant/components/starline/translations/es-419.json new file mode 100644 index 00000000000000..d6cdfa31b13276 --- /dev/null +++ b/homeassistant/components/starline/translations/es-419.json @@ -0,0 +1,41 @@ +{ + "config": { + "error": { + "error_auth_app": "Identificaci\u00f3n de aplicaci\u00f3n incorrecta o secreto", + "error_auth_mfa": "C\u00f3digo incorrecto", + "error_auth_user": "Nombre de usuario o contrase\u00f1a incorrecta" + }, + "step": { + "auth_app": { + "data": { + "app_id": "ID de la aplicaci\u00f3n", + "app_secret": "Secreto" + }, + "description": "ID de la aplicaci\u00f3n y c\u00f3digo secreto de cuenta de desarrollador de StarLine ", + "title": "Credenciales de solicitud" + }, + "auth_captcha": { + "data": { + "captcha_code": "C\u00f3digo de imagen" + }, + "description": "{captcha_img}", + "title": "Captcha" + }, + "auth_mfa": { + "data": { + "mfa_code": "C\u00f3digo SMS" + }, + "description": "Ingrese el c\u00f3digo enviado al tel\u00e9fono {phone_number}", + "title": "Autorizaci\u00f3n de dos factores" + }, + "auth_user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "description": "Correo electr\u00f3nico y contrase\u00f1a de la cuenta StarLine", + "title": "Credenciales de usuario" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index a22ba4a1335799..90e754118abadd 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -32,6 +32,7 @@ DEFAULT_SCAN_INTERVAL, DOMAIN, SPC, + SURE_API_TIMEOUT, TOPIC_UPDATE, ) @@ -78,6 +79,7 @@ async def async_setup(hass, config) -> bool: conf[CONF_PASSWORD], hass.loop, async_get_clientsession(hass), + api_timeout=SURE_API_TIMEOUT, ) await surepy.get_data() except SurePetcareAuthenticationError: diff --git a/homeassistant/components/surepetcare/binary_sensor.py b/homeassistant/components/surepetcare/binary_sensor.py index 26f498d43fe203..efd5048053f3e2 100644 --- a/homeassistant/components/surepetcare/binary_sensor.py +++ b/homeassistant/components/surepetcare/binary_sensor.py @@ -105,7 +105,7 @@ def device_class(self) -> str: return None if not self._device_class else self._device_class @property - def unique_id(self: BinarySensorEntity) -> str: + def unique_id(self) -> str: """Return an unique ID.""" return f"{self._spc_data['household_id']}-{self._id}" @@ -214,7 +214,7 @@ def name(self) -> str: return f"{self._name}_connectivity" @property - def unique_id(self: BinarySensorEntity) -> str: + def unique_id(self) -> str: """Return an unique ID.""" return f"{self._spc_data['household_id']}-{self._id}-connectivity" diff --git a/homeassistant/components/surepetcare/const.py b/homeassistant/components/surepetcare/const.py index d534398784fe4f..7f0213be4ef4bb 100644 --- a/homeassistant/components/surepetcare/const.py +++ b/homeassistant/components/surepetcare/const.py @@ -23,6 +23,9 @@ # platforms TOPIC_UPDATE = f"{DOMAIN}_data_update" +# sure petcare api +SURE_API_TIMEOUT = 15 + # flap BATTERY_ICON = "mdi:battery" SURE_BATT_VOLTAGE_FULL = 1.6 # voltage diff --git a/homeassistant/components/surepetcare/manifest.json b/homeassistant/components/surepetcare/manifest.json index 6d34ff477cead3..659a6091299dd0 100644 --- a/homeassistant/components/surepetcare/manifest.json +++ b/homeassistant/components/surepetcare/manifest.json @@ -3,5 +3,5 @@ "name": "Sure Petcare", "documentation": "https://www.home-assistant.io/integrations/surepetcare", "codeowners": ["@benleb"], - "requirements": ["surepy==0.2.3"] + "requirements": ["surepy==0.2.5"] } diff --git a/homeassistant/components/switch/translations/es-419.json b/homeassistant/components/switch/translations/es-419.json index 83dc31ade83659..7fb04127b15116 100644 --- a/homeassistant/components/switch/translations/es-419.json +++ b/homeassistant/components/switch/translations/es-419.json @@ -1,6 +1,7 @@ { "device_automation": { "action_type": { + "toggle": "Alternar {entity_name}", "turn_off": "Desactivar {entity_name}", "turn_on": "Activar {entity_name}" }, diff --git a/homeassistant/components/synology_dsm/__init__.py b/homeassistant/components/synology_dsm/__init__.py index 3fbed6955d95fe..9431cb7b1c97b8 100644 --- a/homeassistant/components/synology_dsm/__init__.py +++ b/homeassistant/components/synology_dsm/__init__.py @@ -11,6 +11,7 @@ from homeassistant.const import ( CONF_DISKS, CONF_HOST, + CONF_MAC, CONF_PASSWORD, CONF_PORT, CONF_SSL, @@ -77,6 +78,13 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry): hass.data.setdefault(DOMAIN, {}) hass.data[DOMAIN][entry.unique_id] = api + # For SSDP compat + if not entry.data.get(CONF_MAC): + network = await hass.async_add_executor_job(getattr, api.dsm, "network") + hass.config_entries.async_update_entry( + entry, data={**entry.data, CONF_MAC: network.macs} + ) + hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, "sensor") ) @@ -115,7 +123,7 @@ def __init__( self._device_token = device_token self.temp_unit = temp_unit - self._dsm: SynologyDSM = None + self.dsm: SynologyDSM = None self.information: SynoDSMInformation = None self.utilisation: SynoCoreUtilization = None self.storage: SynoStorage = None @@ -129,7 +137,7 @@ def signal_sensor_update(self) -> str: async def async_setup(self): """Start interacting with the NAS.""" - self._dsm = SynologyDSM( + self.dsm = SynologyDSM( self._host, self._port, self._username, @@ -147,9 +155,9 @@ async def async_setup(self): def _fetch_device_configuration(self): """Fetch initial device config.""" - self.information = self._dsm.information - self.utilisation = self._dsm.utilisation - self.storage = self._dsm.storage + self.information = self.dsm.information + self.utilisation = self.dsm.utilisation + self.storage = self.dsm.storage async def async_unload(self): """Stop interacting with the NAS and prepare for removal from hass.""" @@ -157,5 +165,5 @@ async def async_unload(self): async def update(self, now=None): """Update function for updating API information.""" - await self._hass.async_add_executor_job(self._dsm.update) + await self._hass.async_add_executor_job(self.dsm.update) async_dispatcher_send(self._hass, self.signal_sensor_update) diff --git a/homeassistant/components/synology_dsm/config_flow.py b/homeassistant/components/synology_dsm/config_flow.py index 4b09e5164513dd..c3d15aff2fd6f2 100644 --- a/homeassistant/components/synology_dsm/config_flow.py +++ b/homeassistant/components/synology_dsm/config_flow.py @@ -17,6 +17,7 @@ from homeassistant.const import ( CONF_DISKS, CONF_HOST, + CONF_MAC, CONF_NAME, CONF_PASSWORD, CONF_PORT, @@ -145,6 +146,7 @@ async def async_step_user(self, user_input=None): CONF_SSL: use_ssl, CONF_USERNAME: username, CONF_PASSWORD: password, + CONF_MAC: api.network.macs, } if otp_code: config_data["device_token"] = api.device_token @@ -162,16 +164,11 @@ async def async_step_ssdp(self, discovery_info): discovery_info[ssdp.ATTR_UPNP_FRIENDLY_NAME].split("(", 1)[0].strip() ) - if self._host_already_configured(parsed_url.hostname): + # Synology NAS can broadcast on multiple IP addresses, since they can be connected to multiple ethernets. + # The serial of the NAS is actually its MAC address. + if self._mac_already_configured(discovery_info[ssdp.ATTR_UPNP_SERIAL].upper()): return self.async_abort(reason="already_configured") - if ssdp.ATTR_UPNP_SERIAL in discovery_info: - # Synology can broadcast on multiple IP addresses - await self.async_set_unique_id( - discovery_info[ssdp.ATTR_UPNP_SERIAL].upper() - ) - self._abort_if_unique_id_configured() - self.discovered_conf = { CONF_NAME: friendly_name, CONF_HOST: parsed_url.hostname, @@ -205,12 +202,14 @@ async def async_step_2sa(self, user_input, errors=None): return await self.async_step_user(user_input) - def _host_already_configured(self, hostname): - """See if we already have a host matching user input configured.""" - existing_hosts = { - entry.data[CONF_HOST] for entry in self._async_current_entries() - } - return hostname in existing_hosts + def _mac_already_configured(self, mac): + """See if we already have configured a NAS with this MAC address.""" + existing_macs = [ + mac.replace("-", "") + for entry in self._async_current_entries() + for mac in entry.data.get(CONF_MAC, []) + ] + return mac in existing_macs def _login_and_fetch_syno_info(api, otp_code): @@ -221,10 +220,11 @@ def _login_and_fetch_syno_info(api, otp_code): storage = api.storage if ( - api.information.serial is None + not api.information.serial or utilisation.cpu_user_load is None - or storage.disks_ids is None - or storage.volumes_ids is None + or not storage.disks_ids + or not storage.volumes_ids + or not api.network.macs ): raise InvalidData diff --git a/homeassistant/components/synology_dsm/const.py b/homeassistant/components/synology_dsm/const.py index 8c17de5e99737d..b3c9f66c8da272 100644 --- a/homeassistant/components/synology_dsm/const.py +++ b/homeassistant/components/synology_dsm/const.py @@ -2,6 +2,7 @@ from homeassistant.const import ( DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, + DATA_TERABYTES, UNIT_PERCENTAGE, ) @@ -34,8 +35,8 @@ STORAGE_VOL_SENSORS = { "volume_status": ["Status", None, "mdi:checkbox-marked-circle-outline"], "volume_device_type": ["Type", None, "mdi:harddisk"], - "volume_size_total": ["Total Size", None, "mdi:chart-pie"], - "volume_size_used": ["Used Space", None, "mdi:chart-pie"], + "volume_size_total": ["Total Size", DATA_TERABYTES, "mdi:chart-pie"], + "volume_size_used": ["Used Space", DATA_TERABYTES, "mdi:chart-pie"], "volume_percentage_used": ["Volume Used", UNIT_PERCENTAGE, "mdi:chart-pie"], "volume_disk_temp_avg": ["Average Disk Temp", None, "mdi:thermometer"], "volume_disk_temp_max": ["Maximum Disk Temp", None, "mdi:thermometer"], diff --git a/homeassistant/components/synology_dsm/manifest.json b/homeassistant/components/synology_dsm/manifest.json index 4a538606ecba4d..f57f1843f45fcf 100644 --- a/homeassistant/components/synology_dsm/manifest.json +++ b/homeassistant/components/synology_dsm/manifest.json @@ -2,7 +2,7 @@ "domain": "synology_dsm", "name": "Synology DSM", "documentation": "https://www.home-assistant.io/integrations/synology_dsm", - "requirements": ["python-synology==0.7.4"], + "requirements": ["python-synology==0.8.0"], "codeowners": ["@ProtoThis", "@Quentame"], "config_flow": true, "ssdp": [ diff --git a/homeassistant/components/synology_dsm/sensor.py b/homeassistant/components/synology_dsm/sensor.py index 6e5a486ab8900f..87c1ba128c62f4 100644 --- a/homeassistant/components/synology_dsm/sensor.py +++ b/homeassistant/components/synology_dsm/sensor.py @@ -7,11 +7,13 @@ CONF_DISKS, DATA_MEGABYTES, DATA_RATE_KILOBYTES_PER_SECOND, + DATA_TERABYTES, TEMP_CELSIUS, ) from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType +from homeassistant.util.temperature import celsius_to_fahrenheit from . import SynoApi from .const import ( @@ -59,11 +61,11 @@ async def async_setup_entry( for sensor_type in STORAGE_DISK_SENSORS ] - async_add_entities(sensors, True) + async_add_entities(sensors) class SynoNasSensor(Entity): - """Representation of a Synology NAS Sensor.""" + """Representation of a Synology NAS sensor.""" def __init__( self, @@ -130,6 +132,10 @@ def should_poll(self) -> bool: """No polling needed.""" return False + async def async_update(self): + """Only used by the generic entity update service.""" + await self._api.update() + async def async_added_to_hass(self): """Register state update callback.""" self._unsub_dispatcher = async_dispatcher_connect( @@ -142,47 +148,51 @@ async def async_will_remove_from_hass(self): class SynoNasUtilSensor(SynoNasSensor): - """Representation a Synology Utilisation Sensor.""" + """Representation a Synology Utilisation sensor.""" @property def state(self): """Return the state.""" - if self._unit == DATA_RATE_KILOBYTES_PER_SECOND or self._unit == DATA_MEGABYTES: - attr = getattr(self._api.utilisation, self.sensor_type)(False) + attr = getattr(self._api.utilisation, self.sensor_type) + if callable(attr): + attr = attr() + if attr is None: + return None + + # Data (RAM) + if self._unit == DATA_MEGABYTES: + return round(attr / 1024.0 ** 2, 1) - if attr is None: - return None + # Network + if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: + return round(attr / 1024.0, 1) - if self._unit == DATA_RATE_KILOBYTES_PER_SECOND: - return round(attr / 1024.0, 1) - if self._unit == DATA_MEGABYTES: - return round(attr / 1024.0 / 1024.0, 1) - else: - return getattr(self._api.utilisation, self.sensor_type) + return attr class SynoNasStorageSensor(SynoNasSensor): - """Representation a Synology Storage Sensor.""" + """Representation a Synology Storage sensor.""" @property def state(self): """Return the state.""" - if self.monitored_device: - if self.sensor_type in TEMP_SENSORS_KEYS: - attr = getattr(self._api.storage, self.sensor_type)( - self.monitored_device - ) - - if attr is None: - return None - - if self._api.temp_unit == TEMP_CELSIUS: - return attr - - return round(attr * 1.8 + 32.0, 1) + attr = getattr(self._api.storage, self.sensor_type)(self.monitored_device) + if attr is None: + return None + + # Data (disk space) + if self._unit == DATA_TERABYTES: + return round(attr / 1024.0 ** 4, 2) + + # Temperature + if self._api.temp_unit == TEMP_CELSIUS: + # Celsius + return attr + if self.sensor_type in TEMP_SENSORS_KEYS: + # Fahrenheit + return celsius_to_fahrenheit(attr) - return getattr(self._api.storage, self.sensor_type)(self.monitored_device) - return None + return attr @property def device_info(self) -> Dict[str, any]: diff --git a/homeassistant/components/synology_dsm/translations/es-419.json b/homeassistant/components/synology_dsm/translations/es-419.json new file mode 100644 index 00000000000000..cad627e0dfb5cc --- /dev/null +++ b/homeassistant/components/synology_dsm/translations/es-419.json @@ -0,0 +1,45 @@ +{ + "config": { + "abort": { + "already_configured": "Host ya configurado" + }, + "error": { + "connection": "Error de conexi\u00f3n: compruebe su host, puerto y ssl", + "login": "Error de inicio de sesi\u00f3n: compruebe su nombre de usuario y contrase\u00f1a", + "missing_data": "Datos faltantes: vuelva a intentarlo m\u00e1s tarde u otra configuraci\u00f3n", + "otp_failed": "La autenticaci\u00f3n de dos pasos fall\u00f3, vuelva a intentar con un nuevo c\u00f3digo de acceso", + "unknown": "Error desconocido: verifique los registros para obtener m\u00e1s detalles" + }, + "flow_title": "Synology DSM {name} ({host})", + "step": { + "2sa": { + "data": { + "otp_code": "C\u00f3digo" + }, + "title": "Synology DSM: autenticaci\u00f3n en dos pasos" + }, + "link": { + "data": { + "api_version": "Versi\u00f3n DSM", + "password": "Contrase\u00f1a", + "port": "Puerto (opcional)", + "ssl": "Utilice SSL/TLS para conectarse a su NAS", + "username": "Nombre de usuario" + }, + "description": "\u00bfDesea configurar {name} ({host})?", + "title": "Synology DSM" + }, + "user": { + "data": { + "api_version": "Versi\u00f3n DSM", + "host": "Host", + "password": "Contrase\u00f1a", + "port": "Puerto (opcional)", + "ssl": "Utilice SSL/TLS para conectarse a su NAS", + "username": "Nombre de usuario" + }, + "title": "Synology DSM" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/synology_dsm/translations/no.json b/homeassistant/components/synology_dsm/translations/no.json index 7b2301768db866..8c12e1b078aed4 100644 --- a/homeassistant/components/synology_dsm/translations/no.json +++ b/homeassistant/components/synology_dsm/translations/no.json @@ -10,7 +10,7 @@ "otp_failed": "To-trinns autentisering mislyktes. Pr\u00f8v p\u00e5 nytt med en ny passkode", "unknown": "Ukjent feil: sjekk loggene for \u00e5 f\u00e5 flere detaljer" }, - "flow_title": "", + "flow_title": "Synology DSM {name} ({host})", "step": { "2sa": { "data": { diff --git a/homeassistant/components/tado/translations/es-419.json b/homeassistant/components/tado/translations/es-419.json new file mode 100644 index 00000000000000..3b9f1381eda3cf --- /dev/null +++ b/homeassistant/components/tado/translations/es-419.json @@ -0,0 +1,33 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "cannot_connect": "No se pudo conectar, intente nuevamente", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida", + "no_homes": "No hay hogares vinculados a esta cuenta Tado.", + "unknown": "Error inesperado" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Con\u00e9ctese a su cuenta de Tado" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "fallback": "Habilitar el modo de fallback." + }, + "description": "El modo Fallback cambiar\u00e1 a Smart Schedule en el siguiente cambio de programaci\u00f3n despu\u00e9s de ajustar manualmente una zona.", + "title": "Ajusta las opciones de Tado." + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tellduslive/translations/es-419.json b/homeassistant/components/tellduslive/translations/es-419.json index 36b358192be4e5..71529c1f41df07 100644 --- a/homeassistant/components/tellduslive/translations/es-419.json +++ b/homeassistant/components/tellduslive/translations/es-419.json @@ -3,6 +3,7 @@ "abort": { "already_setup": "TelldusLive ya est\u00e1 configurado", "authorize_url_fail": "Error desconocido al generar una URL de autorizaci\u00f3n.", + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\u00f3n.", "unknown": "Se produjo un error desconocido" }, "error": { @@ -16,7 +17,8 @@ "user": { "data": { "host": "Host" - } + }, + "title": "Elegir punto final." } } } diff --git a/homeassistant/components/template/cover.py b/homeassistant/components/template/cover.py index cea91ea2963d26..e8bdebe2f58a51 100644 --- a/homeassistant/components/template/cover.py +++ b/homeassistant/components/template/cover.py @@ -297,7 +297,7 @@ def supported_features(self): if self._position_script is not None: supported_features |= SUPPORT_SET_POSITION - if self.current_cover_tilt_position is not None: + if self._tilt_script is not None: supported_features |= TILT_FEATURES return supported_features diff --git a/homeassistant/components/tensorflow/manifest.json b/homeassistant/components/tensorflow/manifest.json index 3e1fe5b1f5c757..cbbd2d6345bb3e 100644 --- a/homeassistant/components/tensorflow/manifest.json +++ b/homeassistant/components/tensorflow/manifest.json @@ -4,7 +4,7 @@ "documentation": "https://www.home-assistant.io/integrations/tensorflow", "requirements": [ "tensorflow==1.13.2", - "numpy==1.18.2", + "numpy==1.18.4", "protobuf==3.6.1", "pillow==7.1.2" ], diff --git a/homeassistant/components/tesla/translations/es-419.json b/homeassistant/components/tesla/translations/es-419.json new file mode 100644 index 00000000000000..d29077e688db38 --- /dev/null +++ b/homeassistant/components/tesla/translations/es-419.json @@ -0,0 +1,30 @@ +{ + "config": { + "error": { + "connection_error": "Error al conectar verifique la red y vuelva a intentar", + "identifier_exists": "correo electr\u00f3nico ya registrado", + "invalid_credentials": "Credenciales no v\u00e1lidas", + "unknown_error": "Error desconocido, informe la informaci\u00f3n del registro" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Direcci\u00f3n de correo electr\u00f3nico" + }, + "description": "Por favor ingrese su informaci\u00f3n.", + "title": "Tesla - Configuraci\u00f3n" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "enable_wake_on_start": "Forzar a autom\u00f3viles despertar al inicio", + "scan_interval": "Segundos entre escaneos" + } + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/tibber/__init__.py b/homeassistant/components/tibber/__init__.py index 53c02a1461a6c2..657be67c7fc7a9 100644 --- a/homeassistant/components/tibber/__init__.py +++ b/homeassistant/components/tibber/__init__.py @@ -6,16 +6,19 @@ import tibber import voluptuous as vol +from homeassistant import config_entries from homeassistant.const import CONF_ACCESS_TOKEN, CONF_NAME, EVENT_HOMEASSISTANT_STOP +from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import discovery from homeassistant.helpers.aiohttp_client import async_get_clientsession import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.event import async_call_later from homeassistant.util import dt as dt_util -DOMAIN = "tibber" +from .const import DATA_HASS_CONFIG, DOMAIN -FIRST_RETRY_TIME = 60 +PLATFORMS = [ + "sensor", +] CONFIG_SCHEMA = vol.Schema( {DOMAIN: vol.Schema({vol.Required(CONF_ACCESS_TOKEN): cv.string})}, @@ -25,12 +28,30 @@ _LOGGER = logging.getLogger(__name__) -async def async_setup(hass, config, retry_delay=FIRST_RETRY_TIME): +async def async_setup(hass, config): """Set up the Tibber component.""" - conf = config.get(DOMAIN) + + hass.data[DATA_HASS_CONFIG] = config + + if DOMAIN not in config: + return True + + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=config[DOMAIN], + ) + ) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" tibber_connection = tibber.Tibber( - conf[CONF_ACCESS_TOKEN], + access_token=entry.data[CONF_ACCESS_TOKEN], websession=async_get_clientsession(hass), time_zone=dt_util.DEFAULT_TIME_ZONE, ) @@ -44,15 +65,7 @@ async def _close(event): try: await tibber_connection.update_info() except asyncio.TimeoutError: - _LOGGER.warning("Timeout connecting to Tibber. Will retry in %ss", retry_delay) - - async def retry_setup(now): - """Retry setup if a timeout happens on Tibber API.""" - await async_setup(hass, config, retry_delay=min(2 * retry_delay, 900)) - - async_call_later(hass, retry_delay, retry_setup) - - return True + raise ConfigEntryNotReady except aiohttp.ClientError as err: _LOGGER.error("Error connecting to Tibber: %s ", err) return False @@ -60,7 +73,34 @@ async def retry_setup(now): _LOGGER.error("Failed to login. %s", exp) return False - for component in ["sensor", "notify"]: - discovery.load_platform(hass, component, DOMAIN, {CONF_NAME: DOMAIN}, config) - + for component in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, component) + ) + + # set up notify platform, no entry support for notify component yet, + # have to use discovery to load platform. + hass.async_create_task( + discovery.async_load_platform( + hass, "notify", DOMAIN, {CONF_NAME: DOMAIN}, hass.data[DATA_HASS_CONFIG] + ) + ) return True + + +async def async_unload_entry(hass, config_entry): + """Unload a config entry.""" + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(config_entry, component) + for component in PLATFORMS + ] + ) + ) + + if unload_ok: + tibber_connection = hass.data.get(DOMAIN) + await tibber_connection.rt_disconnect() + + return unload_ok diff --git a/homeassistant/components/tibber/config_flow.py b/homeassistant/components/tibber/config_flow.py new file mode 100644 index 00000000000000..b0115d84e2c8af --- /dev/null +++ b/homeassistant/components/tibber/config_flow.py @@ -0,0 +1,68 @@ +"""Adds config flow for Tibber integration.""" +import asyncio +import logging + +import aiohttp +import tibber +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.helpers.aiohttp_client import async_get_clientsession + +from .const import DOMAIN # pylint:disable=unused-import + +_LOGGER = logging.getLogger(__name__) + +DATA_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str}) + + +class TibberConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for Tibber integration.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + + async def async_step_import(self, import_info): + """Set the config entry up from yaml.""" + return await self.async_step_user(import_info) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + + if self._async_current_entries(): + return self.async_abort(reason="already_configured") + + if user_input is not None: + access_token = user_input[CONF_ACCESS_TOKEN].replace(" ", "") + + tibber_connection = tibber.Tibber( + access_token=access_token, + websession=async_get_clientsession(self.hass), + ) + + errors = {} + + try: + await tibber_connection.update_info() + except asyncio.TimeoutError: + errors[CONF_ACCESS_TOKEN] = "timeout" + except aiohttp.ClientError: + errors[CONF_ACCESS_TOKEN] = "connection_error" + except tibber.InvalidLogin: + errors[CONF_ACCESS_TOKEN] = "invalid_access_token" + + if errors: + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors, + ) + + unique_id = tibber_connection.user_id + await self.async_set_unique_id(unique_id) + self._abort_if_unique_id_configured() + + return self.async_create_entry( + title=tibber_connection.name, data={CONF_ACCESS_TOKEN: access_token}, + ) + + return self.async_show_form(step_id="user", data_schema=DATA_SCHEMA, errors={},) diff --git a/homeassistant/components/tibber/const.py b/homeassistant/components/tibber/const.py new file mode 100644 index 00000000000000..a35fa89c40fab0 --- /dev/null +++ b/homeassistant/components/tibber/const.py @@ -0,0 +1,5 @@ +"""Constants for Tibber integration.""" + +DATA_HASS_CONFIG = "tibber_hass_config" +DOMAIN = "tibber" +MANUFACTURER = "Tibber" diff --git a/homeassistant/components/tibber/manifest.json b/homeassistant/components/tibber/manifest.json index 78249a9629138a..36f4002949b34e 100644 --- a/homeassistant/components/tibber/manifest.json +++ b/homeassistant/components/tibber/manifest.json @@ -2,7 +2,8 @@ "domain": "tibber", "name": "Tibber", "documentation": "https://www.home-assistant.io/integrations/tibber", - "requirements": ["pyTibber==0.13.8"], + "requirements": ["pyTibber==0.14.0"], "codeowners": ["@danielhiversen"], - "quality_scale": "silver" + "quality_scale": "silver", + "config_flow": true } diff --git a/homeassistant/components/tibber/sensor.py b/homeassistant/components/tibber/sensor.py index 36f1a65222c651..7fc8820e92d029 100644 --- a/homeassistant/components/tibber/sensor.py +++ b/homeassistant/components/tibber/sensor.py @@ -10,7 +10,7 @@ from homeassistant.helpers.entity import Entity from homeassistant.util import Throttle, dt as dt_util -from . import DOMAIN as TIBBER_DOMAIN +from .const import DOMAIN as TIBBER_DOMAIN, MANUFACTURER _LOGGER = logging.getLogger(__name__) @@ -20,10 +20,8 @@ MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=5) -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the Tibber sensor.""" - if discovery_info is None: - return tibber_connection = hass.data.get(TIBBER_DOMAIN) @@ -66,11 +64,34 @@ def device_state_attributes(self): """Return the state attributes.""" return self._device_state_attributes + @property + def model(self): + """Return the model of the sensor.""" + return None + @property def state(self): """Return the state of the device.""" return self._state + @property + def device_id(self): + """Return the ID of the physical device this sensor is part of.""" + home = self._tibber_home.info["viewer"]["home"] + return home["meteringPointData"]["consumptionEan"] + + @property + def device_info(self): + """Return the device_info of the device.""" + device_info = { + "identifiers": {(TIBBER_DOMAIN, self.device_id)}, + "name": self.name, + "manufacturer": MANUFACTURER, + } + if self.model is not None: + device_info["model"] = self.model + return device_info + class TibberSensorElPrice(TibberSensor): """Representation of a Tibber sensor for el price.""" @@ -112,6 +133,11 @@ def name(self): """Return the name of the sensor.""" return f"Electricity price {self._name}" + @property + def model(self): + """Return the model of the sensor.""" + return "Price Sensor" + @property def icon(self): """Return the icon to use in the frontend.""" @@ -125,8 +151,7 @@ def unit_of_measurement(self): @property def unique_id(self): """Return a unique ID.""" - home = self._tibber_home.info["viewer"]["home"] - return home["meteringPointData"]["consumptionEan"] + return self.device_id @Throttle(MIN_TIME_BETWEEN_UPDATES) async def _fetch_data(self): @@ -149,7 +174,7 @@ class TibberSensorRT(TibberSensor): """Representation of a Tibber sensor for real time consumption.""" async def async_added_to_hass(self): - """Start unavailability tracking.""" + """Start listen for real time data.""" await self._tibber_home.rt_subscribe(self.hass.loop, self._async_callback) async def _async_callback(self, payload): @@ -177,6 +202,11 @@ def available(self): """Return True if entity is available.""" return self._tibber_home.rt_subscription_running + @property + def model(self): + """Return the model of the sensor.""" + return "Tibber Pulse" + @property def name(self): """Return the name of the sensor.""" @@ -200,6 +230,4 @@ def unit_of_measurement(self): @property def unique_id(self): """Return a unique ID.""" - home = self._tibber_home.info["viewer"]["home"] - _id = home["meteringPointData"]["consumptionEan"] - return f"{_id}_rt_consumption" + return f"{self.device_id}_rt_consumption" diff --git a/homeassistant/components/tibber/strings.json b/homeassistant/components/tibber/strings.json new file mode 100644 index 00000000000000..483e7a28fcc5a2 --- /dev/null +++ b/homeassistant/components/tibber/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Tibber", + "config": { + "abort": { + "already_configured": "A Tibber account is already configured." + }, + "error": { + "timeout": "Timeout connecting to Tibber", + "connection_error": "Error connecting to Tibber", + "invalid_access_token": "[%key:common::config_flow::error::invalid_access_token%]" + }, + "step": { + "user": { + "data": { + "access_token": "Access token" + }, + "description": "Enter your access token from https://developer.tibber.com/settings/accesstoken", + "title": "Tibber" + } + } + } +} diff --git a/homeassistant/components/toon/translations/es-419.json b/homeassistant/components/toon/translations/es-419.json index 6fa63e591a31c3..af39c29971da15 100644 --- a/homeassistant/components/toon/translations/es-419.json +++ b/homeassistant/components/toon/translations/es-419.json @@ -1,7 +1,10 @@ { "config": { "abort": { + "client_id": "La identificaci\u00f3n del cliente de la configuraci\u00f3n no es v\u00e1lida.", + "client_secret": "El secreto del cliente de la configuraci\u00f3n no es v\u00e1lido.", "no_agreements": "Esta cuenta no tiene pantallas Toon.", + "no_app": "Debe configurar Toon antes de poder autenticarse con \u00e9l. [Lea las instrucciones] (https://www.home-assistant.io/components/toon/).", "unknown_auth_fail": "Ocurri\u00f3 un error inesperado, mientras se autenticaba." }, "error": { @@ -12,8 +15,10 @@ "authenticate": { "data": { "password": "Contrase\u00f1a", + "tenant": "Tenant", "username": "Nombre de usuario" }, + "description": "Autent\u00edquese con su cuenta de Eneco Toon (no con la cuenta de desarrollador).", "title": "Vincula tu cuenta de Toon" }, "display": { diff --git a/homeassistant/components/totalconnect/translations/es-419.json b/homeassistant/components/totalconnect/translations/es-419.json new file mode 100644 index 00000000000000..421d266709944c --- /dev/null +++ b/homeassistant/components/totalconnect/translations/es-419.json @@ -0,0 +1,19 @@ +{ + "config": { + "abort": { + "already_configured": "La cuenta ya ha sido configurada" + }, + "error": { + "login": "Error de inicio de sesi\u00f3n: compruebe su nombre de usuario y contrase\u00f1a" + }, + "step": { + "user": { + "data": { + "password": "Contrase\u00f1a", + "username": "Nombre de usuario" + }, + "title": "Total Connect" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/traccar/translations/es-419.json b/homeassistant/components/traccar/translations/es-419.json index bfe62cc4e78466..17f7560a464d79 100644 --- a/homeassistant/components/traccar/translations/es-419.json +++ b/homeassistant/components/traccar/translations/es-419.json @@ -3,6 +3,15 @@ "abort": { "not_internet_accessible": "Su instancia de Home Assistant debe estar accesible desde Internet para recibir mensajes de Traccar.", "one_instance_allowed": "Solo una instancia es necesaria." + }, + "create_entry": { + "default": "Para enviar eventos a Home Assistant, deber\u00e1 configurar la funci\u00f3n webhook en Traccar. \n\n Use la siguiente URL: `{webhook_url}` \n\n Consulte [la documentaci\u00f3n] ({docs_url}) para obtener m\u00e1s detalles." + }, + "step": { + "user": { + "description": "\u00bfSeguro que desea configurar Traccar?", + "title": "Configurar Traccar" + } } } } \ No newline at end of file diff --git a/homeassistant/components/tradfri/translations/es.json b/homeassistant/components/tradfri/translations/es.json index 1c66cca87b62ff..cf1a1e81b5a279 100644 --- a/homeassistant/components/tradfri/translations/es.json +++ b/homeassistant/components/tradfri/translations/es.json @@ -1,8 +1,8 @@ { "config": { "abort": { - "already_configured": "El puente ya esta configurado", - "already_in_progress": "La configuraci\u00f3n del bridge ya est\u00e1 en marcha." + "already_configured": "La pasarela ya est\u00e1 configurada", + "already_in_progress": "La configuraci\u00f3n de la pasarela ya est\u00e1 en marcha." }, "error": { "cannot_connect": "No se puede conectar a la puerta de enlace.", diff --git a/homeassistant/components/transmission/translations/es-419.json b/homeassistant/components/transmission/translations/es-419.json index 6a01b3e25a1393..002d03ee7d2f9d 100644 --- a/homeassistant/components/transmission/translations/es-419.json +++ b/homeassistant/components/transmission/translations/es-419.json @@ -1,10 +1,33 @@ { + "config": { + "abort": { + "already_configured": "El host ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se puede conectar al host", + "name_exists": "El nombre ya existe", + "wrong_credentials": "Nombre de usuario o contrase\u00f1a incorrectos" + }, + "step": { + "user": { + "data": { + "host": "Host", + "name": "Nombre", + "password": "Contrase\u00f1a", + "port": "Puerto", + "username": "Nombre de usuario" + }, + "title": "Configurar cliente de transmisi\u00f3n" + } + } + }, "options": { "step": { "init": { "data": { "scan_interval": "Frecuencia de actualizaci\u00f3n" - } + }, + "title": "Configurar opciones para la transmisi\u00f3n" } } } diff --git a/homeassistant/components/trend/manifest.json b/homeassistant/components/trend/manifest.json index edd0bea977d563..0d741bcf264ac1 100644 --- a/homeassistant/components/trend/manifest.json +++ b/homeassistant/components/trend/manifest.json @@ -2,7 +2,7 @@ "domain": "trend", "name": "Trend", "documentation": "https://www.home-assistant.io/integrations/trend", - "requirements": ["numpy==1.18.2"], + "requirements": ["numpy==1.18.4"], "codeowners": [], "quality_scale": "internal" } diff --git a/homeassistant/components/twentemilieu/translations/es-419.json b/homeassistant/components/twentemilieu/translations/es-419.json index 5cc3dc4b2c9d9e..6f190ba503ca00 100644 --- a/homeassistant/components/twentemilieu/translations/es-419.json +++ b/homeassistant/components/twentemilieu/translations/es-419.json @@ -1,11 +1,20 @@ { "config": { + "abort": { + "address_exists": "Direcci\u00f3n ya configurada." + }, + "error": { + "connection_error": "Error al conectar.", + "invalid_address": "Direcci\u00f3n no encontrada en el \u00e1rea de servicio de Twente Milieu." + }, "step": { "user": { "data": { + "house_letter": "Carta de la casa/adicional", "house_number": "N\u00famero de casa", "post_code": "C\u00f3digo postal" }, + "description": "Configure Twente Milieu proporcionando informaci\u00f3n de recolecci\u00f3n de residuos en su direcci\u00f3n.", "title": "Twente Milieu" } } diff --git a/homeassistant/components/twentemilieu/translations/no.json b/homeassistant/components/twentemilieu/translations/no.json index d7d383ae371683..a9d3c184495cd1 100644 --- a/homeassistant/components/twentemilieu/translations/no.json +++ b/homeassistant/components/twentemilieu/translations/no.json @@ -1,7 +1,7 @@ { "config": { "abort": { - "address_exists": "Adressen er allerede konfigurert." + "address_exists": "Adressen er allerede satt opp." }, "error": { "connection_error": "Tilkobling mislyktes.", diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 72ba593bae6ed6..8c836f77131377 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -32,7 +32,12 @@ LOGGER, ) from .controller import get_controller -from .errors import AlreadyConfigured, AuthenticationRequired, CannotConnect +from .errors import ( + AlreadyConfigured, + AuthenticationRequired, + CannotConnect, + NoLocalUser, +) DEFAULT_PORT = 8443 DEFAULT_SITE_ID = "default" @@ -129,6 +134,8 @@ async def async_step_site(self, user_input=None): for site in self.sites.values(): if desc == site["desc"]: + if "role" not in site: + raise NoLocalUser self.config[CONF_SITE_ID] = site["name"] break @@ -147,6 +154,9 @@ async def async_step_site(self, user_input=None): except AlreadyConfigured: return self.async_abort(reason="already_configured") + except NoLocalUser: + return self.async_abort(reason="no_local_user") + if len(self.sites) == 1: self.desc = next(iter(self.sites.values()))["desc"] return await self.async_step_site(user_input={}) @@ -219,7 +229,12 @@ async def async_step_device_tracker(self, user_input=None): self.options.update(user_input) return await self.async_step_client_control() - ssid_filter = {wlan: wlan for wlan in self.controller.api.wlans} + ssids = list(self.controller.api.wlans) + [ + f"{wlan.name}{wlan.name_combine_suffix}" + for wlan in self.controller.api.wlans.values() + if not wlan.name_combine_enabled + ] + ssid_filter = {ssid: ssid for ssid in sorted(ssids)} return self.async_show_form( step_id="device_tracker", diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index 8ccb42794ec1e8..acac9c8e371b62 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -6,14 +6,19 @@ from aiohttp import CookieJar import aiounifi from aiounifi.controller import ( - DATA_CLIENT, DATA_CLIENT_REMOVED, - DATA_DEVICE, DATA_EVENT, SIGNAL_CONNECTION_STATE, SIGNAL_DATA, ) -from aiounifi.events import WIRELESS_CLIENT_CONNECTED, WIRELESS_GUEST_CONNECTED +from aiounifi.events import ( + ACCESS_POINT_CONNECTED, + GATEWAY_CONNECTED, + SWITCH_CONNECTED, + WIRED_CLIENT_CONNECTED, + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, +) from aiounifi.websocket import STATE_DISCONNECTED, STATE_RUNNING import async_timeout @@ -55,6 +60,17 @@ RETRY_TIMER = 15 SUPPORTED_PLATFORMS = [TRACKER_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN] +CLIENT_CONNECTED = ( + WIRED_CLIENT_CONNECTED, + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, +) +DEVICE_CONNECTED = ( + ACCESS_POINT_CONNECTED, + GATEWAY_CONNECTED, + SWITCH_CONNECTED, +) + class UniFiController: """Manages a single UniFi Controller.""" @@ -190,14 +206,33 @@ def async_unifi_signalling_callback(self, signal, data): elif signal == SIGNAL_DATA and data: if DATA_EVENT in data: - if data[DATA_EVENT].event in ( - WIRELESS_CLIENT_CONNECTED, - WIRELESS_GUEST_CONNECTED, - ): - self.update_wireless_clients() + clients_connected = set() + devices_connected = set() + wireless_clients_connected = False - elif DATA_CLIENT in data or DATA_DEVICE in data: - async_dispatcher_send(self.hass, self.signal_update) + for event in data[DATA_EVENT]: + + if event.event in CLIENT_CONNECTED: + clients_connected.add(event.mac) + + if not wireless_clients_connected and event.event in ( + WIRELESS_CLIENT_CONNECTED, + WIRELESS_GUEST_CONNECTED, + ): + wireless_clients_connected = True + + elif event.event in DEVICE_CONNECTED: + devices_connected.add(event.mac) + + if wireless_clients_connected: + self.update_wireless_clients() + if clients_connected or devices_connected: + async_dispatcher_send( + self.hass, + self.signal_update, + clients_connected, + devices_connected, + ) elif DATA_CLIENT_REMOVED in data: async_dispatcher_send( diff --git a/homeassistant/components/unifi/device_tracker.py b/homeassistant/components/unifi/device_tracker.py index 0b71a7d517e5e5..161c862f6b4638 100644 --- a/homeassistant/components/unifi/device_tracker.py +++ b/homeassistant/components/unifi/device_tracker.py @@ -48,10 +48,15 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller.entities[DOMAIN] = {CLIENT_TRACKER: set(), DEVICE_TRACKER: set()} @callback - def items_added(): + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: """Update the values of the controller.""" - if controller.option_track_clients or controller.option_track_devices: - add_entities(controller, async_add_entities) + if controller.option_track_clients: + add_client_entities(controller, async_add_entities, clients) + + if controller.option_track_devices: + add_device_entities(controller, async_add_entities, devices) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -60,38 +65,43 @@ def items_added(): @callback -def add_entities(controller, async_add_entities): - """Add new tracker entities from the controller.""" +def add_client_entities(controller, async_add_entities, clients): + """Add new client tracker entities from the controller.""" trackers = [] - for items, tracker_class, track in ( - (controller.api.clients, UniFiClientTracker, controller.option_track_clients), - (controller.api.devices, UniFiDeviceTracker, controller.option_track_devices), - ): - if not track: + for mac in clients: + if mac in controller.entities[DOMAIN][UniFiClientTracker.TYPE]: continue - for mac in items: + client = controller.api.clients[mac] - if mac in controller.entities[DOMAIN][tracker_class.TYPE]: + if mac not in controller.wireless_clients: + if not controller.option_track_wired_clients: continue + elif ( + client.essid + and controller.option_ssid_filter + and client.essid not in controller.option_ssid_filter + ): + continue - item = items[mac] + trackers.append(UniFiClientTracker(client, controller)) - if tracker_class is UniFiClientTracker: + if trackers: + async_add_entities(trackers) - if mac not in controller.wireless_clients: - if not controller.option_track_wired_clients: - continue - else: - if ( - item.essid - and controller.option_ssid_filter - and item.essid not in controller.option_ssid_filter - ): - continue - trackers.append(tracker_class(item, controller)) +@callback +def add_device_entities(controller, async_add_entities, devices): + """Add new device tracker entities from the controller.""" + trackers = [] + + for mac in devices: + if mac in controller.entities[DOMAIN][UniFiDeviceTracker.TYPE]: + continue + + device = controller.api.devices[mac] + trackers.append(UniFiDeviceTracker(device, controller)) if trackers: async_add_entities(trackers) @@ -147,6 +157,7 @@ def _scheduled_update(now): dt_util.utcnow() + self.controller.option_detection_time, ) + # SSID filter if ( not self.is_wired and self.client.essid @@ -156,6 +167,10 @@ def _scheduled_update(now): ): return False + # A client that has never been seen cannot be connected. + if self.client.last_seen is None: + return False + if self.is_disconnected is not None: return not self.is_disconnected @@ -167,10 +182,6 @@ def _scheduled_update(now): else: self.wired_bug = None - # A client that has never been seen cannot be connected. - if self.client.last_seen is None: - return False - since_last_seen = dt_util.utcnow() - dt_util.utc_from_timestamp( float(self.client.last_seen) ) @@ -240,7 +251,6 @@ def mac(self): async def async_added_to_hass(self): """Subscribe to device events.""" await super().async_added_to_hass() - LOGGER.debug("New device %s (%s)", self.entity_id, self.device.mac) self.device.register_callback(self.async_update_callback) async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/unifi/errors.py b/homeassistant/components/unifi/errors.py index c90c4956312a34..e0da64f245c398 100644 --- a/homeassistant/components/unifi/errors.py +++ b/homeassistant/components/unifi/errors.py @@ -22,5 +22,9 @@ class LoginRequired(UnifiException): """Component got logged out.""" +class NoLocalUser(UnifiException): + """No local user.""" + + class UserLevel(UnifiException): """User level too low.""" diff --git a/homeassistant/components/unifi/manifest.json b/homeassistant/components/unifi/manifest.json index 0a5ba84cdb3cbc..8c05d19531680f 100644 --- a/homeassistant/components/unifi/manifest.json +++ b/homeassistant/components/unifi/manifest.json @@ -3,7 +3,7 @@ "name": "Ubiquiti UniFi", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/unifi", - "requirements": ["aiounifi==18"], + "requirements": ["aiounifi==20"], "codeowners": ["@kane610"], "quality_scale": "platinum" } diff --git a/homeassistant/components/unifi/sensor.py b/homeassistant/components/unifi/sensor.py index 9077db49dac948..cf3cc2b1b5a4c8 100644 --- a/homeassistant/components/unifi/sensor.py +++ b/homeassistant/components/unifi/sensor.py @@ -25,10 +25,12 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller.entities[DOMAIN] = {RX_SENSOR: set(), TX_SENSOR: set()} @callback - def items_added(): + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: """Update the values of the controller.""" if controller.option_allow_bandwidth_sensors: - add_entities(controller, async_add_entities) + add_entities(controller, async_add_entities, clients) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -37,14 +39,17 @@ def items_added(): @callback -def add_entities(controller, async_add_entities): +def add_entities(controller, async_add_entities, clients): """Add new sensor entities from the controller.""" sensors = [] - for mac in controller.api.clients: + for mac in clients: for sensor_class in (UniFiRxBandwidthSensor, UniFiTxBandwidthSensor): - if mac not in controller.entities[DOMAIN][sensor_class.TYPE]: - sensors.append(sensor_class(controller.api.clients[mac], controller)) + if mac in controller.entities[DOMAIN][sensor_class.TYPE]: + continue + + client = controller.api.clients[mac] + sensors.append(sensor_class(client, controller)) if sensors: async_add_entities(sensors) diff --git a/homeassistant/components/unifi/strings.json b/homeassistant/components/unifi/strings.json index da1d6200ed5cf9..2650066da5f478 100644 --- a/homeassistant/components/unifi/strings.json +++ b/homeassistant/components/unifi/strings.json @@ -20,6 +20,7 @@ }, "abort": { "already_configured": "Controller site is already configured", + "no_local_user": "No local user found, configure a local account on controller and try again", "user_privilege": "User needs to be administrator" } }, diff --git a/homeassistant/components/unifi/switch.py b/homeassistant/components/unifi/switch.py index ea39c82853a5e3..aee22691834a6c 100644 --- a/homeassistant/components/unifi/switch.py +++ b/homeassistant/components/unifi/switch.py @@ -51,10 +51,17 @@ async def async_setup_entry(hass, config_entry, async_add_entities): controller.api.clients.process_raw([client.raw]) @callback - def items_added(): + def items_added( + clients: set = controller.api.clients, devices: set = controller.api.devices + ) -> None: """Update the values of the controller.""" - if controller.option_block_clients or controller.option_poe_clients: - add_entities(controller, async_add_entities, previously_known_poe_clients) + if controller.option_block_clients: + add_block_entities(controller, async_add_entities, clients) + + if controller.option_poe_clients: + add_poe_entities( + controller, async_add_entities, clients, previously_known_poe_clients + ) for signal in (controller.signal_update, controller.signal_options_update): controller.listeners.append(async_dispatcher_connect(hass, signal, items_added)) @@ -64,62 +71,66 @@ def items_added(): @callback -def add_entities(controller, async_add_entities, previously_known_poe_clients): +def add_block_entities(controller, async_add_entities, clients): """Add new switch entities from the controller.""" switches = [] for mac in controller.option_block_clients: - - if ( - mac in controller.entities[DOMAIN][BLOCK_SWITCH] - or mac not in controller.api.clients - ): + if mac in controller.entities[DOMAIN][BLOCK_SWITCH] or mac not in clients: continue client = controller.api.clients[mac] switches.append(UniFiBlockClientSwitch(client, controller)) - if controller.option_poe_clients: - devices = controller.api.devices + if switches: + async_add_entities(switches) - for mac in controller.api.clients: - poe_client_id = f"{POE_SWITCH}-{mac}" +@callback +def add_poe_entities( + controller, async_add_entities, clients, previously_known_poe_clients +): + """Add new switch entities from the controller.""" + switches = [] + + devices = controller.api.devices - if mac in controller.entities[DOMAIN][POE_SWITCH]: - continue + for mac in clients: + if mac in controller.entities[DOMAIN][POE_SWITCH]: + continue - client = controller.api.clients[mac] + poe_client_id = f"{POE_SWITCH}-{mac}" + client = controller.api.clients[mac] - if poe_client_id not in previously_known_poe_clients and ( - mac in controller.wireless_clients - or client.sw_mac not in devices - or not devices[client.sw_mac].ports[client.sw_port].port_poe - or not devices[client.sw_mac].ports[client.sw_port].poe_enable - or controller.mac == client.mac - ): - continue + if poe_client_id not in previously_known_poe_clients and ( + mac in controller.wireless_clients + or client.sw_mac not in devices + or not devices[client.sw_mac].ports[client.sw_port].port_poe + or not devices[client.sw_mac].ports[client.sw_port].poe_enable + or controller.mac == client.mac + ): + continue - # Multiple POE-devices on same port means non UniFi POE driven switch - multi_clients_on_port = False - for client2 in controller.api.clients.values(): + # Multiple POE-devices on same port means non UniFi POE driven switch + multi_clients_on_port = False + for client2 in controller.api.clients.values(): - if poe_client_id in previously_known_poe_clients: - break + if poe_client_id in previously_known_poe_clients: + break - if ( - client2.is_wired - and client.mac != client2.mac - and client.sw_mac == client2.sw_mac - and client.sw_port == client2.sw_port - ): - multi_clients_on_port = True - break + if ( + client2.is_wired + and client.mac != client2.mac + and client.sw_mac == client2.sw_mac + and client.sw_port == client2.sw_port + ): + multi_clients_on_port = True + break - if multi_clients_on_port: - continue + if multi_clients_on_port: + continue - switches.append(UniFiPOEClientSwitch(client, controller)) + switches.append(UniFiPOEClientSwitch(client, controller)) if switches: async_add_entities(switches) diff --git a/homeassistant/components/unifi/translations/de.json b/homeassistant/components/unifi/translations/de.json index 0683f3da594108..d7b92502f0bf60 100644 --- a/homeassistant/components/unifi/translations/de.json +++ b/homeassistant/components/unifi/translations/de.json @@ -51,6 +51,9 @@ "other": "andere" } }, + "simple_options": { + "description": "Konfigurieren Sie die UniFi-Integration" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Bandbreitennutzungssensoren f\u00fcr Netzwerkclients" diff --git a/homeassistant/components/unifi/translations/en.json b/homeassistant/components/unifi/translations/en.json index e49bbbcc50ec3c..ad14f09b3007b4 100644 --- a/homeassistant/components/unifi/translations/en.json +++ b/homeassistant/components/unifi/translations/en.json @@ -2,6 +2,7 @@ "config": { "abort": { "already_configured": "Controller site is already configured", + "no_local_user": "No local user found, configure a local account on controller and try again", "user_privilege": "User needs to be administrator" }, "error": { diff --git a/homeassistant/components/unifi/translations/es-419.json b/homeassistant/components/unifi/translations/es-419.json index ac50051b6c8e68..7f92a0b45afe79 100644 --- a/homeassistant/components/unifi/translations/es-419.json +++ b/homeassistant/components/unifi/translations/es-419.json @@ -6,7 +6,8 @@ }, "error": { "faulty_credentials": "Credenciales de usuario incorrectas", - "service_unavailable": "No hay servicio disponible" + "service_unavailable": "No hay servicio disponible", + "unknown_client_mac": "Ning\u00fan cliente disponible en esa direcci\u00f3n MAC" }, "step": { "user": { @@ -21,5 +22,45 @@ "title": "Configurar el controlador UniFi" } } + }, + "options": { + "step": { + "client_control": { + "data": { + "block_client": "Acceso controlado a la red de clientes", + "new_client": "Agregar nuevo cliente para control de acceso a la red", + "poe_clients": "Permitir control POE de clientes" + }, + "description": "Configurar controles de cliente \n\nCree conmutadores para los n\u00fameros de serie para los que desea 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 que se consider\u00f3", + "ignore_wired_bug": "Deshabilitar la l\u00f3gica de error con cable UniFi", + "ssid_filter": "Seleccione SSID para rastrear clientes inal\u00e1mbricos en", + "track_clients": "Rastree clientes de red", + "track_devices": "Dispositivos de red de seguimiento (dispositivos Ubiquiti)", + "track_wired_clients": "Incluir clientes de red cableada" + }, + "description": "Configurar el seguimiento del dispositivo", + "title": "Opciones UniFi 1/3" + }, + "simple_options": { + "data": { + "block_client": "Acceso controlado a la red de clientes", + "track_clients": "Rastree clientes en la red", + "track_devices": "Rastree dispositivos de red (dispositivos Ubiquiti)" + }, + "description": "Configurar la integraci\u00f3n de UniFi" + }, + "statistics_sensors": { + "data": { + "allow_bandwidth_sensors": "Sensores de uso de ancho de banda para clientes de red" + }, + "description": "Configurar sensores de estad\u00edsticas", + "title": "Opciones UniFi 3/3" + } + } } } \ No newline at end of file diff --git a/homeassistant/components/unifi/translations/it.json b/homeassistant/components/unifi/translations/it.json index cde18ca4029479..af902565b924b3 100644 --- a/homeassistant/components/unifi/translations/it.json +++ b/homeassistant/components/unifi/translations/it.json @@ -52,6 +52,14 @@ "other": "altri" } }, + "simple_options": { + "data": { + "block_client": "Accesso alla rete dei client controllati", + "track_clients": "Traccia i client di rete", + "track_devices": "Traccia i dispositivi di rete (dispositivi Ubiquiti)" + }, + "description": "Configurare l'integrazione UniFi" + }, "statistics_sensors": { "data": { "allow_bandwidth_sensors": "Sensori di utilizzo della larghezza di banda per i client di rete" diff --git a/homeassistant/components/unifi/unifi_client.py b/homeassistant/components/unifi/unifi_client.py index 398df4206b6fac..bc456aa273227d 100644 --- a/homeassistant/components/unifi/unifi_client.py +++ b/homeassistant/components/unifi/unifi_client.py @@ -12,6 +12,7 @@ WIRELESS_CLIENT_CONNECTED, WIRELESS_CLIENT_DISCONNECTED, WIRELESS_CLIENT_ROAM, + WIRELESS_CLIENT_ROAMRADIO, WIRELESS_CLIENT_UNBLOCKED, ) @@ -25,7 +26,6 @@ CLIENT_BLOCKED = (WIRED_CLIENT_BLOCKED, WIRELESS_CLIENT_BLOCKED) CLIENT_UNBLOCKED = (WIRED_CLIENT_UNBLOCKED, WIRELESS_CLIENT_UNBLOCKED) WIRED_CLIENT = (WIRED_CLIENT_CONNECTED, WIRED_CLIENT_DISCONNECTED) -WIRELESS_CLIENT_ROAMRADIO = "EVT_WU_RoamRadio" WIRELESS_CLIENT = ( WIRELESS_CLIENT_CONNECTED, WIRELESS_CLIENT_DISCONNECTED, @@ -55,7 +55,6 @@ def mac(self): async def async_added_to_hass(self) -> None: """Client entity created.""" await super().async_added_to_hass() - LOGGER.debug("New client %s (%s)", self.entity_id, self.client.mac) self.client.register_callback(self.async_update_callback) async def async_will_remove_from_hass(self) -> None: diff --git a/homeassistant/components/unifi/unifi_entity_base.py b/homeassistant/components/unifi/unifi_entity_base.py index 94088411411d73..9a7a5567ce8be0 100644 --- a/homeassistant/components/unifi/unifi_entity_base.py +++ b/homeassistant/components/unifi/unifi_entity_base.py @@ -1,10 +1,13 @@ """Base class for UniFi entities.""" +import logging from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.helpers.entity_registry import async_entries_for_device +LOGGER = logging.getLogger(__name__) + class UniFiBase(Entity): """UniFi entity base class.""" @@ -27,6 +30,7 @@ def mac(self): async def async_added_to_hass(self) -> None: """Entity created.""" + LOGGER.debug("New %s entity %s (%s)", self.TYPE, self.entity_id, self.mac) for signal, method in ( (self.controller.signal_reachable, self.async_update_callback), (self.controller.signal_options_update, self.options_updated), diff --git a/homeassistant/components/updater/__init__.py b/homeassistant/components/updater/__init__.py index 5771a4f0cfe17b..869e3c55271994 100644 --- a/homeassistant/components/updater/__init__.py +++ b/homeassistant/components/updater/__init__.py @@ -1,9 +1,7 @@ """Support to check for available updates.""" from datetime import timedelta from distutils.version import StrictVersion -import json import logging -import uuid import async_timeout from distro import linux_distribution # pylint: disable=import-error @@ -25,7 +23,6 @@ DOMAIN = "updater" UPDATER_URL = "https://updater.home-assistant.io/" -UPDATER_UUID_FILE = ".uuid" CONFIG_SCHEMA = vol.Schema( { @@ -52,26 +49,6 @@ def __init__(self, update_available: bool, newest_version: str, release_notes: s self.newest_version = newest_version -def _create_uuid(hass, filename=UPDATER_UUID_FILE): - """Create UUID and save it in a file.""" - with open(hass.config.path(filename), "w") as fptr: - _uuid = uuid.uuid4().hex - fptr.write(json.dumps({"uuid": _uuid})) - return _uuid - - -def _load_uuid(hass, filename=UPDATER_UUID_FILE): - """Load UUID from a file or return None.""" - try: - with open(hass.config.path(filename)) as fptr: - jsonf = json.loads(fptr.read()) - return uuid.UUID(jsonf["uuid"], version=4).hex - except (ValueError, AttributeError): - return None - except FileNotFoundError: - return _create_uuid(hass, filename) - - async def async_setup(hass, config): """Set up the updater component.""" if "dev" in current_version: @@ -80,7 +57,7 @@ async def async_setup(hass, config): conf = config.get(DOMAIN, {}) if conf.get(CONF_REPORTING): - huuid = await hass.async_add_job(_load_uuid, hass) + huuid = await hass.helpers.instance_id.async_get() else: huuid = None diff --git a/homeassistant/components/upnp/__init__.py b/homeassistant/components/upnp/__init__.py index 4d599be88b1cbd..a59e4c641055de 100644 --- a/homeassistant/components/upnp/__init__.py +++ b/homeassistant/components/upnp/__init__.py @@ -1,24 +1,24 @@ """Open ports in your router for Home Assistant and provide statistics.""" from ipaddress import ip_address from operator import itemgetter -from typing import Mapping import voluptuous as vol from homeassistant import config_entries from homeassistant.config_entries import ConfigEntry -from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import config_validation as cv, device_registry as dr from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import get_local_ip from .const import ( - CONF_ENABLE_PORT_MAPPING, - CONF_ENABLE_SENSORS, - CONF_HASS, CONF_LOCAL_IP, - CONF_PORTS, + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, DOMAIN, LOGGER as _LOGGER, ) @@ -28,101 +28,52 @@ NOTIFICATION_TITLE = "UPnP/IGD Setup" CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_ENABLE_PORT_MAPPING, default=False): cv.boolean, - vol.Optional(CONF_ENABLE_SENSORS, default=True): cv.boolean, - vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string), - vol.Optional(CONF_PORTS, default={}): vol.Schema( - {vol.Any(CONF_HASS, cv.port): vol.Any(CONF_HASS, cv.port)} - ), - } - ) - }, + {DOMAIN: vol.Schema({vol.Optional(CONF_LOCAL_IP): vol.All(ip_address, cv.string)})}, extra=vol.ALLOW_EXTRA, ) -def _substitute_hass_ports(ports: Mapping, hass_port: int = None) -> Mapping: - """ - Substitute 'hass' for the hass_port. - - This triggers a warning when hass_port is None. - """ - ports = ports.copy() - - # substitute 'hass' for hass_port, both keys and values - if CONF_HASS in ports: - if hass_port is None: - _LOGGER.warning( - "Could not determine Home Assistant http port, " - "not setting up port mapping from %s to %s. " - "Enable the http-component.", - CONF_HASS, - ports[CONF_HASS], - ) - else: - ports[hass_port] = ports[CONF_HASS] - del ports[CONF_HASS] - - for port in ports: - if ports[port] == CONF_HASS: - if hass_port is None: - _LOGGER.warning( - "Could not determine Home Assistant http port, " - "not setting up port mapping from %s to %s. " - "Enable the http-component.", - port, - ports[port], - ) - del ports[port] - else: - ports[port] = hass_port - - return ports - - async def async_discover_and_construct( hass: HomeAssistantType, udn: str = None, st: str = None ) -> Device: """Discovery devices and construct a Device for one.""" # pylint: disable=invalid-name discovery_infos = await Device.async_discover(hass) + _LOGGER.debug("Discovered devices: %s", discovery_infos) if not discovery_infos: _LOGGER.info("No UPnP/IGD devices discovered") return None if udn: - # get the discovery info with specified UDN - _LOGGER.debug("Discovery_infos: %s", discovery_infos) - filtered = [di for di in discovery_infos if di["udn"] == udn] + # Get the discovery info with specified UDN/ST. + filtered = [di for di in discovery_infos if di[DISCOVERY_UDN] == udn] if st: - _LOGGER.debug("Filtering on ST: %s", st) - filtered = [di for di in discovery_infos if di["st"] == st] + filtered = [di for di in discovery_infos if di[DISCOVERY_ST] == st] if not filtered: _LOGGER.warning( - 'Wanted UPnP/IGD device with UDN "%s" not found, ' "aborting", udn + 'Wanted UPnP/IGD device with UDN "%s" not found, aborting', udn ) return None - # ensure we're always taking the latest - filtered = sorted(filtered, key=itemgetter("st"), reverse=True) + + # Ensure we're always taking the latest, if we filtered only on UDN. + filtered = sorted(filtered, key=itemgetter(DISCOVERY_ST), reverse=True) discovery_info = filtered[0] else: - # get the first/any + # Get the first/any. discovery_info = discovery_infos[0] if len(discovery_infos) > 1: device_name = discovery_info.get( - "usn", discovery_info.get("ssdp_description", "") + DISCOVERY_USN, discovery_info.get(DISCOVERY_LOCATION, "") ) _LOGGER.info("Detected multiple UPnP/IGD devices, using: %s", device_name) - ssdp_description = discovery_info["ssdp_description"] - return await Device.async_create_device(hass, ssdp_description) + location = discovery_info[DISCOVERY_LOCATION] + return await Device.async_create_device(hass, location) async def async_setup(hass: HomeAssistantType, config: ConfigType): """Set up UPnP component.""" + _LOGGER.debug("async_setup, config: %s", config) conf_default = CONFIG_SCHEMA({DOMAIN: {}})[DOMAIN] conf = config.get(DOMAIN, conf_default) local_ip = await hass.async_add_executor_job(get_local_ip) @@ -130,10 +81,10 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): "config": conf, "devices": {}, "local_ip": conf.get(CONF_LOCAL_IP, local_ip), - "ports": conf.get(CONF_PORTS), } - if conf is not None: + # Only start if set up via configuration.yaml. + if DOMAIN in config: hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT} @@ -145,25 +96,26 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType): async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) -> bool: """Set up UPnP/IGD device from a config entry.""" - domain_data = hass.data[DOMAIN] - conf = domain_data["config"] + _LOGGER.debug("async_setup_entry, config_entry: %s", config_entry.data) # discover and construct - udn = config_entry.data.get("udn") - st = config_entry.data.get("st") # pylint: disable=invalid-name + udn = config_entry.data.get(CONFIG_ENTRY_UDN) + st = config_entry.data.get(CONFIG_ENTRY_ST) # pylint: disable=invalid-name device = await async_discover_and_construct(hass, udn, st) if not device: _LOGGER.info("Unable to create UPnP/IGD, aborting") raise ConfigEntryNotReady - # 'register'/save UDN + ST + # Save device hass.data[DOMAIN]["devices"][device.udn] = device - hass.config_entries.async_update_entry( - entry=config_entry, - data={**config_entry.data, "udn": device.udn, "st": device.device_type}, - ) - # create device registry entry + # Ensure entry has proper unique_id. + if config_entry.unique_id != device.unique_id: + hass.config_entries.async_update_entry( + entry=config_entry, unique_id=device.unique_id, + ) + + # Create device registry entry. device_registry = await dr.async_get_registry(hass) device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, @@ -174,35 +126,11 @@ async def async_setup_entry(hass: HomeAssistantType, config_entry: ConfigEntry) model=device.model_name, ) - # set up sensors - if conf.get(CONF_ENABLE_SENSORS): - _LOGGER.debug("Enabling sensors") - - # register sensor setup handlers - hass.async_create_task( - hass.config_entries.async_forward_entry_setup(config_entry, "sensor") - ) - - # set up port mapping - if conf.get(CONF_ENABLE_PORT_MAPPING): - _LOGGER.debug("Enabling port mapping") - local_ip = domain_data[CONF_LOCAL_IP] - ports = conf.get(CONF_PORTS, {}) - - hass_port = None - if hasattr(hass, "http"): - hass_port = hass.http.server_port - - ports = _substitute_hass_ports(ports, hass_port=hass_port) - await device.async_add_port_mappings(ports, local_ip) - - # set up port mapping deletion on stop-hook - async def delete_port_mapping(event): - """Delete port mapping on quit.""" - _LOGGER.debug("Deleting port mappings") - await device.async_delete_port_mappings() - - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, delete_port_mapping) + # Create sensors. + _LOGGER.debug("Enabling sensors") + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(config_entry, "sensor") + ) return True @@ -211,13 +139,5 @@ async def async_unload_entry( hass: HomeAssistantType, config_entry: ConfigEntry ) -> bool: """Unload a UPnP/IGD device from a config entry.""" - udn = config_entry.data["udn"] - device = hass.data[DOMAIN]["devices"][udn] - - # remove port mapping - _LOGGER.debug("Deleting port mappings") - await device.async_delete_port_mappings() - - # remove sensors _LOGGER.debug("Deleting sensors") return await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") diff --git a/homeassistant/components/upnp/config_flow.py b/homeassistant/components/upnp/config_flow.py index 1601595b6a9f5d..4701f21633c22e 100644 --- a/homeassistant/components/upnp/config_flow.py +++ b/homeassistant/components/upnp/config_flow.py @@ -1,10 +1,187 @@ """Config flow for UPNP.""" +from typing import Mapping, Optional + +import voluptuous as vol + from homeassistant import config_entries -from homeassistant.helpers import config_entry_flow +from homeassistant.components import ssdp -from .const import DOMAIN +from .const import ( # pylint: disable=unused-import + CONFIG_ENTRY_ST, + CONFIG_ENTRY_UDN, + DISCOVERY_LOCATION, + DISCOVERY_NAME, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, + DOMAIN, + LOGGER as _LOGGER, +) from .device import Device -config_entry_flow.register_discovery_flow( - DOMAIN, "UPnP/IGD", Device.async_discover, config_entries.CONN_CLASS_LOCAL_POLL -) + +class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a UPnP/IGD config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL + + # Paths: + # - ssdp(discovery_info) --> ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() + # - user(None): scan --> user({...}) --> create_entry() + # - import(None) --> create_entry() + + def __init__(self): + """Initialize the UPnP/IGD config flow.""" + self._discoveries: Mapping = None + + async def async_step_user(self, user_input: Optional[Mapping] = None): + """Handle a flow start.""" + _LOGGER.debug("async_step_user: user_input: %s", user_input) + # This uses DISCOVERY_USN as the identifier for the device. + + if user_input is not None: + # Ensure wanted device was discovered. + matching_discoveries = [ + discovery + for discovery in self._discoveries + if discovery[DISCOVERY_USN] == user_input["usn"] + ] + if not matching_discoveries: + return self.async_abort(reason="no_devices_discovered") + + discovery = matching_discoveries[0] + await self.async_set_unique_id( + discovery[DISCOVERY_USN], raise_on_progress=False + ) + return await self._async_create_entry_from_data(discovery) + + # Discover devices. + discoveries = await Device.async_discover(self.hass) + + # Store discoveries which have not been configured, add name for each discovery. + current_usns = {entry.unique_id for entry in self._async_current_entries()} + self._discoveries = [ + { + **discovery, + DISCOVERY_NAME: await self._async_get_name_for_discovery(discovery), + } + for discovery in discoveries + if discovery[DISCOVERY_USN] not in current_usns + ] + + # Ensure anything to add. + if not self._discoveries: + return self.async_abort(reason="no_devices_found") + + data_schema = vol.Schema( + { + vol.Required("usn"): vol.In( + { + discovery[DISCOVERY_USN]: discovery[DISCOVERY_NAME] + for discovery in self._discoveries + } + ), + } + ) + return self.async_show_form(step_id="user", data_schema=data_schema,) + + async def async_step_import(self, import_info: Optional[Mapping]): + """Import a new UPnP/IGD device as a config entry. + + This flow is triggered by `async_setup`. If no device has been + configured before, find any device and create a config_entry for it. + Otherwise, do nothing. + """ + _LOGGER.debug("async_step_import: import_info: %s", import_info) + + if import_info is None: + # Landed here via configuration.yaml entry. + # Any device already added, then abort. + if self._async_current_entries(): + _LOGGER.debug("aborting, already configured") + return self.async_abort(reason="already_configured") + + # Test if import_info isn't already configured. + if import_info is not None and any( + import_info["udn"] == entry.data[CONFIG_ENTRY_UDN] + and import_info["st"] == entry.data[CONFIG_ENTRY_ST] + for entry in self._async_current_entries() + ): + return self.async_abort(reason="already_configured") + + # Discover devices. + self._discoveries = await Device.async_discover(self.hass) + + # Ensure anything to add. If not, silently abort. + if not self._discoveries: + _LOGGER.info("No UPnP devices discovered, aborting.") + return self.async_abort(reason="no_devices_found") + + discovery = self._discoveries[0] + return await self._async_create_entry_from_data(discovery) + + async def async_step_ssdp(self, discovery_info: Mapping): + """Handle a discovered UPnP/IGD device. + + This flow is triggered by the SSDP component. It will check if the + host is already configured and delegate to the import step if not. + """ + _LOGGER.debug("async_step_ssdp: discovery_info: %s", discovery_info) + + # Ensure not already configuring/configured. + udn = discovery_info[ssdp.ATTR_UPNP_UDN] + st = discovery_info[ssdp.ATTR_SSDP_ST] # pylint: disable=invalid-name + usn = f"{udn}::{st}" + await self.async_set_unique_id(usn) + self._abort_if_unique_id_configured() + + # Store discovery. + name = discovery_info.get("friendlyName", "") + discovery = { + DISCOVERY_UDN: udn, + DISCOVERY_ST: st, + DISCOVERY_NAME: name, + } + self._discoveries = [discovery] + + # Ensure user recognizable. + # pylint: disable=no-member # https://github.com/PyCQA/pylint/issues/3167 + self.context["title_placeholders"] = { + "name": name, + } + + return await self.async_step_ssdp_confirm() + + async def async_step_ssdp_confirm(self, user_input: Optional[Mapping] = None): + """Confirm integration via SSDP.""" + _LOGGER.debug("async_step_ssdp_confirm: user_input: %s", user_input) + if user_input is None: + return self.async_show_form(step_id="ssdp_confirm") + + discovery = self._discoveries[0] + return await self._async_create_entry_from_data(discovery) + + async def _async_create_entry_from_data(self, discovery: Mapping): + """Create an entry from own _data.""" + _LOGGER.debug("_async_create_entry_from_data: discovery: %s", discovery) + # Get name from device, if not found already. + if DISCOVERY_NAME not in discovery and DISCOVERY_LOCATION in discovery: + discovery[DISCOVERY_NAME] = await self._async_get_name_for_discovery( + discovery + ) + + title = discovery.get(DISCOVERY_NAME, "") + data = { + CONFIG_ENTRY_UDN: discovery[DISCOVERY_UDN], + CONFIG_ENTRY_ST: discovery[DISCOVERY_ST], + } + return self.async_create_entry(title=title, data=data) + + async def _async_get_name_for_discovery(self, discovery: Mapping): + """Get the name of the device from a discovery.""" + _LOGGER.debug("_async_get_name_for_discovery: discovery: %s", discovery) + device = await Device.async_create_device( + self.hass, discovery[DISCOVERY_LOCATION] + ) + return device.name diff --git a/homeassistant/components/upnp/const.py b/homeassistant/components/upnp/const.py index 80b5b718bbb622..1673fb4c1137c3 100644 --- a/homeassistant/components/upnp/const.py +++ b/homeassistant/components/upnp/const.py @@ -4,11 +4,7 @@ from homeassistant.const import TIME_SECONDS -CONF_ENABLE_PORT_MAPPING = "port_mapping" -CONF_ENABLE_SENSORS = "sensors" -CONF_HASS = "hass" CONF_LOCAL_IP = "local_ip" -CONF_PORTS = "ports" DOMAIN = "upnp" LOGGER = logging.getLogger(__package__) BYTES_RECEIVED = "bytes_received" @@ -20,3 +16,10 @@ DATA_RATE_PACKETS_PER_SECOND = f"{DATA_PACKETS}/{TIME_SECONDS}" KIBIBYTE = 1024 UPDATE_INTERVAL = timedelta(seconds=30) +DISCOVERY_NAME = "name" +DISCOVERY_LOCATION = "location" +DISCOVERY_ST = "st" +DISCOVERY_UDN = "udn" +DISCOVERY_USN = "usn" +CONFIG_ENTRY_UDN = "udn" +CONFIG_ENTRY_ST = "st" diff --git a/homeassistant/components/upnp/device.py b/homeassistant/components/upnp/device.py index 73ae06d9945039..05113b8f9f6a22 100644 --- a/homeassistant/components/upnp/device.py +++ b/homeassistant/components/upnp/device.py @@ -1,10 +1,9 @@ """Home Assistant representation of an UPnP/IGD.""" import asyncio from ipaddress import IPv4Address -from typing import Mapping +from typing import List, Mapping -import aiohttp -from async_upnp_client import UpnpError, UpnpFactory +from async_upnp_client import UpnpFactory from async_upnp_client.aiohttp import AiohttpSessionRequester from async_upnp_client.profiles.igd import IgdDevice @@ -16,6 +15,10 @@ BYTES_RECEIVED, BYTES_SENT, CONF_LOCAL_IP, + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, DOMAIN, LOGGER as _LOGGER, PACKETS_RECEIVED, @@ -33,7 +36,7 @@ def __init__(self, igd_device): self._mapped_ports = [] @classmethod - async def async_discover(cls, hass: HomeAssistantType): + async def async_discover(cls, hass: HomeAssistantType) -> List[Mapping]: """Discover UPnP/IGD devices.""" _LOGGER.debug("Discovering UPnP/IGD devices") local_ip = None @@ -47,9 +50,11 @@ async def async_discover(cls, hass: HomeAssistantType): # add extra info and store devices devices = [] for discovery_info in discovery_infos: - discovery_info["udn"] = discovery_info["_udn"] - discovery_info["ssdp_description"] = discovery_info["location"] - discovery_info["source"] = "async_upnp_client" + discovery_info[DISCOVERY_UDN] = discovery_info["_udn"] + discovery_info[DISCOVERY_ST] = discovery_info["st"] + discovery_info[DISCOVERY_LOCATION] = discovery_info["location"] + usn = f"{discovery_info[DISCOVERY_UDN]}::{discovery_info[DISCOVERY_ST]}" + discovery_info[DISCOVERY_USN] = usn _LOGGER.debug("Discovered device: %s", discovery_info) devices.append(discovery_info) @@ -57,7 +62,7 @@ async def async_discover(cls, hass: HomeAssistantType): return devices @classmethod - async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: str): + async def async_create_device(cls, hass: HomeAssistantType, ssdp_location: str): """Create UPnP/IGD device.""" # build async_upnp_client requester session = async_get_clientsession(hass) @@ -65,7 +70,7 @@ async def async_create_device(cls, hass: HomeAssistantType, ssdp_description: st # create async_upnp_client device factory = UpnpFactory(requester, disable_state_variable_validation=True) - upnp_device = await factory.async_create_device(ssdp_description) + upnp_device = await factory.async_create_device(ssdp_location) igd_device = IgdDevice(upnp_device, None) @@ -96,74 +101,15 @@ def device_type(self) -> str: """Get the device type.""" return self._igd_device.device_type + @property + def unique_id(self) -> str: + """Get the unique id.""" + return f"{self.udn}::{self.device_type}" + def __str__(self) -> str: """Get string representation.""" return f"IGD Device: {self.name}/{self.udn}" - async def async_add_port_mappings( - self, ports: Mapping[int, int], local_ip: str - ) -> None: - """Add port mappings.""" - if local_ip == "127.0.0.1": - _LOGGER.error("Could not create port mapping, our IP is 127.0.0.1") - - # determine local ip, ensure sane IP - local_ip = IPv4Address(local_ip) - - # create port mappings - for external_port, internal_port in ports.items(): - await self._async_add_port_mapping(external_port, local_ip, internal_port) - self._mapped_ports.append(external_port) - - async def _async_add_port_mapping( - self, external_port: int, local_ip: str, internal_port: int - ) -> None: - """Add a port mapping.""" - # create port mapping - _LOGGER.info( - "Creating port mapping %s:%s:%s (TCP)", - external_port, - local_ip, - internal_port, - ) - try: - await self._igd_device.async_add_port_mapping( - remote_host=None, - external_port=external_port, - protocol="TCP", - internal_port=internal_port, - internal_client=local_ip, - enabled=True, - description="Home Assistant", - lease_duration=None, - ) - - self._mapped_ports.append(external_port) - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - _LOGGER.error( - "Could not add port mapping: %s:%s:%s", - external_port, - local_ip, - internal_port, - ) - - async def async_delete_port_mappings(self) -> None: - """Remove port mappings.""" - for port in self._mapped_ports: - await self._async_delete_port_mapping(port) - - async def _async_delete_port_mapping(self, external_port: int) -> None: - """Remove a port mapping.""" - _LOGGER.info("Deleting port mapping %s (TCP)", external_port) - try: - await self._igd_device.async_delete_port_mapping( - remote_host=None, external_port=external_port, protocol="TCP" - ) - - self._mapped_ports.remove(external_port) - except (asyncio.TimeoutError, aiohttp.ClientError, UpnpError): - _LOGGER.error("Could not delete port mapping") - async def async_get_traffic_data(self) -> Mapping[str, any]: """ Get all traffic data in one go. diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index 2f6e5de5884e61..e3b30cec9a487a 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -5,5 +5,13 @@ "documentation": "https://www.home-assistant.io/integrations/upnp", "requirements": ["async-upnp-client==0.14.13"], "dependencies": [], - "codeowners": ["@StevenLooman"] + "codeowners": ["@StevenLooman"], + "ssdp": [ + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + }, + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" + } + ] } diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 5ad90b2c0cb92a..9f2f6978341212 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,25 +1,22 @@ { "config": { + "flow_title": "UPnP/IGD: {name}", "step": { - "confirm": { - "description": "Do you want to set up UPnP/IGD?" + "init": { + }, + "ssdp_confirm": { + "description": "Do you want to set up this UPnP/IGD device?" }, "user": { - "title": "Configuration options", "data": { - "enable_port_mapping": "Enable port mapping for Home Assistant", - "enable_sensors": "Add traffic sensors", - "igd": "UPnP/IGD" + "usn": "Device" } } }, "abort": { "already_configured": "UPnP/IGD is already configured", - "incomplete_device": "Ignoring incomplete UPnP device", "no_devices_discovered": "No UPnP/IGDs discovered", - "no_devices_found": "No UPnP/IGD devices found on the network.", - "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", - "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." + "no_devices_found": "No UPnP/IGD devices found on the network." } } } diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index 6da89c0e3d601b..124ccfdd17d4ac 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -1,28 +1,21 @@ { "config": { + "flow_title": "UPnP/IGD: {name}", "abort": { "already_configured": "UPnP/IGD is already configured", - "incomplete_device": "Ignoring incomplete UPnP device", "no_devices_discovered": "No UPnP/IGDs discovered", - "no_devices_found": "No UPnP/IGD devices found on the network.", - "no_sensors_or_port_mapping": "Enable at least sensors or port mapping", - "single_instance_allowed": "Only a single configuration of UPnP/IGD is necessary." + "no_devices_found": "No UPnP/IGD devices found on the network." }, "step": { - "confirm": { - "description": "Do you want to set up UPnP/IGD?", - "title": "UPnP/IGD" - }, "init": { - "title": "UPnP/IGD" + }, + "ssdp_confirm": { + "description": "Do you want to set up this UPnP/IGD device?" }, "user": { "data": { - "enable_port_mapping": "Enable port mapping for Home Assistant", - "enable_sensors": "Add traffic sensors", - "igd": "UPnP/IGD" - }, - "title": "Configuration options" + "usn": "Device" + } } } } diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index 3004ab40ee7f5e..75f052773fd420 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -18,7 +18,7 @@ }, "step": { "confirm": { - "description": "\u00d8nsker du \u00e5 konfigurere UPnP / IGD?", + "description": "\u00d8nsker du \u00e5 sette opp UPnP / IGD?", "title": "UPnP / IGD" }, "init": { diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index c891c698cf6f61..c5b6e9a292c24b 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -95,7 +95,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= platform.async_register_entity_service( SERVICE_CALIBRATE_METER, - {vol.Required(ATTR_VALUE): vol.Coerce(float)}, + {vol.Required(ATTR_VALUE): vol.Coerce(Decimal)}, "async_calibrate", ) @@ -222,7 +222,7 @@ async def async_reset_meter(self, entity_id): async def async_calibrate(self, value): """Calibrate the Utility Meter with a given value.""" _LOGGER.debug("Calibrate %s = %s", self._name, value) - self._state = Decimal(value) + self._state = value self.async_write_ha_state() async def async_added_to_hass(self): diff --git a/homeassistant/components/uvc/camera.py b/homeassistant/components/uvc/camera.py index 05937cc3ee91ff..5878023bf2ea75 100644 --- a/homeassistant/components/uvc/camera.py +++ b/homeassistant/components/uvc/camera.py @@ -192,11 +192,7 @@ def _get_image(retry=True): def set_motion_detection(self, mode): """Set motion detection on or off.""" - - if mode is True: - set_mode = "motion" - else: - set_mode = "none" + set_mode = "motion" if mode is True else "none" try: self._nvr.set_recordmode(self._uuid, set_mode) @@ -215,9 +211,7 @@ def disable_motion_detection(self): async def stream_source(self): """Return the source of the stream.""" - caminfo = self._nvr.get_camera(self._uuid) - channels = caminfo["channels"] - for channel in channels: + for channel in self._caminfo["channels"]: if channel["isRtspEnabled"]: return channel["rtspUris"][0] diff --git a/homeassistant/components/vacuum/translations/es-419.json b/homeassistant/components/vacuum/translations/es-419.json index 39ed128de9d3f3..59126d08a426aa 100644 --- a/homeassistant/components/vacuum/translations/es-419.json +++ b/homeassistant/components/vacuum/translations/es-419.json @@ -1,4 +1,18 @@ { + "device_automation": { + "action_type": { + "clean": "Deje que {entity_name} limpie", + "dock": "Dejar que {entity_name} regrese al dock" + }, + "condition_type": { + "is_cleaning": "{entity_name} est\u00e1 limpiando", + "is_docked": "{entity_name} est\u00e1 acoplado" + }, + "trigger_type": { + "cleaning": "{entity_name} comenz\u00f3 a limpiar", + "docked": "{entity_name} acoplado" + } + }, "state": { "_": { "cleaning": "Limpiando", diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 9b2d44fd94beda..1e1538420b5b46 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -25,7 +25,7 @@ from homeassistant.util.dt import utc_from_timestamp from .common import ControllerData, get_configured_platforms -from .config_flow import new_options +from .config_flow import fix_device_id_list, new_options from .const import ( ATTR_CURRENT_ENERGY_KWH, ATTR_CURRENT_POWER_W, @@ -81,9 +81,18 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b ), ) + saved_light_ids = config_entry.options.get(CONF_LIGHTS, []) + saved_exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) + base_url = config_entry.data[CONF_CONTROLLER] - light_ids = config_entry.options.get(CONF_LIGHTS, []) - exclude_ids = config_entry.options.get(CONF_EXCLUDE, []) + light_ids = fix_device_id_list(saved_light_ids) + exclude_ids = fix_device_id_list(saved_exclude_ids) + + # If the ids were corrected. Update the config entry. + if light_ids != saved_light_ids or exclude_ids != saved_exclude_ids: + hass.config_entries.async_update_entry( + entry=config_entry, options=new_options(light_ids, exclude_ids) + ) # Initialize the Vera controller. controller = veraApi.VeraController(base_url) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index 3d2b30f1079fc7..cac17951cc159e 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Vera.""" import logging import re -from typing import List, cast +from typing import Any, List import pyvera as pv from requests.exceptions import RequestException @@ -17,20 +17,22 @@ _LOGGER = logging.getLogger(__name__) -def str_to_int_list(data: str) -> List[str]: - """Convert a string to an int list.""" - if isinstance(str, list): - return cast(List[str], data) +def fix_device_id_list(data: List[Any]) -> List[int]: + """Fix the id list by converting it to a supported int list.""" + return str_to_int_list(list_to_str(data)) + - return [s for s in LIST_REGEX.split(data) if len(s) > 0] +def str_to_int_list(data: str) -> List[int]: + """Convert a string to an int list.""" + return [int(s) for s in LIST_REGEX.split(data) if len(s) > 0] -def int_list_to_str(data: List[str]) -> str: +def list_to_str(data: List[Any]) -> str: """Convert an int list to a string.""" return " ".join([str(i) for i in data]) -def new_options(lights: List[str], exclude: List[str]) -> dict: +def new_options(lights: List[int], exclude: List[int]) -> dict: """Create a standard options object.""" return {CONF_LIGHTS: lights, CONF_EXCLUDE: exclude} @@ -40,10 +42,10 @@ def options_schema(options: dict = None) -> dict: options = options or {} return { vol.Optional( - CONF_LIGHTS, default=int_list_to_str(options.get(CONF_LIGHTS, [])), + CONF_LIGHTS, default=list_to_str(options.get(CONF_LIGHTS, [])), ): str, vol.Optional( - CONF_EXCLUDE, default=int_list_to_str(options.get(CONF_EXCLUDE, [])), + CONF_EXCLUDE, default=list_to_str(options.get(CONF_EXCLUDE, [])), ): str, } diff --git a/homeassistant/components/vera/translations/es-419.json b/homeassistant/components/vera/translations/es-419.json new file mode 100644 index 00000000000000..243050b15449d2 --- /dev/null +++ b/homeassistant/components/vera/translations/es-419.json @@ -0,0 +1,31 @@ +{ + "config": { + "abort": { + "already_configured": "Un controlador ya est\u00e1 configurado.", + "cannot_connect": "No se pudo conectar al controlador con la URL {base_url}" + }, + "step": { + "user": { + "data": { + "exclude": "Identificadores de dispositivo de Vera para excluir de Home Assistant.", + "lights": "Vera cambia los identificadores del dispositivo para tratarlos como luces en Home Assistant.", + "vera_controller_url": "URL del controlador" + }, + "description": "Proporcione una URL del controlador Vera a continuaci\u00f3n. Deber\u00eda verse as\u00ed: http://192.168.1.161:3480.", + "title": "Configurar el controlador Vera" + } + } + }, + "options": { + "step": { + "init": { + "data": { + "exclude": "Identificadores de dispositivo de Vera para excluir de Home Assistant.", + "lights": "Vera cambia los identificadores del dispositivo para tratarlos 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 reiniciar el servidor del asistente de inicio. Para borrar valores, proporcione un espacio.", + "title": "Opciones de controlador Vera" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/vilfo/translations/es-419.json b/homeassistant/components/vilfo/translations/es-419.json index 524b7e7b934f38..9c62c32723591f 100644 --- a/homeassistant/components/vilfo/translations/es-419.json +++ b/homeassistant/components/vilfo/translations/es-419.json @@ -1,7 +1,20 @@ { "config": { + "abort": { + "already_configured": "Este enrutador Vilfo ya est\u00e1 configurado." + }, + "error": { + "cannot_connect": "No se pudo conectar. Verifique la informaci\u00f3n que proporcion\u00f3 e intente nuevamente.", + "invalid_auth": "Autenticaci\u00f3n inv\u00e1lida Verifique el token de acceso e intente nuevamente.", + "unknown": "Se produjo un error inesperado al configurar la integraci\u00f3n." + }, "step": { "user": { + "data": { + "access_token": "Token de acceso para la API del enrutador Vilfo", + "host": "Nombre de host o IP del enrutador" + }, + "description": "Configure la integraci\u00f3n del enrutador Vilfo. Necesita su nombre de host/IP de Vilfo Router y un token de acceso API. Para obtener informaci\u00f3n adicional sobre esta integraci\u00f3n y c\u00f3mo obtener esos detalles, visite: https://www.home-assistant.io/integrations/vilfo", "title": "Conectar con el Router Vilfo" } } diff --git a/homeassistant/components/vilfo/translations/no.json b/homeassistant/components/vilfo/translations/no.json index 36c6e79989ba81..d5b1ffd7296573 100644 --- a/homeassistant/components/vilfo/translations/no.json +++ b/homeassistant/components/vilfo/translations/no.json @@ -14,7 +14,7 @@ "access_token": "Tilgangstoken for Vilfo Router API", "host": "Ruter vertsnavn eller IP" }, - "description": "Konfigurer Vilfo Router-integreringen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. Hvis du vil ha mer informasjon om denne integreringen og hvordan du f\u00e5r disse detaljene, kan du g\u00e5 til: https://www.home-assistant.io/integrations/vilfo", + "description": "Sett opp Vilfo Router-integreringen. Du trenger ditt Vilfo Router vertsnavn/IP og et API-tilgangstoken. Hvis du vil ha mer informasjon om denne integreringen og hvordan du f\u00e5r disse detaljene, kan du g\u00e5 til: https://www.home-assistant.io/integrations/vilfo", "title": "Koble til Vilfo Ruteren" } } diff --git a/homeassistant/components/vizio/translations/es-419.json b/homeassistant/components/vizio/translations/es-419.json index b8dc207c47b4c9..d60f839d653b25 100644 --- a/homeassistant/components/vizio/translations/es-419.json +++ b/homeassistant/components/vizio/translations/es-419.json @@ -1,11 +1,39 @@ { "config": { + "abort": { + "already_setup": "Esta entrada ya se ha configurado.", + "updated_entry": "Esta entrada ya se configur\u00f3, pero el nombre, las aplicaciones o las opciones definidas en la configuraci\u00f3n no coinciden con la configuraci\u00f3n importada anteriormente, por lo que la entrada de configuraci\u00f3n se actualiz\u00f3 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 complet\u00f3 son precisos \n antes de intentar volver a enviar.", + "complete_pairing failed": "No se puede completar el emparejamiento. Aseg\u00farese de que el PIN que proporcion\u00f3 sea correcto y que el televisor siga encendido y conectado a la red antes de volver a enviarlo.", + "host_exists": "Dispositivo VIZIO con el host especificado ya configurado.", + "name_exists": "Dispositivo VIZIO con el nombre especificado ya configurado." + }, "step": { + "pair_tv": { + "data": { + "pin": "PIN" + }, + "description": "Su televisi\u00f3n debe mostrar un c\u00f3digo. Ingrese ese c\u00f3digo en el formulario y luego contin\u00fae con el siguiente paso para completar el emparejamiento.", + "title": "Proceso de emparejamiento completo" + }, + "pairing_complete": { + "description": "Su dispositivo VIZIO SmartCast ahora est\u00e1 conectado a Home Assistant.", + "title": "Emparejamiento completo" + }, + "pairing_complete_import": { + "description": "Su VIZIO SmartCast TV ahora est\u00e1 conectado a Home Assistant. \n\nSu token de acceso es '** {access_token} **'.", + "title": "Emparejamiento completo" + }, "user": { "data": { + "access_token": "Token de acceso", "device_class": "Tipo de dispositivo", + "host": ":", "name": "Nombre" }, + "description": "Solo se necesita un token de acceso para televisores. Si est\u00e1 configurando un televisor y a\u00fan no tiene un token de acceso, d\u00e9jelo en blanco para realizar un proceso de emparejamiento.", "title": "Configurar el dispositivo VIZIO SmartCast" } } @@ -15,8 +43,10 @@ "init": { "data": { "apps_to_include_or_exclude": "Aplicaciones para incluir o excluir", - "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?" + "include_or_exclude": "\u00bfIncluir o excluir aplicaciones?", + "volume_step": "Tama\u00f1o de incremento de volumen" }, + "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.", "title": "Actualizar las opciones de VIZIO SmartCast" } } diff --git a/homeassistant/components/vizio/translations/no.json b/homeassistant/components/vizio/translations/no.json index 3479a013084faa..ddeb75c156ed0b 100644 --- a/homeassistant/components/vizio/translations/no.json +++ b/homeassistant/components/vizio/translations/no.json @@ -19,7 +19,7 @@ "title": "Fullf\u00f8r Sammenkoblings Prosessen" }, "pairing_complete": { - "description": "Din VIZIO SmartCast enheten er n\u00e5 koblet til Hjemme-Assistent.", + "description": "Din VIZIO SmartCast enheten er n\u00e5 koblet til Home Assistant.", "title": "Sammenkoblingen Er Fullf\u00f8rt" }, "pairing_complete_import": { diff --git a/homeassistant/components/websocket_api/manifest.json b/homeassistant/components/websocket_api/manifest.json index 76e2742b996689..66dd76af769f17 100644 --- a/homeassistant/components/websocket_api/manifest.json +++ b/homeassistant/components/websocket_api/manifest.json @@ -1,6 +1,6 @@ { "domain": "websocket_api", - "name": "Home Asssitant WebSocket API", + "name": "Home Assistant WebSocket API", "documentation": "https://www.home-assistant.io/integrations/websocket_api", "dependencies": ["http"], "codeowners": ["@home-assistant/core"], diff --git a/homeassistant/components/withings/translations/es-419.json b/homeassistant/components/withings/translations/es-419.json index 4fc2dec0ac2081..d06470b213f735 100644 --- a/homeassistant/components/withings/translations/es-419.json +++ b/homeassistant/components/withings/translations/es-419.json @@ -1,7 +1,23 @@ { "config": { + "abort": { + "authorize_url_timeout": "Tiempo de espera agotado para generar la URL de autorizaci\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." + }, + "step": { + "pick_implementation": { + "title": "Seleccione el m\u00e9todo de autenticaci\u00f3n" + }, + "profile": { + "data": { + "profile": "Perfil" + }, + "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 del usuario." + } } } } \ No newline at end of file diff --git a/homeassistant/components/wled/translations/es-419.json b/homeassistant/components/wled/translations/es-419.json index a9107341e37962..b5973638373bb4 100644 --- a/homeassistant/components/wled/translations/es-419.json +++ b/homeassistant/components/wled/translations/es-419.json @@ -10,6 +10,9 @@ "flow_title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "step": { "user": { + "data": { + "host": "Host o direcci\u00f3n IP" + }, "description": "Wykryto urz\u0105dzenie [%key:component::wled::title%]", "title": "Wykryto urz\u0105dzenie [%key:component::wled::title%]" }, diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json index e0c165b352cab6..fd3b5f94cbe6ba 100644 --- a/homeassistant/components/wled/translations/no.json +++ b/homeassistant/components/wled/translations/no.json @@ -7,13 +7,13 @@ "error": { "connection_error": "Kunne ikke koble til WLED-enheten." }, - "flow_title": "", + "flow_title": "WLED: {name}", "step": { "user": { "data": { "host": "Vert eller IP-adresse" }, - "description": "Konfigurer WLED til \u00e5 integreres med Home Assistant.", + "description": "Sett opp WLED til \u00e5 integreres med Home Assistant.", "title": "Linken din WLED" }, "zeroconf_confirm": { diff --git a/homeassistant/components/wwlln/translations/es-419.json b/homeassistant/components/wwlln/translations/es-419.json index 1732a5b43bc242..11ae8c64359013 100644 --- a/homeassistant/components/wwlln/translations/es-419.json +++ b/homeassistant/components/wwlln/translations/es-419.json @@ -1,5 +1,8 @@ { "config": { + "abort": { + "already_configured": "Esta ubicaci\u00f3n ya est\u00e1 registrada." + }, "step": { "user": { "data": { diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index 1562bbb652603d..0040e6fdb75513 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -19,11 +19,11 @@ } }, "error": { - "connect_error": "Failed to connect, please try again", + "connect_error": "[%key:common::config_flow::error::cannot_connect%]", "no_device_selected": "No device selected, please select one device." }, "abort": { - "already_configured": "Device is already configured" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" } } } diff --git a/homeassistant/components/xiaomi_miio/translations/es-419.json b/homeassistant/components/xiaomi_miio/translations/es-419.json new file mode 100644 index 00000000000000..4178a80a34766a --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/es-419.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "El dispositivo ya est\u00e1 configurado" + }, + "error": { + "connect_error": "No se pudo conectar, intente nuevamente", + "no_device_selected": "Ning\u00fan dispositivo seleccionado, seleccione un dispositivo." + }, + "step": { + "gateway": { + "data": { + "host": "Direcci\u00f3n IP", + "name": "Nombre de la puerta de enlace", + "token": "Token API" + }, + "description": "Necesitar\u00e1 el token API, consulte https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token para obtener instrucciones.", + "title": "Conectarse a una puerta de enlace Xiaomi" + }, + "user": { + "data": { + "gateway": "Conectarse a una puerta de enlace Xiaomi" + }, + "description": "Seleccione a qu\u00e9 dispositivo desea conectarse.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/fr.json b/homeassistant/components/xiaomi_miio/translations/fr.json new file mode 100644 index 00000000000000..c603eef4cace1e --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/fr.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "L'appareil est d\u00e9j\u00e0 configur\u00e9" + }, + "error": { + "connect_error": "Impossible de se connecter, veuillez r\u00e9essayer", + "no_device_selected": "Aucun appareil s\u00e9lectionn\u00e9, veuillez s\u00e9lectionner un appareil." + }, + "step": { + "gateway": { + "data": { + "host": "adresse IP", + "name": "Nom de la passerelle", + "token": "Jeton d'API" + }, + "description": "Vous aurez besoin du jeton API, voir https://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token pour les instructions.", + "title": "Se connecter \u00e0 la passerelle Xiaomi" + }, + "user": { + "data": { + "gateway": "Se connecter \u00e0 la passerelle Xiaomi" + }, + "description": "S\u00e9lectionnez \u00e0 quel appareil vous souhaitez vous connecter.", + "title": "Xiaomi Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hans.json b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json new file mode 100644 index 00000000000000..254256f9ac98b2 --- /dev/null +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hans.json @@ -0,0 +1,29 @@ +{ + "config": { + "abort": { + "already_configured": "\u8bbe\u5907\u5df2\u7ecf\u914d\u7f6e\u8fc7\u4e86" + }, + "error": { + "connect_error": "\u8fde\u63a5\u5931\u8d25\uff0c\u8bf7\u91cd\u8bd5", + "no_device_selected": "\u672a\u9009\u62e9\u8bbe\u5907\uff0c\u8bf7\u9009\u62e9\u4e00\u4e2a\u8bbe\u5907\u3002" + }, + "step": { + "gateway": { + "data": { + "host": "IP \u5730\u5740", + "name": "\u7f51\u5173\u540d\u79f0", + "token": "API Token" + }, + "description": "\u60a8\u9700\u8981\u83b7\u53d6 API Token\u3002\u5982\u9700\u5e2e\u52a9\uff0c\u8bf7\u53c2\u9605\u4ee5\u4e0b\u94fe\u63a5\uff1ahttps://www.home-assistant.io/integrations/vacuum.xiaomi_miio/#retrieving-the-access-token", + "title": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" + }, + "user": { + "data": { + "gateway": "\u8fde\u63a5\u5230\u5c0f\u7c73\u7f51\u5173" + }, + "description": "\u8bf7\u9009\u62e9\u8981\u8fde\u63a5\u7684\u8bbe\u5907\u3002", + "title": "\u5c0f\u7c73 Miio" + } + } + } +} \ No newline at end of file diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 241cf4432441bf..655ff4dc032c44 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.25.1"], + "requirements": ["zeroconf==0.26.0"], "dependencies": ["api"], "codeowners": ["@robbiet480", "@Kane610"], "quality_scale": "internal" diff --git a/homeassistant/components/zha/api.py b/homeassistant/components/zha/api.py index 232a5666300640..1ba9ada5413030 100644 --- a/homeassistant/components/zha/api.py +++ b/homeassistant/components/zha/api.py @@ -53,6 +53,7 @@ WARNING_DEVICE_STROBE_HIGH, WARNING_DEVICE_STROBE_YES, ) +from .core.group import GroupMember from .core.helpers import async_is_bindable_target, get_matched_clusters _LOGGER = logging.getLogger(__name__) @@ -209,7 +210,7 @@ async def websocket_get_devices(hass, connection, msg): """Get ZHA devices.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - devices = [device.async_get_info() for device in zha_gateway.devices.values()] + devices = [device.zha_device_info for device in zha_gateway.devices.values()] connection.send_result(msg[ID], devices) @@ -221,13 +222,35 @@ async def websocket_get_groupable_devices(hass, connection, msg): """Get ZHA devices that can be grouped.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - devices = [ - device.async_get_info() - for device in zha_gateway.devices.values() - if device.is_groupable or device.is_coordinator - ] + devices = [device for device in zha_gateway.devices.values() if device.is_groupable] + groupable_devices = [] + + for device in devices: + entity_refs = zha_gateway.device_registry.get(device.ieee) + for ep_id in device.async_get_groupable_endpoints(): + groupable_devices.append( + { + "endpoint_id": ep_id, + "entities": [ + { + "name": zha_gateway.ha_entity_registry.async_get( + entity_ref.reference_id + ).name, + "original_name": zha_gateway.ha_entity_registry.async_get( + entity_ref.reference_id + ).original_name, + } + for entity_ref in entity_refs + if list(entity_ref.cluster_channels.values())[ + 0 + ].cluster.endpoint.endpoint_id + == ep_id + ], + "device": device.zha_device_info, + } + ) - connection.send_result(msg[ID], devices) + connection.send_result(msg[ID], groupable_devices) @websocket_api.require_admin @@ -236,7 +259,7 @@ async def websocket_get_groupable_devices(hass, connection, msg): async def websocket_get_groups(hass, connection, msg): """Get ZHA groups.""" zha_gateway = hass.data[DATA_ZHA][DATA_ZHA_GATEWAY] - groups = [group.async_get_info() for group in zha_gateway.groups.values()] + groups = [group.group_info for group in zha_gateway.groups.values()] connection.send_result(msg[ID], groups) @@ -251,7 +274,7 @@ async def websocket_get_device(hass, connection, msg): ieee = msg[ATTR_IEEE] device = None if ieee in zha_gateway.devices: - device = zha_gateway.devices[ieee].async_get_info() + device = zha_gateway.devices[ieee].zha_device_info if not device: connection.send_message( websocket_api.error_message( @@ -274,7 +297,7 @@ async def websocket_get_group(hass, connection, msg): group = None if group_id in zha_gateway.groups: - group = zha_gateway.groups.get(group_id).async_get_info() + group = zha_gateway.groups.get(group_id).group_info if not group: connection.send_message( websocket_api.error_message( @@ -285,13 +308,27 @@ async def websocket_get_group(hass, connection, msg): connection.send_result(msg[ID], group) +def cv_group_member(value: Any) -> GroupMember: + """Validate and transform a group member.""" + if not isinstance(value, Mapping): + raise vol.Invalid("Not a group member") + try: + group_member = GroupMember( + ieee=EUI64.convert(value["ieee"]), endpoint_id=value["endpoint_id"] + ) + except KeyError: + raise vol.Invalid("Not a group member") + + return group_member + + @websocket_api.require_admin @websocket_api.async_response @websocket_api.websocket_command( { vol.Required(TYPE): "zha/group/add", vol.Required(GROUP_NAME): cv.string, - vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + vol.Optional(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) async def websocket_add_group(hass, connection, msg): @@ -300,7 +337,7 @@ async def websocket_add_group(hass, connection, msg): group_name = msg[GROUP_NAME] members = msg.get(ATTR_MEMBERS) group = await zha_gateway.async_create_zigpy_group(group_name, members) - connection.send_result(msg[ID], group.async_get_info()) + connection.send_result(msg[ID], group.group_info) @websocket_api.require_admin @@ -323,7 +360,7 @@ async def websocket_remove_groups(hass, connection, msg): await asyncio.gather(*tasks) else: await zha_gateway.async_remove_zigpy_group(group_ids[0]) - ret_groups = [group.async_get_info() for group in zha_gateway.groups.values()] + ret_groups = [group.group_info for group in zha_gateway.groups.values()] connection.send_result(msg[ID], ret_groups) @@ -333,7 +370,7 @@ async def websocket_remove_groups(hass, connection, msg): { vol.Required(TYPE): "zha/group/members/add", vol.Required(GROUP_ID): cv.positive_int, - vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) async def websocket_add_group_members(hass, connection, msg): @@ -353,7 +390,7 @@ async def websocket_add_group_members(hass, connection, msg): ) ) return - ret_group = zha_group.async_get_info() + ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @@ -363,7 +400,7 @@ async def websocket_add_group_members(hass, connection, msg): { vol.Required(TYPE): "zha/group/members/remove", vol.Required(GROUP_ID): cv.positive_int, - vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [EUI64.convert]), + vol.Required(ATTR_MEMBERS): vol.All(cv.ensure_list, [cv_group_member]), } ) async def websocket_remove_group_members(hass, connection, msg): @@ -383,7 +420,7 @@ async def websocket_remove_group_members(hass, connection, msg): ) ) return - ret_group = zha_group.async_get_info() + ret_group = zha_group.group_info connection.send_result(msg[ID], ret_group) @@ -608,7 +645,7 @@ async def websocket_get_bindable_devices(hass, connection, msg): source_device = zha_gateway.get_device(source_ieee) devices = [ - device.async_get_info() + device.zha_device_info for device in zha_gateway.devices.values() if async_is_bindable_target(source_device, device) ] diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index b4947d121e4d0e..fcbf518a9db0e5 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -235,13 +235,9 @@ def is_end_device(self): @property def is_groupable(self): """Return true if this device has a group cluster.""" - if not self.available: - return False - clusters = self.async_get_clusters() - for cluster_map in clusters.values(): - for clusters in cluster_map.values(): - if Groups.cluster_id in clusters: - return True + return self.is_coordinator or ( + self.available and self.async_get_groupable_endpoints() + ) @property def skip_configuration(self): @@ -411,8 +407,8 @@ def async_update_last_seen(self, 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): + @property + def zha_device_info(self): """Get ZHA device information.""" device_info = {} device_info.update(self.device_info) @@ -442,6 +438,15 @@ def async_get_clusters(self): if ep_id != 0 } + @callback + def async_get_groupable_endpoints(self): + """Get device endpoints that have a group 'in' cluster.""" + return [ + ep_id + for (ep_id, clusters) in self.async_get_clusters().items() + if Groups.cluster_id in clusters[CLUSTER_TYPE_IN] + ] + @callback def async_get_std_clusters(self): """Get ZHA and ZLL clusters for this device.""" @@ -557,7 +562,15 @@ async def issue_cluster_command( async def async_add_to_group(self, group_id): """Add this device to the provided zigbee group.""" - await self._zigpy_device.add_to_group(group_id) + try: + await self._zigpy_device.add_to_group(group_id) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to add device '%s' to group: 0x%04x ex: %s", + self._zigpy_device.ieee, + group_id, + str(ex), + ) async def async_remove_from_group(self, group_id): """Remove this device from the provided zigbee group.""" @@ -571,6 +584,34 @@ async def async_remove_from_group(self, group_id): str(ex), ) + async def async_add_endpoint_to_group(self, endpoint_id, group_id): + """Add the device endpoint to the provided zigbee group.""" + try: + await self._zigpy_device.endpoints[int(endpoint_id)].add_to_group(group_id) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to add endpoint: %s for device: '%s' to group: 0x%04x ex: %s", + endpoint_id, + self._zigpy_device.ieee, + group_id, + str(ex), + ) + + async def async_remove_endpoint_from_group(self, endpoint_id, group_id): + """Remove the device endpoint from the provided zigbee group.""" + try: + await self._zigpy_device.endpoints[int(endpoint_id)].remove_from_group( + group_id + ) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s", + endpoint_id, + self._zigpy_device.ieee, + group_id, + str(ex), + ) + async def async_bind_to_group(self, group_id, cluster_bindings): """Directly bind this device to a group for the given clusters.""" await self._async_group_binding_operation( diff --git a/homeassistant/components/zha/core/discovery.py b/homeassistant/components/zha/core/discovery.py index 4540c9158de603..f72ac2161ec154 100644 --- a/homeassistant/components/zha/core/discovery.py +++ b/homeassistant/components/zha/core/discovery.py @@ -235,21 +235,13 @@ def determine_entity_domains( ) -> 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 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: + for member in group.members: + if member.device.is_coordinator: continue entities = async_entries_for_device( - zha_gateway.ha_entity_registry, device.device_id + zha_gateway.ha_entity_registry, member.device.device_id ) all_domain_occurrences.extend( [ diff --git a/homeassistant/components/zha/core/gateway.py b/homeassistant/components/zha/core/gateway.py index e97e2185dc5fc1..b8efdf873b133f 100644 --- a/homeassistant/components/zha/core/gateway.py +++ b/homeassistant/components/zha/core/gateway.py @@ -78,11 +78,11 @@ ZHA_GW_RADIO_DESCRIPTION, ) from .device import DeviceStatus, ZHADevice -from .group import ZHAGroup +from .group import GroupMember, ZHAGroup from .patches import apply_application_controller_patch from .registries import GROUP_ENTITY_DOMAINS, RADIO_TYPES from .store import async_get_registry -from .typing import ZhaDeviceType, ZhaGroupType, ZigpyEndpointType, ZigpyGroupType +from .typing import ZhaGroupType, ZigpyEndpointType, ZigpyGroupType _LOGGER = logging.getLogger(__name__) @@ -308,7 +308,7 @@ def _send_group_gateway_message( ZHA_GW_MSG, { ATTR_TYPE: gateway_message_type, - ZHA_GW_MSG_GROUP_INFO: zha_group.async_get_info(), + ZHA_GW_MSG_GROUP_INFO: zha_group.group_info, }, ) @@ -327,7 +327,7 @@ def device_removed(self, device): zha_device = self._devices.pop(device.ieee, None) entity_refs = self._device_registry.pop(device.ieee, None) if zha_device is not None: - device_info = zha_device.async_get_info() + device_info = zha_device.zha_device_info zha_device.async_cleanup_handles() async_dispatcher_send( self._hass, "{}_{}".format(SIGNAL_REMOVE, str(zha_device.ieee)) @@ -542,7 +542,7 @@ async def async_device_initialized(self, device: zha_typing.ZigpyDeviceType): ) await self._async_device_joined(zha_device) - device_info = zha_device.async_get_info() + device_info = zha_device.zha_device_info async_dispatcher_send( self._hass, @@ -571,11 +571,11 @@ async def _async_device_rejoined(self, zha_device): zha_device.update_available(True) async def async_create_zigpy_group( - self, name: str, members: List[ZhaDeviceType] + self, name: str, members: List[GroupMember] ) -> ZhaGroupType: """Create a new Zigpy Zigbee group.""" - # we start with one to fill any gaps from a user removing existing groups - group_id = 1 + # we start with two to fill any gaps from a user removing existing groups + group_id = 2 while group_id in self.groups: group_id += 1 @@ -584,14 +584,19 @@ async def async_create_zigpy_group( self.application_controller.groups.add_group(group_id, name) if members is not None: tasks = [] - for ieee in members: + for member in members: _LOGGER.debug( - "Adding member with IEEE: %s to group: %s:0x%04x", - ieee, + "Adding member with IEEE: %s and endpoint id: %s to group: %s:0x%04x", + member.ieee, + member.endpoint_id, name, group_id, ) - tasks.append(self.devices[ieee].async_add_to_group(group_id)) + tasks.append( + self.devices[member.ieee].async_add_endpoint_to_group( + member.endpoint_id, group_id + ) + ) await asyncio.gather(*tasks) return self.groups.get(group_id) @@ -604,7 +609,7 @@ async def async_remove_zigpy_group(self, group_id: int) -> None: if group and group.members: tasks = [] for member in group.members: - tasks.append(member.async_remove_from_group(group_id)) + tasks.append(member.async_remove_from_group()) if tasks: await asyncio.gather(*tasks) self.application_controller.groups.pop(group_id) diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 4fc86012d1afe7..2961f335989e54 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -1,19 +1,110 @@ """Group for Zigbee Home Automation.""" import asyncio +import collections import logging from typing import Any, Dict, List -from zigpy.types.named import EUI64 +import zigpy.exceptions -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 +from .typing import ( + ZhaDeviceType, + ZhaGatewayType, + ZhaGroupType, + ZigpyEndpointType, + ZigpyGroupType, +) _LOGGER = logging.getLogger(__name__) +GroupMember = collections.namedtuple("GroupMember", "ieee endpoint_id") +GroupEntityReference = collections.namedtuple( + "GroupEntityReference", "name original_name entity_id" +) + + +class ZHAGroupMember(LogMixin): + """Composite object that represents a device endpoint in a Zigbee group.""" + + def __init__( + self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int + ): + """Initialize the group member.""" + self._zha_group: ZhaGroupType = zha_group + self._zha_device: ZhaDeviceType = zha_device + self._endpoint_id: int = endpoint_id + + @property + def group(self) -> ZhaGroupType: + """Return the group this member belongs to.""" + return self._zha_group + + @property + def endpoint_id(self) -> int: + """Return the endpoint id for this group member.""" + return self._endpoint_id + + @property + def endpoint(self) -> ZigpyEndpointType: + """Return the endpoint for this group member.""" + return self._zha_device.device.endpoints.get(self.endpoint_id) + + @property + def device(self) -> ZhaDeviceType: + """Return the zha device for this group member.""" + return self._zha_device + + @property + def member_info(self) -> Dict[str, Any]: + """Get ZHA group info.""" + member_info: Dict[str, Any] = {} + member_info["endpoint_id"] = self.endpoint_id + member_info["device"] = self.device.zha_device_info + member_info["entities"] = self.associated_entities + return member_info + + @property + def associated_entities(self) -> List[GroupEntityReference]: + """Return the list of entities that were derived from this endpoint.""" + ha_entity_registry = self.device.gateway.ha_entity_registry + zha_device_registry = self.device.gateway.device_registry + return [ + GroupEntityReference( + ha_entity_registry.async_get(entity_ref.reference_id).name, + ha_entity_registry.async_get(entity_ref.reference_id).original_name, + entity_ref.reference_id, + )._asdict() + for entity_ref in zha_device_registry.get(self.device.ieee) + if list(entity_ref.cluster_channels.values())[ + 0 + ].cluster.endpoint.endpoint_id + == self.endpoint_id + ] + + async def async_remove_from_group(self) -> None: + """Remove the device endpoint from the provided zigbee group.""" + try: + await self._zha_device.device.endpoints[ + self._endpoint_id + ].remove_from_group(self._zha_group.group_id) + except (zigpy.exceptions.ZigbeeException, asyncio.TimeoutError) as ex: + self.debug( + "Failed to remove endpoint: %s for device '%s' from group: 0x%04x ex: %s", + self._endpoint_id, + self._zha_device.ieee, + self._zha_group.group_id, + str(ex), + ) + + def log(self, level: int, msg: str, *args) -> None: + """Log a message.""" + msg = f"[%s](%s): {msg}" + args = (f"0x{self._zha_group.group_id:04x}", self.endpoint_id) + args + _LOGGER.log(level, msg, *args) + class ZHAGroup(LogMixin): """ZHA Zigbee group object.""" @@ -45,77 +136,79 @@ def endpoint(self) -> ZigpyEndpointType: return self._zigpy_group.endpoint @property - def members(self) -> List[ZhaDeviceType]: + def members(self) -> List[ZHAGroupMember]: """Return the ZHA devices that are members of this group.""" return [ - self._zha_gateway.devices.get(member_ieee[0]) - for member_ieee in self._zigpy_group.members.keys() - if member_ieee[0] in self._zha_gateway.devices + ZHAGroupMember( + self, self._zha_gateway.devices.get(member_ieee), endpoint_id + ) + for (member_ieee, endpoint_id) in self._zigpy_group.members.keys() + if member_ieee in self._zha_gateway.devices ] - async def async_add_members(self, member_ieee_addresses: List[EUI64]) -> None: + async def async_add_members(self, members: List[GroupMember]) -> None: """Add members to this group.""" - if len(member_ieee_addresses) > 1: + if len(members) > 1: tasks = [] - for ieee in member_ieee_addresses: + for member in members: tasks.append( - self._zha_gateway.devices[ieee].async_add_to_group(self.group_id) + self._zha_gateway.devices[member.ieee].async_add_endpoint_to_group( + member.endpoint_id, self.group_id + ) ) await asyncio.gather(*tasks) else: await self._zha_gateway.devices[ - member_ieee_addresses[0] - ].async_add_to_group(self.group_id) + members[0].ieee + ].async_add_endpoint_to_group(members[0].endpoint_id, self.group_id) - async def async_remove_members(self, member_ieee_addresses: List[EUI64]) -> None: + async def async_remove_members(self, members: List[GroupMember]) -> None: """Remove members from this group.""" - if len(member_ieee_addresses) > 1: + if len(members) > 1: tasks = [] - for ieee in member_ieee_addresses: + for member in members: tasks.append( - self._zha_gateway.devices[ieee].async_remove_from_group( - self.group_id + self._zha_gateway.devices[ + member.ieee + ].async_remove_endpoint_from_group( + member.endpoint_id, self.group_id ) ) await asyncio.gather(*tasks) else: await self._zha_gateway.devices[ - member_ieee_addresses[0] - ].async_remove_from_group(self.group_id) + members[0].ieee + ].async_remove_endpoint_from_group(members[0].endpoint_id, 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) + for member in self.members: + entity_references = member.associated_entities + for entity_reference in entity_references: + all_entity_ids.append(entity_reference["entity_id"]) return all_entity_ids def get_domain_entity_ids(self, domain) -> List[str]: """Return entity ids from the entity domain for this group.""" domain_entity_ids: List[str] = [] - for device in self.members: + for member in self.members: entities = async_entries_for_device( - self._zha_gateway.ha_entity_registry, device.device_id + self._zha_gateway.ha_entity_registry, member.device.device_id ) domain_entity_ids.extend( [entity.entity_id for entity in entities if entity.domain == domain] ) return domain_entity_ids - @callback - def async_get_info(self) -> Dict[str, Any]: + @property + def group_info(self) -> Dict[str, Any]: """Get ZHA group info.""" group_info: Dict[str, Any] = {} group_info["group_id"] = self.group_id group_info["name"] = self.name - group_info["members"] = [ - zha_device.async_get_info() for zha_device in self.members - ] + group_info["members"] = [member.member_info for member in self.members] return group_info def log(self, level: int, msg: str, *args): diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 755ba7ae710267..6906b5b3e8cf88 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -3,7 +3,10 @@ "step": { "user": { "title": "ZHA", - "data": { "radio_type": "Radio Type", "usb_path": "USB Device Path" } + "data": { + "radio_type": "Radio Type", + "usb_path": "[%key:common::config_flow::data::usb_path%]" + } } }, "error": { "cannot_connect": "Unable to connect to ZHA device." }, diff --git a/homeassistant/components/zha/translations/es-419.json b/homeassistant/components/zha/translations/es-419.json index 81803aa8cf4d6d..f072420dfc5d20 100644 --- a/homeassistant/components/zha/translations/es-419.json +++ b/homeassistant/components/zha/translations/es-419.json @@ -17,7 +17,28 @@ } }, "device_automation": { + "action_type": { + "squawk": "Graznido", + "warn": "Advertir" + }, "trigger_subtype": { + "both_buttons": "Ambos botones", + "button_1": "Primer bot\u00f3n", + "button_2": "Segundo bot\u00f3n", + "button_3": "Tercer bot\u00f3n", + "button_4": "Cuarto bot\u00f3n", + "button_5": "Quinto bot\u00f3n", + "button_6": "Sexto bot\u00f3n", + "close": "Cerrar", + "dim_down": "Bajar la intensidad", + "dim_up": "Aumentar intensidad", + "face_1": "con la cara 1 activada", + "face_2": "con la cara 2 activada", + "face_3": "con la cara 3 activada", + "face_4": "con la cara 4 activada", + "face_5": "con la cara 5 activada", + "face_6": "con la cara 6 activada", + "face_any": "Con cualquier cara/especificada(s) activada(s)", "left": "Izquierda", "open": "Abrir", "right": "Derecha", @@ -31,7 +52,23 @@ "device_rotated": "Dispositivo girado \"{subtype}\"", "device_shaken": "Dispositivo agitado", "device_slid": "Dispositivo deslizado \"{subtype}\"", - "device_tilted": "Dispositivo inclinado" + "device_tilted": "Dispositivo inclinado", + "remote_button_alt_double_press": "El bot\u00f3n \"{subtype}\" fue presionado 2 veces (modo alternativo)", + "remote_button_alt_long_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado continuamente (modo alternativo)", + "remote_button_alt_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada (modo alternativo)", + "remote_button_alt_quadruple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 4 veces (modo alternativo)", + "remote_button_alt_quintuple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 5 veces (modo alternativo)", + "remote_button_alt_short_press": "El bot\u00f3n \"{subtype}\" ha sido presionado (modo alternativo)", + "remote_button_alt_short_release": "El bot\u00f3n \"{subtype}\" ha sido soltado (modo alternativo)", + "remote_button_alt_triple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 3 veces (modo alternativo)", + "remote_button_double_press": "El bot\u00f3n \"{subtype}\" fue presionado 2 veces", + "remote_button_long_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado continuamente", + "remote_button_long_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\" despu\u00e9s de una pulsaci\u00f3n prolongada", + "remote_button_quadruple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 4 veces", + "remote_button_quintuple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 5 veces", + "remote_button_short_press": "Se presion\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_short_release": "Se solt\u00f3 el bot\u00f3n \"{subtype}\"", + "remote_button_triple_press": "El bot\u00f3n \"{subtype}\" ha sido pulsado 3 veces" } } } \ No newline at end of file diff --git a/homeassistant/components/zwave/lock.py b/homeassistant/components/zwave/lock.py index 924dfdfa4ad768..c9601679f573f9 100644 --- a/homeassistant/components/zwave/lock.py +++ b/homeassistant/components/zwave/lock.py @@ -38,6 +38,10 @@ (0x0090, 0x238): WORKAROUND_DEVICE_STATE, # Kwikset 888ZW500-15S Smartcode 888 (0x0090, 0x541): WORKAROUND_DEVICE_STATE, + # Kwikset 916 + (0x0090, 0x0001): WORKAROUND_DEVICE_STATE, + # Kwikset Obsidian + (0x0090, 0x0742): WORKAROUND_DEVICE_STATE, # Yale Locks # Yale YRD210, YRD220, YRL220 (0x0129, 0x0000): WORKAROUND_DEVICE_STATE | WORKAROUND_ALARM_TYPE, diff --git a/homeassistant/components/zwave_mqtt/__init__.py b/homeassistant/components/zwave_mqtt/__init__.py new file mode 100644 index 00000000000000..8b12457475059d --- /dev/null +++ b/homeassistant/components/zwave_mqtt/__init__.py @@ -0,0 +1,328 @@ +"""The zwave_mqtt integration.""" +import asyncio +import json +import logging + +from openzwavemqtt import OZWManager, OZWOptions +from openzwavemqtt.const import ( + EVENT_INSTANCE_EVENT, + EVENT_NODE_ADDED, + EVENT_NODE_CHANGED, + EVENT_NODE_REMOVED, + EVENT_VALUE_ADDED, + EVENT_VALUE_CHANGED, + EVENT_VALUE_REMOVED, + CommandClass, + ValueType, +) +from openzwavemqtt.models.node import OZWNode +from openzwavemqtt.models.value import OZWValue +import voluptuous as vol + +from homeassistant.components import mqtt +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.device_registry import async_get_registry as get_dev_reg +from homeassistant.helpers.dispatcher import async_dispatcher_send + +from . import const +from .const import DATA_UNSUBSCRIBE, DOMAIN, PLATFORMS, TOPIC_OPENZWAVE +from .discovery import DISCOVERY_SCHEMAS, check_node_schema, check_value_schema +from .entity import ( + ZWaveDeviceEntityValues, + create_device_id, + create_device_name, + create_value_id, +) +from .services import ZWaveServices + +_LOGGER = logging.getLogger(__name__) + +CONFIG_SCHEMA = vol.Schema({DOMAIN: vol.Schema({})}, extra=vol.ALLOW_EXTRA) +DATA_DEVICES = "zwave-mqtt-devices" + + +async def async_setup(hass: HomeAssistant, config: dict): + """Initialize basic config of zwave_mqtt component.""" + if "mqtt" not in hass.config.components: + _LOGGER.error("MQTT integration is not set up") + return False + hass.data[DOMAIN] = {} + return True + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up zwave_mqtt from a config entry.""" + zwave_mqtt_data = hass.data[DOMAIN][entry.entry_id] = {} + zwave_mqtt_data[DATA_UNSUBSCRIBE] = [] + + data_nodes = {} + data_values = {} + removed_nodes = [] + + @callback + def send_message(topic, payload): + mqtt.async_publish(hass, topic, json.dumps(payload)) + + options = OZWOptions(send_message=send_message, topic_prefix=f"{TOPIC_OPENZWAVE}/") + manager = OZWManager(options) + + @callback + def async_node_added(node): + # Caution: This is also called on (re)start. + _LOGGER.debug("[NODE ADDED] node_id: %s", node.id) + data_nodes[node.id] = node + if node.id not in data_values: + data_values[node.id] = [] + + @callback + def async_node_changed(node): + _LOGGER.debug("[NODE CHANGED] node_id: %s", node.id) + data_nodes[node.id] = node + # notify devices about the node change + if node.id not in removed_nodes: + hass.async_create_task(async_handle_node_update(hass, node)) + + @callback + def async_node_removed(node): + _LOGGER.debug("[NODE REMOVED] node_id: %s", node.id) + data_nodes.pop(node.id) + # node added/removed events also happen on (re)starts of hass/mqtt/ozw + # cleanup device/entity registry if we know this node is permanently deleted + # entities itself are removed by the values logic + if node.id in removed_nodes: + hass.async_create_task(async_handle_remove_node(hass, node)) + removed_nodes.remove(node.id) + + @callback + def async_instance_event(message): + event = message["event"] + event_data = message["data"] + _LOGGER.debug("[INSTANCE EVENT]: %s - data: %s", event, event_data) + # The actual removal action of a Z-Wave node is reported as instance event + # Only when this event is detected we cleanup the device and entities from hass + if event == "removenode" and "Node" in event_data: + removed_nodes.append(event_data["Node"]) + + @callback + def async_value_added(value): + node = value.node + node_id = value.node.node_id + + # Filter out CommandClasses we're definitely not interested in. + if value.command_class in [ + CommandClass.CONFIGURATION, + CommandClass.VERSION, + CommandClass.MANUFACTURER_SPECIFIC, + ]: + return + + _LOGGER.debug( + "[VALUE ADDED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", + value.node.id, + value.label, + value.value, + value.value_id_key, + value.command_class, + ) + + node_data_values = data_values[node_id] + + # Check if this value should be tracked by an existing entity + value_unique_id = create_value_id(value) + for values in node_data_values: + values.async_check_value(value) + if values.values_id == value_unique_id: + return # this value already has an entity + + # Run discovery on it and see if any entities need created + for schema in DISCOVERY_SCHEMAS: + if not check_node_schema(node, schema): + continue + if not check_value_schema( + value, schema[const.DISC_VALUES][const.DISC_PRIMARY] + ): + continue + + values = ZWaveDeviceEntityValues(hass, options, schema, value) + values.async_setup() + + # We create a new list and update the reference here so that + # the list can be safely iterated over in the main thread + data_values[node_id] = node_data_values + [values] + + @callback + def async_value_changed(value): + # if an entity belonging to this value needs updating, + # it's handled within the entity logic + _LOGGER.debug( + "[VALUE CHANGED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", + value.node.id, + value.label, + value.value, + value.value_id_key, + value.command_class, + ) + # Handle a scene activation message + if value.command_class in [ + CommandClass.SCENE_ACTIVATION, + CommandClass.CENTRAL_SCENE, + ]: + async_handle_scene_activated(hass, value) + return + + @callback + def async_value_removed(value): + _LOGGER.debug( + "[VALUE REMOVED] node_id: %s - label: %s - value: %s - value_id: %s - CC: %s", + value.node.id, + value.label, + value.value, + value.value_id_key, + value.command_class, + ) + # signal all entities using this value for removal + value_unique_id = create_value_id(value) + async_dispatcher_send(hass, const.SIGNAL_DELETE_ENTITY, value_unique_id) + # remove value from our local list + node_data_values = data_values[value.node.id] + node_data_values[:] = [ + item for item in node_data_values if item.values_id != value_unique_id + ] + + # Listen to events for node and value changes + options.listen(EVENT_NODE_ADDED, async_node_added) + options.listen(EVENT_NODE_CHANGED, async_node_changed) + options.listen(EVENT_NODE_REMOVED, async_node_removed) + options.listen(EVENT_VALUE_ADDED, async_value_added) + options.listen(EVENT_VALUE_CHANGED, async_value_changed) + options.listen(EVENT_VALUE_REMOVED, async_value_removed) + options.listen(EVENT_INSTANCE_EVENT, async_instance_event) + + # Register Services + services = ZWaveServices(hass, manager) + services.async_register() + + @callback + def async_receive_message(msg): + manager.receive_message(msg.topic, msg.payload) + + async def start_platforms(): + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_setup(entry, component) + for component in PLATFORMS + ] + ) + zwave_mqtt_data[DATA_UNSUBSCRIBE].append( + await mqtt.async_subscribe( + hass, f"{TOPIC_OPENZWAVE}/#", async_receive_message + ) + ) + + hass.async_create_task(start_platforms()) + + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): + """Unload a config entry.""" + # cleanup platforms + unload_ok = all( + await asyncio.gather( + *[ + hass.config_entries.async_forward_entry_unload(entry, component) + for component in PLATFORMS + ] + ) + ) + if not unload_ok: + return False + + # unsubscribe all listeners + for unsubscribe_listener in hass.data[DOMAIN][entry.entry_id][DATA_UNSUBSCRIBE]: + unsubscribe_listener() + hass.data[DOMAIN].pop(entry.entry_id) + + return True + + +async def async_handle_remove_node(hass: HomeAssistant, node: OZWNode): + """Handle the removal of a Z-Wave node, removing all traces in device/entity registry.""" + dev_registry = await get_dev_reg(hass) + # grab device in device registry attached to this node + dev_id = create_device_id(node) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + if not device: + return + devices_to_remove = [device.id] + # also grab slave devices (node instances) + for item in dev_registry.devices.values(): + if item.via_device_id == device.id: + devices_to_remove.append(item.id) + # remove all devices in registry related to this node + # note: removal of entity registry is handled by core + for dev_id in devices_to_remove: + dev_registry.async_remove_device(dev_id) + + +async def async_handle_node_update(hass: HomeAssistant, node: OZWNode): + """ + Handle a node updated event from OZW. + + Meaning some of the basic info like name/model is updated. + We want these changes to be pushed to the device registry. + """ + dev_registry = await get_dev_reg(hass) + # grab device in device registry attached to this node + dev_id = create_device_id(node) + device = dev_registry.async_get_device({(DOMAIN, dev_id)}, set()) + if not device: + return + # update device in device registry with (updated) info + for item in dev_registry.devices.values(): + if item.id != device.id and item.via_device_id != device.id: + continue + dev_name = create_device_name(node) + dev_registry.async_update_device( + item.id, + manufacturer=node.node_manufacturer_name, + model=node.node_product_name, + name=dev_name, + ) + + +@callback +def async_handle_scene_activated(hass: HomeAssistant, scene_value: OZWValue): + """Handle a (central) scene activation message.""" + node_id = scene_value.node.id + scene_id = scene_value.index + scene_label = scene_value.label + if scene_value.command_class == CommandClass.SCENE_ACTIVATION: + # legacy/network scene + scene_value_id = scene_value.value + scene_value_label = scene_value.label + else: + # central scene command + if scene_value.type != ValueType.LIST: + return + scene_value_label = scene_value.value["Selected"] + scene_value_id = scene_value.value["Selected_id"] + + _LOGGER.debug( + "[SCENE_ACTIVATED] node_id: %s - scene_id: %s - scene_value_id: %s", + node_id, + scene_id, + scene_value_id, + ) + # Simply forward it to the hass event bus + hass.bus.async_fire( + const.EVENT_SCENE_ACTIVATED, + { + const.ATTR_NODE_ID: node_id, + const.ATTR_SCENE_ID: scene_id, + const.ATTR_SCENE_LABEL: scene_label, + const.ATTR_SCENE_VALUE_ID: scene_value_id, + const.ATTR_SCENE_VALUE_LABEL: scene_value_label, + }, + ) diff --git a/homeassistant/components/zwave_mqtt/config_flow.py b/homeassistant/components/zwave_mqtt/config_flow.py new file mode 100644 index 00000000000000..ff6ab21994f1d1 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/config_flow.py @@ -0,0 +1,24 @@ +"""Config flow for zwave_mqtt integration.""" +from homeassistant import config_entries + +from .const import DOMAIN # pylint:disable=unused-import + +TITLE = "Z-Wave MQTT" + + +class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for zwave_mqtt.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if self._async_current_entries(): + return self.async_abort(reason="one_instance_allowed") + if "mqtt" not in self.hass.config.components: + return self.async_abort(reason="mqtt_required") + if user_input is not None: + return self.async_create_entry(title=TITLE, data={}) + + return self.async_show_form(step_id="user") diff --git a/homeassistant/components/zwave_mqtt/const.py b/homeassistant/components/zwave_mqtt/const.py new file mode 100644 index 00000000000000..37270748a6a835 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/const.py @@ -0,0 +1,44 @@ +"""Constants for the zwave_mqtt integration.""" +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN + +DOMAIN = "zwave_mqtt" +DATA_UNSUBSCRIBE = "unsubscribe" +PLATFORMS = [SENSOR_DOMAIN, SWITCH_DOMAIN] + +# MQTT Topics +TOPIC_OPENZWAVE = "OpenZWave" + +# Common Attributes +ATTR_INSTANCE_ID = "instance_id" +ATTR_SECURE = "secure" +ATTR_NODE_ID = "node_id" +ATTR_SCENE_ID = "scene_id" +ATTR_SCENE_LABEL = "scene_label" +ATTR_SCENE_VALUE_ID = "scene_value_id" +ATTR_SCENE_VALUE_LABEL = "scene_value_label" + +# Service specific +SERVICE_ADD_NODE = "add_node" +SERVICE_REMOVE_NODE = "remove_node" + +# Home Assistant Events +EVENT_SCENE_ACTIVATED = f"{DOMAIN}.scene_activated" + +# Signals +SIGNAL_DELETE_ENTITY = f"{DOMAIN}_delete_entity" + +# Discovery Information +DISC_COMMAND_CLASS = "command_class" +DISC_COMPONENT = "component" +DISC_GENERIC_DEVICE_CLASS = "generic_device_class" +DISC_GENRE = "genre" +DISC_INDEX = "index" +DISC_INSTANCE = "instance" +DISC_NODE_ID = "node_id" +DISC_OPTIONAL = "optional" +DISC_PRIMARY = "primary" +DISC_SCHEMAS = "schemas" +DISC_SPECIFIC_DEVICE_CLASS = "specific_device_class" +DISC_TYPE = "type" +DISC_VALUES = "values" diff --git a/homeassistant/components/zwave_mqtt/discovery.py b/homeassistant/components/zwave_mqtt/discovery.py new file mode 100644 index 00000000000000..19bcd9818d2f28 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/discovery.py @@ -0,0 +1,107 @@ +"""Map Z-Wave nodes and values to Home Assistant entities.""" +import openzwavemqtt.const as const_ozw +from openzwavemqtt.const import CommandClass, ValueGenre, ValueType + +from . import const + +DISCOVERY_SCHEMAS = ( + { # All other text/numeric sensors + const.DISC_COMPONENT: "sensor", + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: ( + CommandClass.SENSOR_MULTILEVEL, + CommandClass.METER, + CommandClass.ALARM, + CommandClass.SENSOR_ALARM, + CommandClass.INDICATOR, + CommandClass.BATTERY, + CommandClass.NOTIFICATION, + CommandClass.BASIC, + ), + const.DISC_TYPE: ( + ValueType.DECIMAL, + ValueType.INT, + ValueType.STRING, + ValueType.BYTE, + ValueType.LIST, + ), + } + }, + }, + { # Switch platform + const.DISC_COMPONENT: "switch", + const.DISC_GENERIC_DEVICE_CLASS: ( + const_ozw.GENERIC_TYPE_METER, + const_ozw.GENERIC_TYPE_SENSOR_ALARM, + const_ozw.GENERIC_TYPE_SENSOR_BINARY, + const_ozw.GENERIC_TYPE_SWITCH_BINARY, + const_ozw.GENERIC_TYPE_ENTRY_CONTROL, + const_ozw.GENERIC_TYPE_SENSOR_MULTILEVEL, + const_ozw.GENERIC_TYPE_SWITCH_MULTILEVEL, + const_ozw.GENERIC_TYPE_GENERIC_CONTROLLER, + const_ozw.GENERIC_TYPE_SWITCH_REMOTE, + const_ozw.GENERIC_TYPE_REPEATER_SLAVE, + const_ozw.GENERIC_TYPE_THERMOSTAT, + const_ozw.GENERIC_TYPE_WALL_CONTROLLER, + ), + const.DISC_VALUES: { + const.DISC_PRIMARY: { + const.DISC_COMMAND_CLASS: (CommandClass.SWITCH_BINARY,), + const.DISC_TYPE: ValueType.BOOL, + const.DISC_GENRE: ValueGenre.USER, + } + }, + }, +) + + +def check_node_schema(node, schema): + """Check if node matches the passed node schema.""" + if const.DISC_NODE_ID in schema and node.node_id not in schema[const.DISC_NODE_ID]: + return False + if const.DISC_GENERIC_DEVICE_CLASS in schema and not eq_or_in( + node.node_generic, schema[const.DISC_GENERIC_DEVICE_CLASS] + ): + return False + if const.DISC_SPECIFIC_DEVICE_CLASS in schema and not eq_or_in( + node.node_specific, schema[const.DISC_SPECIFIC_DEVICE_CLASS] + ): + return False + return True + + +def check_value_schema(value, schema): + """Check if the value matches the passed value schema.""" + if ( + const.DISC_COMMAND_CLASS in schema + and value.parent.command_class_id not in schema[const.DISC_COMMAND_CLASS] + ): + return False + if const.DISC_TYPE in schema and not eq_or_in(value.type, schema[const.DISC_TYPE]): + return False + if const.DISC_GENRE in schema and not eq_or_in( + value.genre, schema[const.DISC_GENRE] + ): + return False + if const.DISC_INDEX in schema and not eq_or_in( + value.index, schema[const.DISC_INDEX] + ): + return False + if const.DISC_INSTANCE in schema and not eq_or_in( + value.instance, schema[const.DISC_INSTANCE] + ): + return False + if const.DISC_SCHEMAS in schema: + found = False + for schema_item in schema[const.DISC_SCHEMAS]: + found = found or check_value_schema(value, schema_item) + if not found: + return False + + return True + + +def eq_or_in(val, options): + """Return True if options contains value or if value is equal to options.""" + return val in options if isinstance(options, tuple) else val == options diff --git a/homeassistant/components/zwave_mqtt/entity.py b/homeassistant/components/zwave_mqtt/entity.py new file mode 100644 index 00000000000000..0f23a573eed683 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/entity.py @@ -0,0 +1,289 @@ +"""Generic Z-Wave Entity Classes.""" + +import copy +import logging + +from openzwavemqtt.const import ( + EVENT_INSTANCE_STATUS_CHANGED, + EVENT_VALUE_CHANGED, + OZW_READY_STATES, +) +from openzwavemqtt.models.node import OZWNode +from openzwavemqtt.models.value import OZWValue + +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, + async_dispatcher_send, +) +from homeassistant.helpers.entity import Entity + +from . import const +from .const import DOMAIN, PLATFORMS +from .discovery import check_node_schema, check_value_schema + +_LOGGER = logging.getLogger(__name__) + + +class ZWaveDeviceEntityValues: + """Manages entity access to the underlying Z-Wave value objects.""" + + def __init__(self, hass, options, schema, primary_value): + """Initialize the values object with the passed entity schema.""" + self._hass = hass + self._entity_created = False + self._schema = copy.deepcopy(schema) + self._values = {} + self.options = options + + # Go through values listed in the discovery schema, initialize them, + # and add a check to the schema to make sure the Instance matches. + for name, disc_settings in self._schema[const.DISC_VALUES].items(): + self._values[name] = None + disc_settings[const.DISC_INSTANCE] = [primary_value.instance] + + self._values[const.DISC_PRIMARY] = primary_value + self._node = primary_value.node + self._schema[const.DISC_NODE_ID] = [self._node.node_id] + + def async_setup(self): + """Set up values instance.""" + # Check values that have already been discovered for node + # and see if they match the schema and need added to the entity. + for value in self._node.values(): + self.async_check_value(value) + + # Check if all the _required_ values in the schema are present and + # create the entity. + self._async_check_entity_ready() + + def __getattr__(self, name): + """Get the specified value for this entity.""" + return self._values.get(name, None) + + def __iter__(self): + """Allow iteration over all values.""" + return iter(self._values.values()) + + def __contains__(self, name): + """Check if the specified name/key exists in the values.""" + return name in self._values + + @callback + def async_check_value(self, value): + """Check if the new value matches a missing value for this entity. + + If a match is found, it is added to the values mapping. + """ + # Make sure the node matches the schema for this entity. + if not check_node_schema(value.node, self._schema): + return + + # Go through the possible values for this entity defined by the schema. + for name in self._values: + # Skip if it's already been added. + if self._values[name] is not None: + continue + # Skip if the value doesn't match the schema. + if not check_value_schema(value, self._schema[const.DISC_VALUES][name]): + continue + + # Add value to mapping. + self._values[name] = value + + # If the entity has already been created, notify it of the new value. + if self._entity_created: + async_dispatcher_send( + self._hass, f"{DOMAIN}_{self.values_id}_value_added" + ) + + # Check if entity has all required values and create the entity if needed. + self._async_check_entity_ready() + + @callback + def _async_check_entity_ready(self): + """Check if all required values are discovered and create entity.""" + # Abort if the entity has already been created + if self._entity_created: + return + + # Go through values defined in the schema and abort if a required value is missing. + for name, disc_settings in self._schema[const.DISC_VALUES].items(): + if self._values[name] is None and not disc_settings.get( + const.DISC_OPTIONAL + ): + return + + # We have all the required values, so create the entity. + component = self._schema[const.DISC_COMPONENT] + + _LOGGER.debug( + "Adding Node_id=%s Generic_command_class=%s, " + "Specific_command_class=%s, " + "Command_class=%s, Index=%s, Value type=%s, " + "Genre=%s as %s", + self._node.node_id, + self._node.node_generic, + self._node.node_specific, + self.primary.command_class, + self.primary.index, + self.primary.type, + self.primary.genre, + component, + ) + self._entity_created = True + + if component in PLATFORMS: + async_dispatcher_send(self._hass, f"{DOMAIN}_new_{component}", self) + + @property + def values_id(self): + """Identification for this values collection.""" + return create_value_id(self.primary) + + +class ZWaveDeviceEntity(Entity): + """Generic Entity Class for a Z-Wave Device.""" + + def __init__(self, values): + """Initialize a generic Z-Wave device entity.""" + self.values = values + self.options = values.options + + @callback + def on_value_update(self): + """Call when a value is added/updated in the entity EntityValues Collection. + + To be overridden by platforms needing this event. + """ + + async def async_added_to_hass(self): + """Call when entity is added.""" + # add dispatcher and OZW listeners callbacks, + self.options.listen(EVENT_VALUE_CHANGED, self._value_changed) + self.options.listen(EVENT_INSTANCE_STATUS_CHANGED, self._instance_updated) + # add to on_remove so they will be cleaned up on entity removal + self.async_on_remove( + async_dispatcher_connect( + self.hass, const.SIGNAL_DELETE_ENTITY, self._delete_callback + ) + ) + self.async_on_remove( + async_dispatcher_connect( + self.hass, + f"{DOMAIN}_{self.values.values_id}_value_added", + self._value_added, + ) + ) + + @property + def device_info(self): + """Return device information for the device registry.""" + node = self.values.primary.node + node_instance = self.values.primary.instance + dev_id = create_device_id(node, self.values.primary.instance) + device_info = { + "identifiers": {(DOMAIN, dev_id)}, + "name": create_device_name(node), + "manufacturer": node.node_manufacturer_name, + "model": node.node_product_name, + } + # device with multiple instances is split up into virtual devices for each instance + if node_instance > 1: + parent_dev_id = create_device_id(node) + device_info["name"] += f" - Instance {node_instance}" + device_info["via_device"] = (DOMAIN, parent_dev_id) + return device_info + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + return {const.ATTR_NODE_ID: self.values.primary.node.node_id} + + @property + def name(self): + """Return the name of the entity.""" + node = self.values.primary.node + return f"{create_device_name(node)}: {self.values.primary.label}" + + @property + def unique_id(self): + """Return the unique_id of the entity.""" + return self.values.values_id + + @property + def available(self) -> bool: + """Return entity availability.""" + # Use OZW Daemon status for availability. + instance_status = self.values.primary.ozw_instance.get_status() + return instance_status and instance_status.status in ( + state.value for state in OZW_READY_STATES + ) + + @callback + def _value_changed(self, value): + """Call when a value from ZWaveDeviceEntityValues is changed. + + Should not be overridden by subclasses. + """ + if value.value_id_key in (v.value_id_key for v in self.values if v): + self.on_value_update() + self.async_write_ha_state() + + @callback + def _value_added(self): + """Call when a value from ZWaveDeviceEntityValues is added. + + Should not be overridden by subclasses. + """ + self.on_value_update() + + @callback + def _instance_updated(self, new_status): + """Call when the instance status changes. + + Should not be overridden by subclasses. + """ + self.on_value_update() + self.async_write_ha_state() + + async def _delete_callback(self, values_id): + """Remove this entity.""" + if not self.values: + return # race condition: delete already requested + if values_id == self.values.values_id: + await self.async_remove() + + async def async_will_remove_from_hass(self) -> None: + """Call when entity will be removed from hass.""" + # cleanup OZW listeners + self.options.listeners[EVENT_VALUE_CHANGED].remove(self._value_changed) + self.options.listeners[EVENT_INSTANCE_STATUS_CHANGED].remove( + self._instance_updated + ) + + +def create_device_name(node: OZWNode): + """Generate sensible (short) default device name from a OZWNode.""" + if node.meta_data["Name"]: + dev_name = node.meta_data["Name"] + elif node.node_product_name: + dev_name = node.node_product_name + elif node.node_device_type_string: + dev_name = node.node_device_type_string + else: + dev_name = node.specific_string + return dev_name + + +def create_device_id(node: OZWNode, node_instance: int = 1): + """Generate unique device_id from a OZWNode.""" + ozw_instance = node.parent.id + dev_id = f"{ozw_instance}.{node.node_id}.{node_instance}" + return dev_id + + +def create_value_id(value: OZWValue): + """Generate unique value_id from an OZWValue.""" + # [OZW_INSTANCE_ID]-[NODE_ID]-[VALUE_ID_KEY] + return f"{value.node.parent.id}-{value.node.id}-{value.value_id_key}" diff --git a/homeassistant/components/zwave_mqtt/manifest.json b/homeassistant/components/zwave_mqtt/manifest.json new file mode 100644 index 00000000000000..8d067bf5c3541f --- /dev/null +++ b/homeassistant/components/zwave_mqtt/manifest.json @@ -0,0 +1,17 @@ +{ + "domain": "zwave_mqtt", + "name": "Z-Wave over MQTT", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/zwave_mqtt", + "requirements": [ + "python-openzwave-mqtt==1.0.1" + ], + "after_dependencies": [ + "mqtt" + ], + "codeowners": [ + "@cgarwood", + "@marcelveldt", + "@MartinHjelmare" + ] +} diff --git a/homeassistant/components/zwave_mqtt/sensor.py b/homeassistant/components/zwave_mqtt/sensor.py new file mode 100644 index 00000000000000..309c2784405625 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/sensor.py @@ -0,0 +1,131 @@ +"""Representation of Z-Wave sensors.""" + +import logging + +from openzwavemqtt.const import CommandClass + +from homeassistant.components.sensor import ( + DEVICE_CLASS_BATTERY, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_ILLUMINANCE, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_TEMPERATURE, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.const import TEMP_CELSIUS, TEMP_FAHRENHEIT +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave sensor from config entry.""" + + @callback + def async_add_sensor(value): + """Add Z-Wave Sensor.""" + # Basic Sensor types + if isinstance(value.primary.value, (float, int)): + sensor = ZWaveNumericSensor(value) + + elif isinstance(value.primary.value, dict): + sensor = ZWaveListSensor(value) + + else: + _LOGGER.warning("Sensor not implemented for value %s", value.primary.label) + return + + async_add_entities([sensor]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{SENSOR_DOMAIN}", async_add_sensor + ) + ) + + +class ZwaveSensorBase(ZWaveDeviceEntity): + """Basic Representation of a Z-Wave sensor.""" + + @property + def device_class(self): + """Return the device class of the sensor.""" + if self.values.primary.command_class == CommandClass.BATTERY: + return DEVICE_CLASS_BATTERY + if self.values.primary.command_class == CommandClass.METER: + return DEVICE_CLASS_POWER + if "Temperature" in self.values.primary.label: + return DEVICE_CLASS_TEMPERATURE + if "Illuminance" in self.values.primary.label: + return DEVICE_CLASS_ILLUMINANCE + if "Humidity" in self.values.primary.label: + return DEVICE_CLASS_HUMIDITY + if "Power" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Energy" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Electric" in self.values.primary.label: + return DEVICE_CLASS_POWER + if "Pressure" in self.values.primary.label: + return DEVICE_CLASS_PRESSURE + return None + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # We hide some of the more advanced sensors by default to not overwhelm users + if self.values.primary.command_class in [ + CommandClass.BASIC, + CommandClass.INDICATOR, + CommandClass.NOTIFICATION, + ]: + return False + return True + + +class ZWaveNumericSensor(ZwaveSensorBase): + """Representation of a Z-Wave sensor.""" + + @property + def state(self): + """Return state of the sensor.""" + return round(self.values.primary.value, 2) + + @property + def unit_of_measurement(self): + """Return unit of measurement the value is expressed in.""" + if self.values.primary.units == "C": + return TEMP_CELSIUS + if self.values.primary.units == "F": + return TEMP_FAHRENHEIT + + return self.values.primary.units + + +class ZWaveListSensor(ZwaveSensorBase): + """Representation of a Z-Wave list sensor.""" + + @property + def state(self): + """Return the state of the sensor.""" + # We use the id as value for backwards compatibility + return self.values.primary.value["Selected_id"] + + @property + def device_state_attributes(self): + """Return the device specific state attributes.""" + attributes = super().device_state_attributes + # add the value's label as property + attributes["label"] = self.values.primary.value["Selected"] + return attributes + + @property + def entity_registry_enabled_default(self) -> bool: + """Return if the entity should be enabled when first added to the entity registry.""" + # these sensors are only here for backwards compatibility, disable them by default + return False diff --git a/homeassistant/components/zwave_mqtt/services.py b/homeassistant/components/zwave_mqtt/services.py new file mode 100644 index 00000000000000..a2f4ca2e5531db --- /dev/null +++ b/homeassistant/components/zwave_mqtt/services.py @@ -0,0 +1,53 @@ +"""Methods and classes related to executing Z-Wave commands and publishing these to hass.""" +import voluptuous as vol + +from homeassistant.core import callback + +from . import const + + +class ZWaveServices: + """Class that holds our services ( Zwave Commands) that should be published to hass.""" + + def __init__(self, hass, manager): + """Initialize with both hass and ozwmanager objects.""" + self._hass = hass + self._manager = manager + + @callback + def async_register(self): + """Register all our services.""" + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_ADD_NODE, + self.async_add_node, + schema=vol.Schema( + { + vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int), + vol.Optional(const.ATTR_SECURE, default=False): vol.Coerce(bool), + } + ), + ) + self._hass.services.async_register( + const.DOMAIN, + const.SERVICE_REMOVE_NODE, + self.async_remove_node, + schema=vol.Schema( + {vol.Optional(const.ATTR_INSTANCE_ID, default=1): vol.Coerce(int)} + ), + ) + + @callback + def async_add_node(self, service): + """Enter inclusion mode on the controller.""" + instance_id = service.data[const.ATTR_INSTANCE_ID] + secure = service.data[const.ATTR_SECURE] + instance = self._manager.get_instance(instance_id) + instance.add_node(secure) + + @callback + def async_remove_node(self, service): + """Enter exclusion mode on the controller.""" + instance_id = service.data[const.ATTR_INSTANCE_ID] + instance = self._manager.get_instance(instance_id) + instance.remove_node() diff --git a/homeassistant/components/zwave_mqtt/services.yaml b/homeassistant/components/zwave_mqtt/services.yaml new file mode 100644 index 00000000000000..92685f1a463083 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/services.yaml @@ -0,0 +1,14 @@ +# Describes the format for available Z-Wave services +add_node: + description: Add a new node to the Z-Wave network. + fields: + secure: + description: Add the new node with secure communications. Secure network key must be set, this process will fallback to add_node (unsecure) for unsupported devices. Note that unsecure devices can't directly talk to secure devices. + instance_id: + description: (Optional) The OZW Instance/Controller to use, defaults to 1. + +remove_node: + description: Remove a node from the Z-Wave network. Will set the controller into exclusion mode. + fields: + instance_id: + description: (Optional) The OZW Instance/Controller to use, defaults to 1. diff --git a/homeassistant/components/zwave_mqtt/strings.json b/homeassistant/components/zwave_mqtt/strings.json new file mode 100644 index 00000000000000..949b545086b536 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/strings.json @@ -0,0 +1,14 @@ +{ + "title": "Z-Wave over MQTT", + "config": { + "step": { + "user": { + "title": "Confirm set up" + } + }, + "abort": { + "one_instance_allowed": "The integration only supports one Z-Wave instance", + "mqtt_required": "The MQTT integration is not set up" + } + } +} diff --git a/homeassistant/components/zwave_mqtt/switch.py b/homeassistant/components/zwave_mqtt/switch.py new file mode 100644 index 00000000000000..c1a0ef353b8804 --- /dev/null +++ b/homeassistant/components/zwave_mqtt/switch.py @@ -0,0 +1,41 @@ +"""Representation of Z-Wave switches.""" +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntity +from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect + +from .const import DATA_UNSUBSCRIBE, DOMAIN +from .entity import ZWaveDeviceEntity + + +async def async_setup_entry(hass, config_entry, async_add_entities): + """Set up Z-Wave switch from config entry.""" + + @callback + def async_add_switch(value): + """Add Z-Wave Switch.""" + switch = ZWaveSwitch(value) + + async_add_entities([switch]) + + hass.data[DOMAIN][config_entry.entry_id][DATA_UNSUBSCRIBE].append( + async_dispatcher_connect( + hass, f"{DOMAIN}_new_{SWITCH_DOMAIN}", async_add_switch + ) + ) + + +class ZWaveSwitch(ZWaveDeviceEntity, SwitchEntity): + """Representation of a Z-Wave switch.""" + + @property + def is_on(self): + """Return a boolean for the state of the switch.""" + return bool(self.values.primary.value) + + async def async_turn_on(self, **kwargs): + """Turn the switch on.""" + self.values.primary.send_value(True) + + async def async_turn_off(self, **kwargs): + """Turn the switch off.""" + self.values.primary.send_value(False) diff --git a/homeassistant/components/zwave_mqtt/translations/en.json b/homeassistant/components/zwave_mqtt/translations/en.json new file mode 100644 index 00000000000000..4b5b44a76bb93b --- /dev/null +++ b/homeassistant/components/zwave_mqtt/translations/en.json @@ -0,0 +1,14 @@ +{ + "config": { + "abort": { + "mqtt_required": "The MQTT integration is not set up", + "one_instance_allowed": "The integration only supports one Z-Wave instance" + }, + "step": { + "user": { + "title": "Confirm set up" + } + } + }, + "title": "Z-Wave over MQTT" +} \ No newline at end of file diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index 340ec671edaaa6..49195e1a89a54c 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -94,7 +94,12 @@ async def async_post_init( def async_progress(self) -> List[Dict]: """Return the flows in progress.""" return [ - {"flow_id": flow.flow_id, "handler": flow.handler, "context": flow.context} + { + "flow_id": flow.flow_id, + "handler": flow.handler, + "context": flow.context, + "step_id": flow.cur_step["step_id"], + } for flow in self._progress.values() if flow.cur_step is not None ] diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 1f08da03648fce..015e240d766e27 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -16,6 +16,7 @@ "atag", "august", "axis", + "blebox", "braviatv", "brother", "cast", @@ -50,6 +51,7 @@ "harmony", "heos", "hisense_aehw4a1", + "home_connect", "homekit", "homekit_controller", "homematicip_cloud", @@ -125,6 +127,7 @@ "tado", "tellduslive", "tesla", + "tibber", "toon", "totalconnect", "tplink", @@ -146,5 +149,6 @@ "wwlln", "xiaomi_miio", "zha", - "zwave" + "zwave", + "zwave_mqtt" ] diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 5dbef37d9bf169..f46ba1611a8d1f 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -81,6 +81,14 @@ "manufacturer": "Synology" } ], + "upnp": [ + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + }, + { + "st": "urn:schemas-upnp-org:device:InternetGatewayDevice:2" + } + ], "wemo": [ { "manufacturer": "Belkin International Inc." diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 6206081dc8c701..3f297dcbbe866f 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -21,7 +21,7 @@ def __init__( """Initialize debounce. immediate: indicate if the function needs to be called right away and - wait 0.3s until executing next invocation. + wait until executing next invocation. function: optional and can be instantiated later. """ self.hass = hass diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 56b8170b99aba5..8fbb81962fff25 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -1,17 +1,21 @@ """Provide a way to connect entities belonging to one device.""" from collections import OrderedDict import logging -from typing import Any, Dict, List, Optional +from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Tuple import uuid import attr -from homeassistant.core import callback -from homeassistant.loader import bind_hass +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import Event, callback +from .debounce import Debouncer from .singleton import singleton from .typing import HomeAssistantType +if TYPE_CHECKING: + from . import entity_registry + # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs _LOGGER = logging.getLogger(__name__) @@ -22,6 +26,7 @@ STORAGE_KEY = "core.device_registry" STORAGE_VERSION = 1 SAVE_DELAY = 10 +CLEANUP_DELAY = 10 CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" @@ -32,19 +37,24 @@ class DeviceEntry: """Device Registry Entry.""" - config_entries = attr.ib(type=set, converter=set, default=attr.Factory(set)) - connections = attr.ib(type=set, converter=set, default=attr.Factory(set)) - identifiers = attr.ib(type=set, converter=set, default=attr.Factory(set)) - manufacturer = attr.ib(type=str, default=None) - model = attr.ib(type=str, default=None) - name = attr.ib(type=str, default=None) - sw_version = attr.ib(type=str, default=None) - via_device_id = attr.ib(type=str, default=None) - area_id = attr.ib(type=str, default=None) - name_by_user = attr.ib(type=str, default=None) - id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex)) + config_entries: Set[str] = attr.ib(converter=set, default=attr.Factory(set)) + connections: Set[Tuple[str, str]] = attr.ib( + converter=set, default=attr.Factory(set) + ) + identifiers: Set[Tuple[str, str]] = attr.ib( + converter=set, default=attr.Factory(set) + ) + manufacturer: str = attr.ib(default=None) + model: str = attr.ib(default=None) + name: str = attr.ib(default=None) + sw_version: str = attr.ib(default=None) + via_device_id: str = attr.ib(default=None) + area_id: str = attr.ib(default=None) + name_by_user: str = attr.ib(default=None) + entry_type: str = attr.ib(default=None) + id: str = attr.ib(default=attr.Factory(lambda: uuid.uuid4().hex)) # This value is not stored, just used to keep track of events to fire. - is_new = attr.ib(type=bool, default=False) + is_new: bool = attr.ib(default=False) def format_mac(mac: str) -> str: @@ -105,6 +115,7 @@ def async_get_or_create( model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, + entry_type=_UNDEF, via_device=None, ): """Get device. Create if it doesn't exist.""" @@ -144,6 +155,7 @@ def async_get_or_create( model=model, name=name, sw_version=sw_version, + entry_type=entry_type, ) @callback @@ -189,6 +201,7 @@ def _async_update_device( model=_UNDEF, name=_UNDEF, sw_version=_UNDEF, + entry_type=_UNDEF, via_device_id=_UNDEF, area_id=_UNDEF, name_by_user=_UNDEF, @@ -236,6 +249,7 @@ def _async_update_device( ("model", model), ("name", name), ("sw_version", sw_version), + ("entry_type", entry_type), ("via_device_id", via_device_id), ): if value is not _UNDEF and value != getattr(old, attr_name): @@ -277,6 +291,8 @@ def async_remove_device(self, device_id: str) -> None: async def async_load(self): """Load the device registry.""" + async_setup_cleanup(self.hass, self) + data = await self._store.async_load() devices = OrderedDict() @@ -291,6 +307,8 @@ async def async_load(self): model=device["model"], name=device["name"], sw_version=device["sw_version"], + # Introduced in 0.110 + entry_type=device.get("entry_type"), id=device["id"], # Introduced in 0.79 # renamed in 0.95 @@ -323,6 +341,7 @@ def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: "model": entry.model, "name": entry.name, "sw_version": entry.sw_version, + "entry_type": entry.entry_type, "id": entry.id, "via_device_id": entry.via_device_id, "area_id": entry.area_id, @@ -336,16 +355,8 @@ def _data_to_save(self) -> Dict[str, List[Dict[str, Any]]]: @callback def async_clear_config_entry(self, config_entry_id: str) -> None: """Clear config entry from registry entries.""" - remove = [] - for dev_id, device in self.devices.items(): - if device.config_entries == {config_entry_id}: - remove.append(dev_id) - else: - self._async_update_device( - dev_id, remove_config_entry_id=config_entry_id - ) - for dev_id in remove: - self.async_remove_device(dev_id) + for device in list(self.devices.values()): + self._async_update_device(device.id, remove_config_entry_id=config_entry_id) @callback def async_clear_area_id(self, area_id: str) -> None: @@ -355,7 +366,6 @@ def async_clear_area_id(self, area_id: str) -> None: self._async_update_device(dev_id, area_id=None) -@bind_hass @singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> DeviceRegistry: """Create entity registry.""" @@ -380,3 +390,69 @@ def async_entries_for_config_entry( for device in registry.devices.values() if config_entry_id in device.config_entries ] + + +@callback +def async_cleanup( + hass: HomeAssistantType, + dev_reg: DeviceRegistry, + ent_reg: "entity_registry.EntityRegistry", +) -> None: + """Clean up device registry.""" + # Find all devices that are no longer referenced in the entity registry. + referenced = {entry.device_id for entry in ent_reg.entities.values()} + orphan = set(dev_reg.devices) - referenced + + for dev_id in orphan: + dev_reg.async_remove_device(dev_id) + + # Find all referenced config entries that no longer exist + # This shouldn't happen but have not been able to track down the bug :( + config_entry_ids = {entry.entry_id for entry in hass.config_entries.async_entries()} + + for device in list(dev_reg.devices.values()): + for config_entry_id in device.config_entries: + if config_entry_id not in config_entry_ids: + dev_reg.async_update_device( + device.id, remove_config_entry_id=config_entry_id + ) + + +@callback +def async_setup_cleanup(hass: HomeAssistantType, dev_reg: DeviceRegistry) -> None: + """Clean up device registry when entities removed.""" + from . import entity_registry # pylint: disable=import-outside-toplevel + + async def cleanup(): + """Cleanup.""" + ent_reg = await entity_registry.async_get_registry(hass) + async_cleanup(hass, dev_reg, ent_reg) + + debounced_cleanup = Debouncer( + hass, _LOGGER, cooldown=CLEANUP_DELAY, immediate=False, function=cleanup + ) + + async def entity_registry_changed(event: Event) -> None: + """Handle entity updated or removed.""" + if ( + event.data["action"] == "update" + and "device_id" not in event.data["changes"] + ) or event.data["action"] == "create": + return + + await debounced_cleanup.async_call() + + if hass.is_running: + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed + ) + return + + async def startup_clean(event: Event) -> None: + """Clean up on startup.""" + hass.bus.async_listen( + entity_registry.EVENT_ENTITY_REGISTRY_UPDATED, entity_registry_changed + ) + await debounced_cleanup.async_call() + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, startup_clean) diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 0a7c52f70599e2..fb7762fb9aec5f 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -201,7 +201,7 @@ def async_register_entity_service( name: str, schema: Union[Dict[str, Any], vol.Schema], func: str, - required_features: Optional[int] = None, + required_features: Optional[List[int]] = None, ) -> None: """Register an entity service.""" if isinstance(schema, dict): diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e4d52aaa3a17e3..30b07c982522aa 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -353,6 +353,7 @@ async def _async_add_entity( "model", "name", "sw_version", + "entry_type", "via_device", ): if key in device_info: diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index f276cc498503d4..de4f5eeb2978b2 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -34,7 +34,6 @@ ) from homeassistant.core import Event, callback, split_entity_id, valid_entity_id from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED -from homeassistant.loader import bind_hass from homeassistant.util import slugify from homeassistant.util.yaml import load_yaml @@ -429,6 +428,12 @@ async def async_load(self) -> None: if data is not None: for entity in data["entities"]: + # Some old installations can have some bad entities. + # Filter them out as they cause errors down the line. + # Can be removed in Jan 2021 + if not valid_entity_id(entity["entity_id"]): + continue + entities[entity["entity_id"]] = RegistryEntry( entity_id=entity["entity_id"], config_entry_id=entity.get("config_entry_id"), @@ -491,7 +496,6 @@ def async_clear_config_entry(self, config_entry: str) -> None: self.async_remove(entity_id) -@bind_hass @singleton(DATA_REGISTRY) async def async_get_registry(hass: HomeAssistantType) -> EntityRegistry: """Create entity registry.""" diff --git a/homeassistant/helpers/instance_id.py b/homeassistant/helpers/instance_id.py new file mode 100644 index 00000000000000..1df039da47aaeb --- /dev/null +++ b/homeassistant/helpers/instance_id.py @@ -0,0 +1,31 @@ +"""Helper to create a unique instance ID.""" +from typing import Dict, Optional +import uuid + +from homeassistant.core import HomeAssistant + +from . import singleton, storage + +DATA_KEY = "core.uuid" +DATA_VERSION = 1 + +LEGACY_UUID_FILE = ".uuid" + + +@singleton.singleton(DATA_KEY) +async def async_get(hass: HomeAssistant) -> str: + """Get unique ID for the hass instance.""" + store = storage.Store(hass, DATA_VERSION, DATA_KEY, True) + + data: Optional[Dict[str, str]] = await storage.async_migrator( # type: ignore + hass, hass.config.path(LEGACY_UUID_FILE), store, + ) + + if data is not None: + return data["uuid"] + + data = {"uuid": uuid.uuid4().hex} + + await store.async_save(data) + + return data["uuid"] diff --git a/homeassistant/helpers/singleton.py b/homeassistant/helpers/singleton.py index 8d0f6873c69707..82b666be40acaa 100644 --- a/homeassistant/helpers/singleton.py +++ b/homeassistant/helpers/singleton.py @@ -4,6 +4,7 @@ from typing import Awaitable, Callable, TypeVar, cast from homeassistant.core import HomeAssistant +from homeassistant.loader import bind_hass T = TypeVar("T") @@ -19,6 +20,7 @@ def singleton(data_key: str) -> Callable[[FUNC], FUNC]: def wrapper(func: FUNC) -> FUNC: """Wrap a function with caching logic.""" + @bind_hass @functools.wraps(func) async def wrapped(hass: HomeAssistant) -> T: obj_or_evt = hass.data.get(data_key) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 00df728fb36d08..b5ac942bf2fa62 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -20,29 +20,32 @@ @bind_hass async def async_migrator( - hass, - old_path, - store, - *, - old_conf_load_func=json_util.load_json, - old_conf_migrate_func=None, + hass, old_path, store, *, old_conf_load_func=None, old_conf_migrate_func=None, ): """Migrate old data to a store and then load data. async def old_conf_migrate_func(old_data) """ + store_data = await store.async_load() + + # If we already have store data we have already migrated in the past. + if store_data is not None: + return store_data def load_old_config(): """Load old config.""" if not os.path.isfile(old_path): return None - return old_conf_load_func(old_path) + if old_conf_load_func is not None: + return old_conf_load_func(old_path) + + return json_util.load_json(old_path) config = await hass.async_add_executor_job(load_old_config) if config is None: - return await store.async_load() + return None if old_conf_migrate_func is not None: config = await old_conf_migrate_func(config) diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 3f9924d00d58cb..bc868fa16b8ec3 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -1,5 +1,6 @@ """Template helper methods for rendering strings with Home Assistant data.""" import base64 +import collections.abc from datetime import datetime from functools import wraps import json @@ -503,7 +504,7 @@ def expand(hass: HomeAssistantType, *args: Any) -> Iterable[State]: continue elif isinstance(entity, State): entity_id = entity.entity_id - elif isinstance(entity, Iterable): + elif isinstance(entity, collections.abc.Iterable): search += entity continue else: diff --git a/homeassistant/loader.py b/homeassistant/loader.py index 3c7e46991274f6..bb02b35a6e889b 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -243,6 +243,11 @@ def documentation(self) -> Optional[str]: """Return documentation.""" return cast(str, self.manifest.get("documentation")) + @property + def issue_tracker(self) -> Optional[str]: + """Return issue tracker link.""" + return cast(str, self.manifest.get("issue_tracker")) + @property def quality_scale(self) -> Optional[str]: """Return Integration Quality Scale.""" diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index c888f44c1735da..eb3092354f4967 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -8,24 +8,24 @@ attrs==19.3.0 bcrypt==3.1.7 certifi>=2020.4.5.1 ciso8601==2.1.3 -cryptography==2.9 +cryptography==2.9.2 defusedxml==0.6.0 distro==1.5.0 hass-nabucasa==0.34.2 -home-assistant-frontend==20200427.1 +home-assistant-frontend==20200505.0 importlib-metadata==1.6.0 jinja2>=2.11.1 netdisco==2.6.0 pip>=8.0.3 python-slugify==4.0.0 -pytz>=2019.03 +pytz>=2020.1 pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 sqlalchemy==1.3.16 voluptuous-serialize==2.3.0 voluptuous==0.11.7 -zeroconf==0.25.1 +zeroconf==0.26.0 pycryptodome>=3.6.6 diff --git a/homeassistant/scripts/check_config.py b/homeassistant/scripts/check_config.py index 627f5b9d976c12..8b4c6a446f2153 100644 --- a/homeassistant/scripts/check_config.py +++ b/homeassistant/scripts/check_config.py @@ -1,10 +1,11 @@ """Script to check the configuration file.""" import argparse from collections import OrderedDict +from collections.abc import Mapping, Sequence from glob import glob import logging import os -from typing import Any, Callable, Dict, List, Sequence, Tuple +from typing import Any, Callable, Dict, List, Tuple from unittest.mock import patch from homeassistant import bootstrap, core @@ -252,7 +253,7 @@ def sort_dict_key(val): indent_str = indent_count * " " if listi or isinstance(layer, list): indent_str = indent_str[:-1] + "-" - if isinstance(layer, Dict): + if isinstance(layer, Mapping): for key, value in sorted(layer.items(), key=sort_dict_key): if isinstance(value, (dict, list)): print(indent_str, str(key) + ":", line_info(value, **kwargs)) diff --git a/homeassistant/strings.json b/homeassistant/strings.json index 129b633fbf3994..d43bd749d00c44 100644 --- a/homeassistant/strings.json +++ b/homeassistant/strings.json @@ -15,6 +15,43 @@ "paused": "Paused", "home": "Home", "not_home": "Away" + }, + "config_flow": { + "title": { + "oauth2_pick_implementation": "Pick Authentication Method", + "via_hassio_addon": "{name} via Home Assistant add-on" + }, + "description": { + "confirm_setup": "Do you want to start set up?" + }, + "data": { + "username": "Username", + "password": "Password", + "host": "Host", + "port": "Port", + "usb_path": "USB Device Path", + "access_token": "Access Token", + "api_key": "API Key" + }, + "create_entry": { + "authenticated": "Successfully authenticated" + }, + "error": { + "invalid_api_key": "Invalid API key", + "invalid_access_token": "Invalid access token", + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "abort": { + "single_instance_allowed": "Already configured. Only a single configuration possible.", + "already_configured_account": "Account is already configured", + "already_configured_service": "Service is already configured", + "already_configured_device": "Device is already configured", + "no_devices_found": "No devices found on the network", + "oauth2_missing_configuration": "The component is not configured. Please follow the documentation.", + "oauth2_authorize_url_timeout": "Timeout generating authorize url." + } } } } diff --git a/requirements_all.txt b/requirements_all.txt index fe439d5c73da0d..c3b5be78d8e96d 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -9,10 +9,10 @@ ciso8601==2.1.3 importlib-metadata==1.6.0 jinja2>=2.11.1 PyJWT==1.7.1 -cryptography==2.9 +cryptography==2.9.2 pip>=8.0.3 python-slugify==4.0.0 -pytz>=2019.03 +pytz>=2020.1 pyyaml==5.3.1 requests==2.23.0 ruamel.yaml==0.15.100 @@ -215,7 +215,7 @@ aiopylgtv==0.3.3 aioswitcher==1.1.0 # homeassistant.components.unifi -aiounifi==18 +aiounifi==20 # homeassistant.components.wwlln aiowwlln==2.0.2 @@ -230,7 +230,7 @@ aladdin_connect==0.3 alarmdecoder==1.13.2 # homeassistant.components.alpha_vantage -alpha_vantage==2.1.3 +alpha_vantage==2.2.0 # homeassistant.components.ambiclimate ambiclimate==0.2.1 @@ -335,6 +335,9 @@ bimmer_connected==0.7.5 # homeassistant.components.bizkaibus bizkaibus==0.1.1 +# homeassistant.components.blebox +blebox_uniapi==1.3.2 + # homeassistant.components.blink blinkpy==0.14.3 @@ -362,7 +365,7 @@ bomradarloop==0.1.4 boto3==1.9.252 # homeassistant.components.braviatv -bravia-tv==1.0.2 +bravia-tv==1.0.3 # homeassistant.components.broadlink broadlink==0.13.2 @@ -716,11 +719,14 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200427.1 +home-assistant-frontend==20200505.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 +# homeassistant.components.home_connect +homeconnect==0.5 + # homeassistant.components.homematicip_cloud homematicip==0.10.17 @@ -961,7 +967,7 @@ numato-gpio==0.7.1 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.18.2 +numpy==1.18.4 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1158,7 +1164,7 @@ pyRFXtrx==0.25 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.13.8 +pyTibber==0.14.0 # homeassistant.components.dlink pyW215==0.7.0 @@ -1351,7 +1357,7 @@ pyhomeworks==0.0.6 pyialarm==0.3 # homeassistant.components.icloud -pyicloud==0.9.6.1 +pyicloud==0.9.7 # homeassistant.components.intesishome pyintesishome==1.7.4 @@ -1551,7 +1557,7 @@ pysensibo==1.0.3 pyserial-asyncio==0.4 # homeassistant.components.acer_projector -pyserial==3.1.1 +pyserial==3.4 # homeassistant.components.sesame pysesame2==1.0.1 @@ -1584,13 +1590,13 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.25 +pysonos==0.0.28 # homeassistant.components.spc pyspcwebgw==0.4.0 # homeassistant.components.squeezebox -pysqueezebox==0.1.2 +pysqueezebox==0.1.4 # homeassistant.components.stiebel_eltron pystiebeleltron==0.0.1.dev2 @@ -1679,6 +1685,9 @@ python-nest==4.1.0 # homeassistant.components.nmap_tracker python-nmap==0.6.1 +# homeassistant.components.zwave_mqtt +python-openzwave-mqtt==1.0.1 + # homeassistant.components.qbittorrent python-qbittorrent==0.4.1 @@ -1692,7 +1701,7 @@ python-sochain-api==0.0.2 python-songpal==0.11.2 # homeassistant.components.synology_dsm -python-synology==0.7.4 +python-synology==0.8.0 # homeassistant.components.tado python-tado==0.8.1 @@ -1837,7 +1846,7 @@ rocketchat-API==0.6.1 roku==4.1.0 # homeassistant.components.roomba -roombapy==1.5.1 +roombapy==1.5.3 # homeassistant.components.rova rova==0.1.0 @@ -1965,7 +1974,7 @@ spiderpy==1.3.1 spotcrime==1.0.4 # homeassistant.components.spotify -spotipy==2.11.1 +spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql @@ -2002,7 +2011,7 @@ sucks==0.9.4 sunwatcher==0.2.1 # homeassistant.components.surepetcare -surepy==0.2.3 +surepy==0.2.5 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.0.3 @@ -2190,13 +2199,13 @@ yeelight==0.5.1 yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2020.03.24 +youtube_dl==2020.05.03 # homeassistant.components.zengge zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.25.1 +zeroconf==0.26.0 # homeassistant.components.zha zha-quirks==0.0.38 diff --git a/requirements_test.txt b/requirements_test.txt index b3525b05c08f76..3f565d2c3a8f74 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -17,5 +17,5 @@ pytest-cov==2.8.1 pytest-sugar==0.9.3 pytest-timeout==1.3.4 pytest==5.4.1 -requests_mock==1.7.0 +requests_mock==1.8.0 responses==0.10.6 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index f1d80a3c9f0477..a7f3c6075da0a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -101,7 +101,7 @@ aiopylgtv==0.3.3 aioswitcher==1.1.0 # homeassistant.components.unifi -aiounifi==18 +aiounifi==20 # homeassistant.components.wwlln aiowwlln==2.0.2 @@ -143,11 +143,14 @@ base36==0.1.1 # homeassistant.components.zha bellows-homeassistant==0.15.2 +# homeassistant.components.blebox +blebox_uniapi==1.3.2 + # homeassistant.components.bom bomradarloop==0.1.4 # homeassistant.components.braviatv -bravia-tv==1.0.2 +bravia-tv==1.0.3 # homeassistant.components.broadlink broadlink==0.13.2 @@ -297,11 +300,14 @@ hole==0.5.1 holidays==0.10.2 # homeassistant.components.frontend -home-assistant-frontend==20200427.1 +home-assistant-frontend==20200505.0 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 +# homeassistant.components.home_connect +homeconnect==0.5 + # homeassistant.components.homematicip_cloud homematicip==0.10.17 @@ -387,7 +393,7 @@ numato-gpio==0.7.1 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.18.2 +numpy==1.18.4 # homeassistant.components.google oauth2client==4.0.0 @@ -475,6 +481,9 @@ pyMetno==0.4.6 # homeassistant.components.rfxtrx pyRFXtrx==0.25 +# homeassistant.components.tibber +pyTibber==0.14.0 + # homeassistant.components.nextbus py_nextbusnext==0.1.4 @@ -549,7 +558,7 @@ pyheos==0.6.0 pyhomematic==0.1.66 # homeassistant.components.icloud -pyicloud==0.9.6.1 +pyicloud==0.9.7 # homeassistant.components.ipma pyipma==2.0.5 @@ -641,7 +650,7 @@ pysmartthings==0.7.1 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.25 +pysonos==0.0.28 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -661,8 +670,11 @@ python-miio==0.5.0.1 # homeassistant.components.nest python-nest==4.1.0 +# homeassistant.components.zwave_mqtt +python-openzwave-mqtt==1.0.1 + # homeassistant.components.synology_dsm -python-synology==0.7.4 +python-synology==0.8.0 # homeassistant.components.tado python-tado==0.8.1 @@ -713,7 +725,7 @@ ring_doorbell==0.6.0 roku==4.1.0 # homeassistant.components.roomba -roombapy==1.5.1 +roombapy==1.5.3 # homeassistant.components.yamaha rxv==0.6.0 @@ -752,7 +764,7 @@ somecomfort==0.5.2 speak2mary==1.4.0 # homeassistant.components.spotify -spotipy==2.11.1 +spotipy==2.12.0 # homeassistant.components.recorder # homeassistant.components.sql @@ -839,7 +851,7 @@ xmltodict==0.12.0 ya_ma==0.3.8 # homeassistant.components.zeroconf -zeroconf==0.25.1 +zeroconf==0.26.0 # homeassistant.components.zha zha-quirks==0.0.38 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index e64e499baf734c..798377780ffcf4 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -7,5 +7,5 @@ flake8-docstrings==1.5.0 flake8==3.7.9 isort==4.3.21 pydocstyle==5.0.2 -pyupgrade==2.2.1 +pyupgrade==2.3.0 yamllint==1.23.0 diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 7ae2ae818a5143..9e326cb79658d9 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -21,7 +21,7 @@ def documentation_url(value: str) -> str: return value parsed_url = urlparse(value) - if not parsed_url.scheme == DOCUMENTATION_URL_SCHEMA: + if parsed_url.scheme != DOCUMENTATION_URL_SCHEMA: raise vol.Invalid("Documentation url is not prefixed with https") if parsed_url.netloc == DOCUMENTATION_URL_HOST and not parsed_url.path.startswith( DOCUMENTATION_URL_PATH_PREFIX @@ -46,6 +46,9 @@ def documentation_url(value: str) -> str: vol.Required("documentation"): vol.All( vol.Url(), documentation_url # pylint: disable=no-value-for-parameter ), + vol.Optional( + "issue_tracker" + ): vol.Url(), # pylint: disable=no-value-for-parameter vol.Optional("quality_scale"): vol.In(SUPPORTED_QUALITY_SCALES), vol.Optional("requirements"): [str], vol.Optional("dependencies"): [str], diff --git a/script/scaffold/generate.py b/script/scaffold/generate.py index 4317618ed52367..77317a20185ede 100644 --- a/script/scaffold/generate.py +++ b/script/scaffold/generate.py @@ -116,14 +116,22 @@ def _custom_tasks(template, info) -> None: title=info.name, config={ "step": { - "user": {"title": "Connect to the device", "data": {"host": "Host"}} + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]", + }, + } }, "error": { - "cannot_connect": "Failed to connect, please try again", - "invalid_auth": "Invalid authentication", - "unknown": "Unexpected error", + "cannot_connect": "[%key:common::config_flow::abort::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::abort::invalid_auth%]", + "unknown": "[%key:common::config_flow::abort::unknown%]", + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" }, - "abort": {"already_configured": "Device is already configured"}, }, ) @@ -133,11 +141,13 @@ def _custom_tasks(template, info) -> None: title=info.name, config={ "step": { - "confirm": {"description": f"Do you want to set up {info.name}?"} + "confirm": { + "description": "[%key:common::config_flow::description::confirm_setup%]", + } }, "abort": { - "single_instance_allowed": f"Only a single configuration of {info.name} is possible.", - "no_devices_found": f"No {info.name} devices found on the network.", + "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", }, }, ) @@ -148,13 +158,16 @@ def _custom_tasks(template, info) -> None: title=info.name, config={ "step": { - "pick_implementation": {"title": "Pick Authentication Method"} + "pick_implementation": { + "title": "[%key:common::config_flow::title::oauth2_pick_implementation%]" + } }, "abort": { - "missing_configuration": "The {info.name} component is not configured. Please follow the documentation." + "missing_configuration": "[%key:common::config_flow::abort::oauth2_missing_configuration%]", + "authorize_url_timeout": "[%key:common::config_flow::abort::oauth2_authorize_url_timeout%]", }, "create_entry": { - "default": f"Successfully authenticated with {info.name}." + "default": "[%key:common::config_flow::create_entry::authenticated%]" }, }, ) diff --git a/script/translations/migrate.py b/script/translations/migrate.py index 8081a4429555d9..9b0fce4765ebcb 100644 --- a/script/translations/migrate.py +++ b/script/translations/migrate.py @@ -327,12 +327,25 @@ def find_frontend_states(): def run(): """Migrate translations.""" # Import new common keys - migrate_project_keys_translations( + rename_keys( CORE_PROJECT_ID, - FRONTEND_PROJECT_ID, { - "common::state::off": "state::default::off", - "common::state::on": "state::default::on", + "component::netatmo::config::step::pick_implementation::title": "common::config_flow::title::oauth2_pick_implementation", + "component::doorbird::config::step::user::data::username": "common::config_flow::data::username", + "component::doorbird::config::step::user::data::password": "common::config_flow::data::password", + "component::adguard::config::step::user::data::host": "common::config_flow::data::host", + "component::adguard::config::step::user::data::port": "common::config_flow::data::port", + "component::zha::config::step::user::data::usb_path": "common::config_flow::data::usb_path", + "component::smartthings::config::step::pat::data::access_token": "common::config_flow::data::access_token", + "component::airvisual::config::step::geography::data::api_key": "common::config_flow::data::api_key", + "component::doorbird::config::error::invalid_auth": "common::config_flow::error::invalid_auth", + "component::airvisual::config::error::invalid_api_key": "common::config_flow::error::invalid_api_key", + "component::tibber::config::error::invalid_access_token": "common::config_flow::error::invalid_access_token", + "component::doorbird::config::error::unknown": "common::config_flow::error::unknown", + "component::life360::config::abort::user_already_configured": "common::config_flow::abort::already_configured_account", + "component::xiaomi_miio::config::abort::already_configured": "common::config_flow::abort::already_configured_device", + "component::netatmo::config::abort::missing_configuration": "common::config_flow::abort::oauth2_missing_configuration", + "component::netatmo::config::abort::authorize_url_timeout": "common::config_flow::abort::oauth2_authorize_url_timeout", }, ) diff --git a/setup.py b/setup.py index 0c56e89b67ce20..1473fd1f5f9277 100755 --- a/setup.py +++ b/setup.py @@ -43,10 +43,10 @@ "jinja2>=2.11.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. - "cryptography==2.9", + "cryptography==2.9.2", "pip>=8.0.3", "python-slugify==4.0.0", - "pytz>=2019.03", + "pytz>=2020.1", "pyyaml==5.3.1", "requests==2.23.0", "ruamel.yaml==0.15.100", diff --git a/tests/auth/mfa_modules/test_notify.py b/tests/auth/mfa_modules/test_notify.py index c79d76baf4fffb..65ee5d5d0c5baa 100644 --- a/tests/auth/mfa_modules/test_notify.py +++ b/tests/auth/mfa_modules/test_notify.py @@ -1,12 +1,12 @@ """Test the HMAC-based One Time Password (MFA) auth module.""" import asyncio -from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config from homeassistant.components.notify import NOTIFY_SERVICE_SCHEMA +from tests.async_mock import patch from tests.common import MockUser, async_mock_service MOCK_CODE = "123456" diff --git a/tests/auth/mfa_modules/test_totp.py b/tests/auth/mfa_modules/test_totp.py index d0a4f3cf3acaf9..b14b20297ebb91 100644 --- a/tests/auth/mfa_modules/test_totp.py +++ b/tests/auth/mfa_modules/test_totp.py @@ -1,11 +1,11 @@ """Test the Time-based One Time Password (MFA) auth module.""" import asyncio -from unittest.mock import patch from homeassistant import data_entry_flow from homeassistant.auth import auth_manager_from_config, models as auth_models from homeassistant.auth.mfa_modules import auth_mfa_module_from_config +from tests.async_mock import patch from tests.common import MockUser MOCK_CODE = "123456" diff --git a/tests/auth/test_init.py b/tests/auth/test_init.py index edcd01d51e1fc5..f303a59179ba41 100644 --- a/tests/auth/test_init.py +++ b/tests/auth/test_init.py @@ -1,6 +1,5 @@ """Tests for the Home Assistant auth module.""" from datetime import timedelta -from unittest.mock import Mock, patch import jwt import pytest @@ -12,6 +11,7 @@ from homeassistant.core import callback from homeassistant.util import dt as dt_util +from tests.async_mock import Mock, patch from tests.common import CLIENT_ID, MockUser, ensure_auth_manager_loaded, flush_store diff --git a/tests/components/abode/common.py b/tests/components/abode/common.py index aabc732daa2e6f..157a5441bb1045 100644 --- a/tests/components/abode/common.py +++ b/tests/components/abode/common.py @@ -1,10 +1,9 @@ """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.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/abode/test_alarm_control_panel.py b/tests/components/abode/test_alarm_control_panel.py index ca546157c933a5..d64b211f304c3a 100644 --- a/tests/components/abode/test_alarm_control_panel.py +++ b/tests/components/abode/test_alarm_control_panel.py @@ -1,6 +1,4 @@ """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 @@ -19,6 +17,8 @@ from .common import setup_platform +from tests.async_mock import PropertyMock, patch + DEVICE_ID = "alarm_control_panel.abode_alarm" diff --git a/tests/components/abode/test_camera.py b/tests/components/abode/test_camera.py index 8b11671a4560b8..0e843c59023290 100644 --- a/tests/components/abode/test_camera.py +++ b/tests/components/abode/test_camera.py @@ -1,12 +1,12 @@ """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 +from tests.async_mock import patch + async def test_entity_registry(hass): """Tests that the devices are registered in the entity registry.""" diff --git a/tests/components/abode/test_config_flow.py b/tests/components/abode/test_config_flow.py index d9762296e7081b..01508d412a2754 100644 --- a/tests/components/abode/test_config_flow.py +++ b/tests/components/abode/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for the Abode config flow.""" -from unittest.mock import patch - from abodepy.exceptions import AbodeAuthenticationException from homeassistant import data_entry_flow from homeassistant.components.abode import config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, HTTP_INTERNAL_SERVER_ERROR +from tests.async_mock import patch from tests.common import MockConfigEntry CONF_POLLING = "polling" diff --git a/tests/components/abode/test_cover.py b/tests/components/abode/test_cover.py index bb1b8fceffb4d6..b166ec5464aa4e 100644 --- a/tests/components/abode/test_cover.py +++ b/tests/components/abode/test_cover.py @@ -1,6 +1,4 @@ """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 ( @@ -13,6 +11,8 @@ from .common import setup_platform +from tests.async_mock import patch + DEVICE_ID = "cover.garage_door" diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 3f73ccd77ce455..1598e7bfa91aef 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,6 +1,4 @@ """Tests for the Abode module.""" -from unittest.mock import patch - from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -11,6 +9,8 @@ from .common import setup_platform +from tests.async_mock import patch + async def test_change_settings(hass): """Test change_setting service.""" diff --git a/tests/components/abode/test_light.py b/tests/components/abode/test_light.py index f0eee4b209bd62..6506746783c2c8 100644 --- a/tests/components/abode/test_light.py +++ b/tests/components/abode/test_light.py @@ -1,6 +1,4 @@ """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, @@ -19,6 +17,8 @@ from .common import setup_platform +from tests.async_mock import patch + DEVICE_ID = "light.living_room_lamp" diff --git a/tests/components/abode/test_lock.py b/tests/components/abode/test_lock.py index 45e17861d33216..6850eebe0cebb8 100644 --- a/tests/components/abode/test_lock.py +++ b/tests/components/abode/test_lock.py @@ -1,6 +1,4 @@ """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 ( @@ -13,6 +11,8 @@ from .common import setup_platform +from tests.async_mock import patch + DEVICE_ID = "lock.test_lock" diff --git a/tests/components/abode/test_switch.py b/tests/components/abode/test_switch.py index 3ec9648d87de0c..5c480b332254aa 100644 --- a/tests/components/abode/test_switch.py +++ b/tests/components/abode/test_switch.py @@ -1,6 +1,4 @@ """Tests for the Abode switch device.""" -from unittest.mock import patch - from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_TRIGGER_AUTOMATION, @@ -16,6 +14,8 @@ from .common import setup_platform +from tests.async_mock import patch + AUTOMATION_ID = "switch.test_automation" AUTOMATION_UID = "47fae27488f74f55b964a81a066c3a01" DEVICE_ID = "switch.test_switch" diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index 9186402787379e..0da4c4da6fd26a 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -4,6 +4,7 @@ from homeassistant.components.alexa import messages, smart_home import homeassistant.components.camera as camera +from homeassistant.components.cover import DEVICE_CLASS_GATE from homeassistant.components.media_player.const import ( SUPPORT_NEXT_TRACK, SUPPORT_PAUSE, @@ -2630,6 +2631,28 @@ async def test_cover_garage_door(hass): ) +async def test_cover_gate(hass): + """Test gate cover discovery.""" + device = ( + "cover.test_gate", + "off", + { + "friendly_name": "Test cover gate", + "supported_features": 3, + "device_class": DEVICE_CLASS_GATE, + }, + ) + appliance = await discovery_test(device, hass) + + assert appliance["endpointId"] == "cover#test_gate" + assert appliance["displayCategories"][0] == "GARAGE_DOOR" + assert appliance["friendlyName"] == "Test cover gate" + + assert_endpoint_capabilities( + appliance, "Alexa.ModeController", "Alexa.EndpointHealth", "Alexa" + ) + + async def test_cover_position_mode(hass): """Test cover discovery and position using modeController.""" device = ( diff --git a/tests/components/androidtv/patchers.py b/tests/components/androidtv/patchers.py index c49b6ad11e9c07..85c80dd0b1c426 100644 --- a/tests/components/androidtv/patchers.py +++ b/tests/components/androidtv/patchers.py @@ -1,6 +1,6 @@ """Define patches used for androidtv tests.""" -from unittest.mock import mock_open, patch +from tests.async_mock import mock_open, patch class AdbDeviceTcpFake: diff --git a/tests/components/androidtv/test_media_player.py b/tests/components/androidtv/test_media_player.py index c9f2c271000a85..fa4f6ffbed69ac 100644 --- a/tests/components/androidtv/test_media_player.py +++ b/tests/components/androidtv/test_media_player.py @@ -1,7 +1,6 @@ """The tests for the androidtv platform.""" import base64 import logging -from unittest.mock import patch from androidtv.exceptions import LockNotAcquiredException @@ -41,6 +40,8 @@ from . import patchers +from tests.async_mock import patch + # Android TV device with Python ADB implementation CONFIG_ANDROIDTV_PYTHON_ADB = { DOMAIN: { diff --git a/tests/components/api/test_init.py b/tests/components/api/test_init.py index e73b65661c9222..1c93158ec0378c 100644 --- a/tests/components/api/test_init.py +++ b/tests/components/api/test_init.py @@ -1,7 +1,6 @@ """The tests for the Home Assistant API component.""" # pylint: disable=protected-access import json -from unittest.mock import patch from aiohttp import web import pytest @@ -12,6 +11,7 @@ import homeassistant.core as ha from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/apns/test_notify.py b/tests/components/apns/test_notify.py index 61092899e248d9..5c69e19435e803 100644 --- a/tests/components/apns/test_notify.py +++ b/tests/components/apns/test_notify.py @@ -1,7 +1,6 @@ """The tests for the APNS component.""" import io import unittest -from unittest.mock import Mock, mock_open, patch from apns2.errors import Unregistered import yaml @@ -11,6 +10,7 @@ from homeassistant.core import State from homeassistant.setup import setup_component +from tests.async_mock import Mock, mock_open, patch from tests.common import assert_setup_component, get_test_home_assistant CONFIG = { diff --git a/tests/components/apprise/test_notify.py b/tests/components/apprise/test_notify.py index 8135f4e8e2c2e5..125971016cbfd9 100644 --- a/tests/components/apprise/test_notify.py +++ b/tests/components/apprise/test_notify.py @@ -1,8 +1,8 @@ """The tests for the apprise notification platform.""" -from unittest.mock import MagicMock, patch - from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock, patch + BASE_COMPONENT = "notify" diff --git a/tests/components/aprs/test_device_tracker.py b/tests/components/aprs/test_device_tracker.py index dc0cf09f28d2c2..95cdf4befec1c1 100644 --- a/tests/components/aprs/test_device_tracker.py +++ b/tests/components/aprs/test_device_tracker.py @@ -1,11 +1,10 @@ """Test APRS device tracker.""" -from unittest.mock import Mock, patch - import aprslib import homeassistant.components.aprs.device_tracker as device_tracker from homeassistant.const import EVENT_HOMEASSISTANT_START +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant DEFAULT_PORT = 14580 diff --git a/tests/components/arlo/test_sensor.py b/tests/components/arlo/test_sensor.py index 15b85959ff057a..e75db4a57dd84c 100644 --- a/tests/components/arlo/test_sensor.py +++ b/tests/components/arlo/test_sensor.py @@ -1,6 +1,5 @@ """The tests for the Netgear Arlo sensors.""" from collections import namedtuple -from unittest.mock import MagicMock, patch import pytest @@ -12,6 +11,8 @@ UNIT_PERCENTAGE, ) +from tests.async_mock import patch + def _get_named_tuple(input_dict): return namedtuple("Struct", input_dict.keys())(*input_dict.values()) @@ -94,7 +95,7 @@ def sensor_with_hass_data(default_sensor, hass): def mock_dispatch(): """Mock the dispatcher connect method.""" target = "homeassistant.components.arlo.sensor.async_dispatcher_connect" - with patch(target, MagicMock()) as _mock: + with patch(target) as _mock: yield _mock diff --git a/tests/components/atag/test_config_flow.py b/tests/components/atag/test_config_flow.py index c860b85c240923..9885c9081dd3ac 100644 --- a/tests/components/atag/test_config_flow.py +++ b/tests/components/atag/test_config_flow.py @@ -1,13 +1,11 @@ """Tests for the Atag config flow.""" -from unittest.mock import PropertyMock - from pyatag import AtagException from homeassistant import config_entries, data_entry_flow from homeassistant.components.atag import DOMAIN from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_PORT -from tests.async_mock import patch +from tests.async_mock import PropertyMock, patch from tests.common import MockConfigEntry FIXTURE_USER_INPUT = { diff --git a/tests/components/august/test_gateway.py b/tests/components/august/test_gateway.py index b9d1959d18a687..ec035b9ec38bac 100644 --- a/tests/components/august/test_gateway.py +++ b/tests/components/august/test_gateway.py @@ -1,10 +1,8 @@ """The gateway tests for the august platform.""" -from unittest.mock import MagicMock - from homeassistant.components.august.const import DOMAIN from homeassistant.components.august.gateway import AugustGateway -from tests.async_mock import patch +from tests.async_mock import MagicMock, patch from tests.components.august.mocks import _mock_august_authentication, _mock_get_config diff --git a/tests/components/auth/test_init.py b/tests/components/auth/test_init.py index 2c9a39c6fb62f0..3d799fe0078508 100644 --- a/tests/components/auth/test_init.py +++ b/tests/components/auth/test_init.py @@ -1,6 +1,5 @@ """Integration tests for the auth component.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.auth.models import Credentials from homeassistant.components import auth @@ -10,6 +9,7 @@ from . import async_setup_auth +from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI, MockUser diff --git a/tests/components/auth/test_login_flow.py b/tests/components/auth/test_login_flow.py index e6e5281d601ffa..f2629a27bb990c 100644 --- a/tests/components/auth/test_login_flow.py +++ b/tests/components/auth/test_login_flow.py @@ -1,8 +1,7 @@ """Tests for the login flow.""" -from unittest.mock import patch - from . import async_setup_auth +from tests.async_mock import patch from tests.common import CLIENT_ID, CLIENT_REDIRECT_URI diff --git a/tests/components/automation/test_homeassistant.py b/tests/components/automation/test_homeassistant.py index b0db66e16a96bb..d7bdfbeef3e87f 100644 --- a/tests/components/automation/test_homeassistant.py +++ b/tests/components/automation/test_homeassistant.py @@ -1,11 +1,9 @@ """The tests for the Event automation.""" -from unittest.mock import patch - import homeassistant.components.automation as automation from homeassistant.core import CoreState from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, patch from tests.common import async_mock_service diff --git a/tests/components/automation/test_init.py b/tests/components/automation/test_init.py index a039604525f967..7a082ba1931b90 100644 --- a/tests/components/automation/test_init.py +++ b/tests/components/automation/test_init.py @@ -1,6 +1,5 @@ """The tests for the automation component.""" from datetime import timedelta -from unittest.mock import Mock, patch import pytest @@ -19,6 +18,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/automation/test_numeric_state.py b/tests/components/automation/test_numeric_state.py index 1173de4b02ae88..46b774c1cc9779 100644 --- a/tests/components/automation/test_numeric_state.py +++ b/tests/components/automation/test_numeric_state.py @@ -1,6 +1,5 @@ """The tests for numeric state automation.""" from datetime import timedelta -from unittest.mock import patch import pytest import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/automation/test_state.py b/tests/components/automation/test_state.py index 033ce44e5a43c8..0d591c967bed0e 100644 --- a/tests/components/automation/test_state.py +++ b/tests/components/automation/test_state.py @@ -1,6 +1,5 @@ """The test for state automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -9,6 +8,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/automation/test_sun.py b/tests/components/automation/test_sun.py index 4cb2672ab64de2..4efb19ff2015bb 100644 --- a/tests/components/automation/test_sun.py +++ b/tests/components/automation/test_sun.py @@ -1,6 +1,5 @@ """The tests for the sun automation.""" from datetime import datetime -from unittest.mock import patch import pytest @@ -10,6 +9,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed, async_mock_service, mock_component from tests.components.automation import common diff --git a/tests/components/automation/test_time.py b/tests/components/automation/test_time.py index ec8d504652bcf5..0ba85467fcd0c8 100644 --- a/tests/components/automation/test_time.py +++ b/tests/components/automation/test_time.py @@ -1,6 +1,5 @@ """The tests for the time automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -8,6 +7,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, async_fire_time_changed, diff --git a/tests/components/axis/test_init.py b/tests/components/axis/test_init.py index 5803f31d5260d3..b8baf18a67d37e 100644 --- a/tests/components/axis/test_init.py +++ b/tests/components/axis/test_init.py @@ -1,12 +1,10 @@ """Test Axis component setup process.""" -from unittest.mock import Mock, patch - from homeassistant.components import axis from homeassistant.setup import async_setup_component from .test_device import MAC, setup_axis_integration -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, Mock, patch from tests.common import MockConfigEntry diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index 844cfedf7fe927..d8d69265f3a25b 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -1,13 +1,13 @@ """Axis switch platform tests.""" -from unittest.mock import Mock, call as mock_call - from homeassistant.components import axis import homeassistant.components.switch as switch from homeassistant.setup import async_setup_component from .test_device import NAME, setup_axis_integration +from tests.async_mock import Mock, call as mock_call + EVENTS = [ { "operation": "Initialized", diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index 1ac24e037024c2..968b54b7892ce6 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -1,6 +1,5 @@ """The test for binary_sensor device automation.""" from datetime import timedelta -from unittest.mock import patch import pytest @@ -12,6 +11,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( MockConfigEntry, async_get_device_automation_capabilities, diff --git a/tests/components/blebox/__init__.py b/tests/components/blebox/__init__.py new file mode 100644 index 00000000000000..00afae7ad287b2 --- /dev/null +++ b/tests/components/blebox/__init__.py @@ -0,0 +1 @@ +"""Tests for the blebox component.""" diff --git a/tests/components/blebox/conftest.py b/tests/components/blebox/conftest.py new file mode 100644 index 00000000000000..23488181203669 --- /dev/null +++ b/tests/components/blebox/conftest.py @@ -0,0 +1,87 @@ +"""PyTest fixtures and test helpers.""" + +from unittest import mock + +import blebox_uniapi +import pytest + +from homeassistant.components.blebox.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.setup import async_setup_component + +from tests.async_mock import AsyncMock, PropertyMock, patch +from tests.common import MockConfigEntry + + +def patch_product_identify(path=None, **kwargs): + """Patch the blebox_uniapi Products class.""" + if path is None: + path = "homeassistant.components.blebox.Products" + patcher = patch(path, mock.DEFAULT, blebox_uniapi.products.Products, True, True) + products_class = patcher.start() + products_class.async_from_host = AsyncMock(**kwargs) + return products_class + + +def setup_product_mock(category, feature_mocks, path=None): + """Mock a product returning the given features.""" + + product_mock = mock.create_autospec( + blebox_uniapi.box.Box, True, True, features=None + ) + type(product_mock).features = PropertyMock(return_value={category: feature_mocks}) + + for feature in feature_mocks: + type(feature).product = PropertyMock(return_value=product_mock) + + patch_product_identify(path, return_value=product_mock) + return product_mock + + +def mock_only_feature(spec, **kwargs): + """Mock just the feature, without the product setup.""" + return mock.create_autospec(spec, True, True, **kwargs) + + +def mock_feature(category, spec, **kwargs): + """Mock a feature along with whole product setup.""" + feature_mock = mock_only_feature(spec, **kwargs) + feature_mock.async_update = AsyncMock() + product = setup_product_mock(category, [feature_mock]) + + type(feature_mock.product).name = PropertyMock(return_value="Some name") + type(feature_mock.product).type = PropertyMock(return_value="some type") + type(feature_mock.product).model = PropertyMock(return_value="some model") + type(feature_mock.product).brand = PropertyMock(return_value="BleBox") + type(feature_mock.product).firmware_version = PropertyMock(return_value="1.23") + type(feature_mock.product).unique_id = PropertyMock(return_value="abcd0123ef5678") + type(feature_mock).product = PropertyMock(return_value=product) + return feature_mock + + +def mock_config(ip_address="172.100.123.4"): + """Return a Mock of the HA entity config.""" + return MockConfigEntry(domain=DOMAIN, data={CONF_HOST: ip_address, CONF_PORT: 80}) + + +@pytest.fixture(name="config") +def config_fixture(): + """Create hass config fixture.""" + return {DOMAIN: {CONF_HOST: "172.100.123.4", CONF_PORT: 80}} + + +@pytest.fixture +def wrapper(request): + """Return an entity wrapper from given fixture name.""" + return request.getfixturevalue(request.param) + + +async def async_setup_entity(hass, config, entity_id): + """Return a configured entity with the given entity_id.""" + config_entry = mock_config() + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + return entity_registry.async_get(entity_id) diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py new file mode 100644 index 00000000000000..bcb91b8c587a83 --- /dev/null +++ b/tests/components/blebox/test_config_flow.py @@ -0,0 +1,192 @@ +"""Test Home Assistant config flow for BleBox devices.""" + +import blebox_uniapi +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.blebox import config_flow +from homeassistant.setup import async_setup_component + +from .conftest import mock_config, mock_only_feature, setup_product_mock + +from tests.async_mock import DEFAULT, AsyncMock, PropertyMock, patch + + +def create_valid_feature_mock(path="homeassistant.components.blebox.Products"): + """Return a valid, complete BleBox feature mock.""" + feature = mock_only_feature( + blebox_uniapi.cover.Cover, + unique_id="BleBox-gateBox-1afe34db9437-0.position", + full_name="gateBox-0.position", + device_class="gate", + state=0, + async_update=AsyncMock(), + current=None, + ) + + product = setup_product_mock("covers", [feature], path) + + type(product).name = PropertyMock(return_value="My gate controller") + type(product).model = PropertyMock(return_value="gateController") + type(product).type = PropertyMock(return_value="gateBox") + type(product).brand = PropertyMock(return_value="BleBox") + type(product).firmware_version = PropertyMock(return_value="1.23") + type(product).unique_id = PropertyMock(return_value="abcd0123ef5678") + + return feature + + +@pytest.fixture +def valid_feature_mock(): + """Return a valid, complete BleBox feature mock.""" + return create_valid_feature_mock() + + +@pytest.fixture +def flow_feature_mock(): + """Return a mocked user flow feature.""" + return create_valid_feature_mock( + "homeassistant.components.blebox.config_flow.Products" + ) + + +async def test_flow_works(hass, flow_feature_mock): + """Test that config flow works.""" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == "My gate controller" + assert result["data"] == { + config_flow.CONF_HOST: "172.2.3.4", + config_flow.CONF_PORT: 80, + } + + +@pytest.fixture +def product_class_mock(): + """Return a mocked feature.""" + path = "homeassistant.components.blebox.config_flow.Products" + patcher = patch(path, DEFAULT, blebox_uniapi.products.Products, True, True) + yield patcher + + +async def test_flow_with_connection_failure(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock( + side_effect=blebox_uniapi.error.ConnectionError + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_with_api_failure(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock( + side_effect=blebox_uniapi.error.Error + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_flow_with_unknown_failure(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock(side_effect=RuntimeError) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "unknown"} + + +async def test_flow_with_unsupported_version(hass, product_class_mock): + """Test that config flow works.""" + with product_class_mock as products_class: + products_class.async_from_host = AsyncMock( + side_effect=blebox_uniapi.error.UnsupportedBoxVersion + ) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["errors"] == {"base": "unsupported_version"} + + +async def test_async_setup(hass): + """Test async_setup (for coverage).""" + assert await async_setup_component(hass, "blebox", {"host": "172.2.3.4"}) + await hass.async_block_till_done() + + +async def test_already_configured(hass, valid_feature_mock): + """Test that same device cannot be added twice.""" + + config = mock_config("172.2.3.4") + config.add_to_hass(hass) + + await hass.config_entries.async_setup(config.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={config_flow.CONF_HOST: "172.2.3.4", config_flow.CONF_PORT: 80}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "address_already_configured" + + +async def test_async_setup_entry(hass, valid_feature_mock): + """Test async_setup_entry (for coverage).""" + + config = mock_config() + config.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config.entry_id) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries() == [config] + assert config.state == config_entries.ENTRY_STATE_LOADED + + +async def test_async_remove_entry(hass, valid_feature_mock): + """Test async_setup_entry (for coverage).""" + + config = mock_config() + config.add_to_hass(hass) + + assert await hass.config_entries.async_setup(config.entry_id) + await hass.async_block_till_done() + + assert await hass.config_entries.async_remove(config.entry_id) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries() == [] + assert config.state == config_entries.ENTRY_STATE_NOT_LOADED diff --git a/tests/components/blebox/test_cover.py b/tests/components/blebox/test_cover.py new file mode 100644 index 00000000000000..5b2e13dcf23378 --- /dev/null +++ b/tests/components/blebox/test_cover.py @@ -0,0 +1,381 @@ +"""BleBox cover entities tests.""" + +import blebox_uniapi +import pytest + +from homeassistant.components.cover import ( + ATTR_CURRENT_POSITION, + ATTR_POSITION, + DEVICE_CLASS_DOOR, + DEVICE_CLASS_GATE, + DEVICE_CLASS_SHUTTER, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + SUPPORT_CLOSE, + SUPPORT_OPEN, + SUPPORT_SET_POSITION, + SUPPORT_STOP, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_SUPPORTED_FEATURES, + SERVICE_CLOSE_COVER, + SERVICE_OPEN_COVER, + SERVICE_SET_COVER_POSITION, + SERVICE_STOP_COVER, + STATE_UNKNOWN, +) + +from .conftest import async_setup_entity, mock_feature + +from tests.async_mock import ANY, AsyncMock, PropertyMock, call, patch + +ALL_COVER_FIXTURES = ["gatecontroller", "shutterbox", "gatebox"] +FIXTURES_SUPPORTING_STOP = ["gatecontroller", "shutterbox"] + + +@pytest.fixture(name="shutterbox") +def shutterbox_fixture(): + """Return a shutterBox fixture.""" + feature = mock_feature( + "covers", + blebox_uniapi.cover.Cover, + unique_id="BleBox-shutterBox-2bee34e750b8-position", + full_name="shutterBox-position", + device_class="shutter", + current=None, + state=None, + has_stop=True, + is_slider=True, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My shutter") + type(product).model = PropertyMock(return_value="shutterBox") + return (feature, "cover.shutterbox_position") + + +@pytest.fixture(name="gatebox") +def gatebox_fixture(): + """Return a gateBox fixture.""" + feature = mock_feature( + "covers", + blebox_uniapi.cover.Cover, + unique_id="BleBox-gateBox-1afe34db9437-position", + device_class="gatebox", + full_name="gateBox-position", + current=None, + state=None, + has_stop=False, + is_slider=False, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My gatebox") + type(product).model = PropertyMock(return_value="gateBox") + return (feature, "cover.gatebox_position") + + +@pytest.fixture(name="gatecontroller") +def gate_fixture(): + """Return a gateController fixture.""" + feature = mock_feature( + "covers", + blebox_uniapi.cover.Cover, + unique_id="BleBox-gateController-2bee34e750b8-position", + full_name="gateController-position", + device_class="gate", + current=None, + state=None, + has_stop=True, + is_slider=True, + ) + product = feature.product + type(product).name = PropertyMock(return_value="My gate controller") + type(product).model = PropertyMock(return_value="gateController") + return (feature, "cover.gatecontroller_position") + + +async def test_init_gatecontroller(gatecontroller, hass, config): + """Test gateController default state.""" + + _, entity_id = gatecontroller + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-gateController-2bee34e750b8-position" + + state = hass.states.get(entity_id) + assert state.name == "gateController-position" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_GATE + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_OPEN + assert supported_features & SUPPORT_CLOSE + assert supported_features & SUPPORT_STOP + + assert supported_features & SUPPORT_SET_POSITION + assert ATTR_CURRENT_POSITION not in state.attributes + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My gate controller" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "gateController" + assert device.sw_version == "1.23" + + +async def test_init_shutterbox(shutterbox, hass, config): + """Test gateBox default state.""" + + _, entity_id = shutterbox + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-shutterBox-2bee34e750b8-position" + + state = hass.states.get(entity_id) + assert state.name == "shutterBox-position" + assert entry.device_class == DEVICE_CLASS_SHUTTER + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_OPEN + assert supported_features & SUPPORT_CLOSE + assert supported_features & SUPPORT_STOP + + assert supported_features & SUPPORT_SET_POSITION + assert ATTR_CURRENT_POSITION not in state.attributes + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My shutter" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "shutterBox" + assert device.sw_version == "1.23" + + +async def test_init_gatebox(gatebox, hass, config): + """Test cover default state.""" + + _, entity_id = gatebox + entry = await async_setup_entity(hass, config, entity_id) + assert entry.unique_id == "BleBox-gateBox-1afe34db9437-position" + + state = hass.states.get(entity_id) + assert state.name == "gateBox-position" + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_DOOR + + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_OPEN + assert supported_features & SUPPORT_CLOSE + + # Not available during init since requires fetching state to detect + assert not supported_features & SUPPORT_STOP + + assert not supported_features & SUPPORT_SET_POSITION + assert ATTR_CURRENT_POSITION not in state.attributes + assert state.state == STATE_UNKNOWN + + device_registry = await hass.helpers.device_registry.async_get_registry() + device = device_registry.async_get(entry.device_id) + + assert device.name == "My gatebox" + assert device.identifiers == {("blebox", "abcd0123ef5678")} + assert device.manufacturer == "BleBox" + assert device.model == "gateBox" + assert device.sw_version == "1.23" + + +@pytest.mark.parametrize("wrapper", ALL_COVER_FIXTURES, indirect=["wrapper"]) +async def test_open(wrapper, hass, config): + """Test cover opening.""" + + feature_mock, entity_id = wrapper + + def initial_update(): + feature_mock.state = 3 # manually stopped + + def open_gate(): + feature_mock.state = 1 # opening + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_open = AsyncMock(side_effect=open_gate) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_CLOSED + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", SERVICE_OPEN_COVER, {"entity_id": entity_id}, blocking=True, + ) + assert hass.states.get(entity_id).state == STATE_OPENING + + +@pytest.mark.parametrize("wrapper", ALL_COVER_FIXTURES, indirect=["wrapper"]) +async def test_close(wrapper, hass, config): + """Test cover closing.""" + + feature_mock, entity_id = wrapper + + def initial_update(): + feature_mock.state = 4 # open + + def close(): + feature_mock.state = 0 # closing + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_close = AsyncMock(side_effect=close) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_OPEN + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", SERVICE_CLOSE_COVER, {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_CLOSING + + +def opening_to_stop_feature_mock(feature_mock): + """Return an mocked feature which can be updated and stopped.""" + + def initial_update(): + feature_mock.state = 1 # opening + + def stop(): + feature_mock.state = 2 # manually stopped + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_stop = AsyncMock(side_effect=stop) + + +@pytest.mark.parametrize("wrapper", FIXTURES_SUPPORTING_STOP, indirect=["wrapper"]) +async def test_stop(wrapper, hass, config): + """Test cover stopping.""" + + feature_mock, entity_id = wrapper + opening_to_stop_feature_mock(feature_mock) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_OPENING + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", SERVICE_STOP_COVER, {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OPEN + + +@pytest.mark.parametrize("wrapper", ALL_COVER_FIXTURES, indirect=["wrapper"]) +async def test_update(wrapper, hass, config): + """Test cover updating.""" + + feature_mock, entity_id = wrapper + + def initial_update(): + feature_mock.current = 29 # inverted + feature_mock.state = 2 # manually stopped + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.attributes[ATTR_CURRENT_POSITION] == 71 # 100 - 29 + assert state.state == STATE_OPEN + + +@pytest.mark.parametrize( + "wrapper", ["gatecontroller", "shutterbox"], indirect=["wrapper"] +) +async def test_set_position(wrapper, hass, config): + """Test cover position setting.""" + + feature_mock, entity_id = wrapper + + def initial_update(): + feature_mock.state = 3 # closed + + def set_position(position): + assert position == 99 # inverted + feature_mock.state = 1 # opening + # feature_mock.current = position + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + feature_mock.async_set_position = AsyncMock(side_effect=set_position) + + await async_setup_entity(hass, config, entity_id) + assert hass.states.get(entity_id).state == STATE_CLOSED + + feature_mock.async_update = AsyncMock() + await hass.services.async_call( + "cover", + SERVICE_SET_COVER_POSITION, + {"entity_id": entity_id, ATTR_POSITION: 1}, + blocking=True, + ) # almost closed + assert hass.states.get(entity_id).state == STATE_OPENING + + +async def test_unknown_position(shutterbox, hass, config): + """Test cover position setting.""" + + feature_mock, entity_id = shutterbox + + def initial_update(): + feature_mock.state = 4 # opening + feature_mock.current = -1 + + feature_mock.async_update = AsyncMock(side_effect=initial_update) + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + assert state.state == STATE_OPEN + assert ATTR_CURRENT_POSITION not in state.attributes + + +async def test_with_stop(gatebox, hass, config): + """Test stop capability is available.""" + + feature_mock, entity_id = gatebox + opening_to_stop_feature_mock(feature_mock) + feature_mock.has_stop = True + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert supported_features & SUPPORT_STOP + + +async def test_with_no_stop(gatebox, hass, config): + """Test stop capability is not available.""" + + feature_mock, entity_id = gatebox + opening_to_stop_feature_mock(feature_mock) + feature_mock.has_stop = False + + await async_setup_entity(hass, config, entity_id) + + state = hass.states.get(entity_id) + supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] + assert not supported_features & SUPPORT_STOP + + +@pytest.mark.parametrize("wrapper", ALL_COVER_FIXTURES, indirect=["wrapper"]) +async def test_update_failure(wrapper, hass, config): + """Test that update failures are logged.""" + + feature_mock, entity_id = wrapper + + feature_mock.async_update = AsyncMock(side_effect=blebox_uniapi.error.ClientError) + name = feature_mock.full_name + + with patch("homeassistant.components.blebox._LOGGER.error") as error: + await async_setup_entity(hass, config, entity_id) + + error.assert_has_calls([call("Updating '%s' failed: %s", name, ANY)]) + assert isinstance(error.call_args[0][2], blebox_uniapi.error.ClientError) diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py new file mode 100644 index 00000000000000..098c10f2cfce53 --- /dev/null +++ b/tests/components/blebox/test_init.py @@ -0,0 +1,60 @@ +"""BleBox devices setup tests.""" + +import logging + +import blebox_uniapi + +from homeassistant.components.blebox.const import DOMAIN +from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED, ENTRY_STATE_SETUP_RETRY + +from .conftest import mock_config, patch_product_identify + + +async def test_setup_failure(hass, caplog): + """Test that setup failure is handled and logged.""" + + patch_product_identify(None, side_effect=blebox_uniapi.error.ClientError) + + entry = mock_config() + entry.add_to_hass(hass) + + caplog.set_level(logging.ERROR) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "Identify failed at 172.100.123.4:80 ()" in caplog.text + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_setup_failure_on_connection(hass, caplog): + """Test that setup failure is handled and logged.""" + + patch_product_identify(None, side_effect=blebox_uniapi.error.ConnectionError) + + entry = mock_config() + entry.add_to_hass(hass) + + caplog.set_level(logging.ERROR) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "Identify failed at 172.100.123.4:80 ()" in caplog.text + assert entry.state == ENTRY_STATE_SETUP_RETRY + + +async def test_unload_config_entry(hass): + """Test that unloading works properly.""" + patch_product_identify(None) + + entry = mock_config() + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert hass.data[DOMAIN] + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + assert not hass.data.get(DOMAIN) + + assert entry.state == ENTRY_STATE_NOT_LOADED diff --git a/tests/components/bluetooth_le_tracker/test_device_tracker.py b/tests/components/bluetooth_le_tracker/test_device_tracker.py index 308371c9aaaa9e..af4df4633391d8 100644 --- a/tests/components/bluetooth_le_tracker/test_device_tracker.py +++ b/tests/components/bluetooth_le_tracker/test_device_tracker.py @@ -1,7 +1,6 @@ """Test Bluetooth LE device tracker.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.components.bluetooth_le_tracker import device_tracker from homeassistant.components.device_tracker.const import ( @@ -13,6 +12,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt as dt_util, slugify +from tests.async_mock import patch from tests.common import async_fire_time_changed diff --git a/tests/components/bom/test_sensor.py b/tests/components/bom/test_sensor.py index 7a9daa26c2b6b2..6e85dbca1cd125 100644 --- a/tests/components/bom/test_sensor.py +++ b/tests/components/bom/test_sensor.py @@ -2,7 +2,6 @@ import json import re import unittest -from unittest.mock import patch from urllib.parse import urlparse import requests @@ -11,6 +10,7 @@ from homeassistant.components.bom.sensor import BOMCurrentData from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { diff --git a/tests/components/broadlink/test_init.py b/tests/components/broadlink/test_init.py index 1bdad193f52549..c5477dff49f2ee 100644 --- a/tests/components/broadlink/test_init.py +++ b/tests/components/broadlink/test_init.py @@ -1,7 +1,6 @@ """The tests for the broadlink component.""" from base64 import b64decode from datetime import timedelta -from unittest.mock import MagicMock, call, patch import pytest @@ -9,6 +8,8 @@ from homeassistant.components.broadlink.const import DOMAIN, SERVICE_LEARN, SERVICE_SEND from homeassistant.util.dt import utcnow +from tests.async_mock import MagicMock, call, patch + DUMMY_IR_PACKET = ( "JgBGAJKVETkRORA6ERQRFBEUERQRFBE5ETkQOhAVEBUQFREUEBUQ" "OhEUERQRORE5EBURFBA6EBUQOhE5EBUQFRA6EDoRFBEADQUAAA==" diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index 8e88c6e564e988..1b6c21c7358376 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -1,6 +1,5 @@ """The tests for the webdav calendar component.""" import datetime -from unittest.mock import MagicMock, Mock from caldav.objects import Event import pytest @@ -9,7 +8,7 @@ from homeassistant.setup import async_setup_component from homeassistant.util import dt -from tests.async_mock import patch +from tests.async_mock import MagicMock, Mock, patch # pylint: disable=redefined-outer-name diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 401350ec93cf41..3eaef6575ac2f0 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -2,7 +2,6 @@ import asyncio import base64 import io -from unittest.mock import PropertyMock, mock_open import pytest @@ -14,7 +13,7 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component -from tests.async_mock import patch +from tests.async_mock import PropertyMock, mock_open, patch from tests.components.camera import common diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index 819d1ce0e90715..a3f6fbd7e2d8e3 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -1,10 +1,10 @@ """The tests for the Canary component.""" import unittest -from unittest.mock import MagicMock, PropertyMock, patch from homeassistant import setup import homeassistant.components.canary as canary +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import get_test_home_assistant diff --git a/tests/components/canary/test_sensor.py b/tests/components/canary/test_sensor.py index 1d559dbb7ba613..5a4a82ccc5a170 100644 --- a/tests/components/canary/test_sensor.py +++ b/tests/components/canary/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Canary sensor platform.""" import copy import unittest -from unittest.mock import Mock from homeassistant.components.canary import DATA_CANARY, sensor as canary from homeassistant.components.canary.sensor import ( @@ -14,6 +13,7 @@ ) from homeassistant.const import TEMP_CELSIUS, UNIT_PERCENTAGE +from tests.async_mock import Mock from tests.common import get_test_home_assistant from tests.components.canary.test_init import mock_device, mock_location diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 2ec02da766960a..50685d5f3e7480 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -1,8 +1,7 @@ """Test Home Assistant Cast.""" -from unittest.mock import Mock, patch - from homeassistant.components.cast import home_assistant_cast +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry, async_mock_signal diff --git a/tests/components/climate/test_init.py b/tests/components/climate/test_init.py index b5ec45d3aa056f..e42bf8c7e3cd76 100644 --- a/tests/components/climate/test_init.py +++ b/tests/components/climate/test_init.py @@ -1,6 +1,5 @@ """The tests for the climate component.""" from typing import List -from unittest.mock import MagicMock import pytest import voluptuous as vol @@ -13,6 +12,7 @@ ClimateEntity, ) +from tests.async_mock import MagicMock from tests.common import async_mock_service diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 4755d470418fad..02d9b4c41aab33 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -1,6 +1,4 @@ """Fixtures for cloud tests.""" -from unittest.mock import patch - import jwt import pytest @@ -8,6 +6,8 @@ from . import mock_cloud, mock_cloud_prefs +from tests.async_mock import patch + @pytest.fixture(autouse=True) def mock_user_data(): diff --git a/tests/components/cloud/test_binary_sensor.py b/tests/components/cloud/test_binary_sensor.py index 38c3067873fa1a..6a2d76dc4034ab 100644 --- a/tests/components/cloud/test_binary_sensor.py +++ b/tests/components/cloud/test_binary_sensor.py @@ -1,10 +1,8 @@ """Tests for the cloud binary sensor.""" -from unittest.mock import Mock - from homeassistant.components.cloud.const import DISPATCHER_REMOTE_UPDATE from homeassistant.setup import async_setup_component -from tests.async_mock import patch +from tests.async_mock import Mock, patch async def test_remote_connection_sensor(hass): diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index 9866270473de0f..3808f9b179cb23 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -1,6 +1,4 @@ """Test the Cloud Google Config.""" -from unittest.mock import Mock - from homeassistant.components.cloud import GACTIONS_SCHEMA from homeassistant.components.cloud.google_config import CloudGoogleConfig from homeassistant.components.google_assistant import helpers as ga_helpers @@ -9,7 +7,7 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED from homeassistant.util.dt import utcnow -from tests.async_mock import AsyncMock, patch +from tests.async_mock import AsyncMock, Mock, patch from tests.common import async_fire_time_changed diff --git a/tests/components/cloud/test_prefs.py b/tests/components/cloud/test_prefs.py index d1b6f9ed867dab..4f2d5d6d6610e2 100644 --- a/tests/components/cloud/test_prefs.py +++ b/tests/components/cloud/test_prefs.py @@ -1,9 +1,9 @@ """Test Cloud preferences.""" -from unittest.mock import patch - from homeassistant.auth.const import GROUP_ID_ADMIN from homeassistant.components.cloud.prefs import STORAGE_KEY, CloudPreferences +from tests.async_mock import patch + async def test_set_username(hass): """Test we clear config if we set different username.""" diff --git a/tests/components/coinmarketcap/test_sensor.py b/tests/components/coinmarketcap/test_sensor.py index 9d1e89fbc24370..8997bc4a5d605a 100644 --- a/tests/components/coinmarketcap/test_sensor.py +++ b/tests/components/coinmarketcap/test_sensor.py @@ -1,11 +1,11 @@ """Tests for the CoinMarketCap sensor platform.""" import json import unittest -from unittest.mock import patch import homeassistant.components.sensor as sensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, load_fixture VALID_CONFIG = { diff --git a/tests/components/command_line/test_notify.py b/tests/components/command_line/test_notify.py index 0cb7e9293e95cb..f20011d4482f58 100644 --- a/tests/components/command_line/test_notify.py +++ b/tests/components/command_line/test_notify.py @@ -2,11 +2,11 @@ import os import tempfile import unittest -from unittest.mock import patch import homeassistant.components.notify as notify from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/command_line/test_sensor.py b/tests/components/command_line/test_sensor.py index b923be81888eda..bbf69dc73a0b51 100644 --- a/tests/components/command_line/test_sensor.py +++ b/tests/components/command_line/test_sensor.py @@ -1,10 +1,10 @@ """The tests for the Command line sensor platform.""" import unittest -from unittest.mock import patch from homeassistant.components.command_line import sensor as command_line from homeassistant.helpers.template import Template +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index cee86fc046eceb..e5da27818fcd84 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -390,7 +390,12 @@ async def async_step_account(self, user_input=None): assert response["success"] assert response["result"] == [ - {"flow_id": form["flow_id"], "handler": "test", "context": {"source": "hassio"}} + { + "flow_id": form["flow_id"], + "handler": "test", + "step_id": "account", + "context": {"source": "hassio"}, + } ] diff --git a/tests/components/config/test_device_registry.py b/tests/components/config/test_device_registry.py index a3710d48b940ba..c2557c83a4a3e9 100644 --- a/tests/components/config/test_device_registry.py +++ b/tests/components/config/test_device_registry.py @@ -34,6 +34,7 @@ async def test_list_devices(hass, client, registry): manufacturer="manufacturer", model="model", via_device=("bridgeid", "0123"), + entry_type="service", ) await client.send_json({"id": 5, "type": "config/device_registry/list"}) @@ -49,6 +50,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, + "entry_type": None, "via_device_id": None, "area_id": None, "name_by_user": None, @@ -60,6 +62,7 @@ async def test_list_devices(hass, client, registry): "model": "model", "name": None, "sw_version": None, + "entry_type": "service", "via_device_id": dev1, "area_id": None, "name_by_user": None, diff --git a/tests/components/config/test_group.py b/tests/components/config/test_group.py index f555660fb7a4f2..98ad2041713cc0 100644 --- a/tests/components/config/test_group.py +++ b/tests/components/config/test_group.py @@ -1,11 +1,10 @@ """Test Group config panel.""" import json -from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.components import config -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, patch VIEW_NAME = "api:config:group:config" diff --git a/tests/components/config/test_script.py b/tests/components/config/test_script.py index 0026729766c5bd..4dc906e92f3e99 100644 --- a/tests/components/config/test_script.py +++ b/tests/components/config/test_script.py @@ -1,9 +1,9 @@ """Tests for config/script.""" -from unittest.mock import patch - from homeassistant.bootstrap import async_setup_component from homeassistant.components import config +from tests.async_mock import patch + async def test_delete_script(hass, hass_client): """Test deleting a script.""" diff --git a/tests/components/config/test_zwave.py b/tests/components/config/test_zwave.py index 3624554843a77f..75a66f61939b5e 100644 --- a/tests/components/config/test_zwave.py +++ b/tests/components/config/test_zwave.py @@ -1,6 +1,5 @@ """Test Z-Wave config panel.""" import json -from unittest.mock import MagicMock, patch import pytest @@ -9,6 +8,7 @@ from homeassistant.components.zwave import DATA_NETWORK, const from homeassistant.const import HTTP_NOT_FOUND +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue VIEW_NAME = "api:config:zwave:device_config" diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index aea78f17564af7..996aef3db4b35e 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -1,7 +1,6 @@ # pylint: disable=redefined-outer-name """Tests for the Daikin config flow.""" import asyncio -from unittest.mock import patch import pytest @@ -10,6 +9,7 @@ from homeassistant.components.daikin.const import KEY_IP, KEY_MAC from homeassistant.const import CONF_HOST +from tests.async_mock import patch from tests.common import MockConfigEntry MAC = "AABBCCDDEEFF" diff --git a/tests/components/darksky/test_sensor.py b/tests/components/darksky/test_sensor.py index 2dc2a3ff30b6bf..b4707af01b2e43 100644 --- a/tests/components/darksky/test_sensor.py +++ b/tests/components/darksky/test_sensor.py @@ -2,7 +2,6 @@ from datetime import timedelta import re import unittest -from unittest.mock import MagicMock, patch import forecastio from requests.exceptions import HTTPError @@ -11,6 +10,7 @@ from homeassistant.components.darksky import sensor as darksky from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG_MINIMAL = { diff --git a/tests/components/darksky/test_weather.py b/tests/components/darksky/test_weather.py index 09ffe7bdc90a75..f871d424db62cc 100644 --- a/tests/components/darksky/test_weather.py +++ b/tests/components/darksky/test_weather.py @@ -1,7 +1,6 @@ """The tests for the Dark Sky weather component.""" import re import unittest -from unittest.mock import patch import forecastio from requests.exceptions import ConnectionError @@ -11,6 +10,7 @@ from homeassistant.setup import setup_component from homeassistant.util.unit_system import METRIC_SYSTEM +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture diff --git a/tests/components/default_config/test_init.py b/tests/components/default_config/test_init.py index 8e0862eb0cb71c..00fb1c1047bfad 100644 --- a/tests/components/default_config/test_init.py +++ b/tests/components/default_config/test_init.py @@ -1,10 +1,10 @@ """Test the default_config init.""" -from unittest.mock import patch - import pytest from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(autouse=True) def recorder_url_mock(): diff --git a/tests/components/demo/test_camera.py b/tests/components/demo/test_camera.py index d46d1fdc62b524..49b8e017f1ad80 100644 --- a/tests/components/demo/test_camera.py +++ b/tests/components/demo/test_camera.py @@ -1,6 +1,4 @@ """The tests for local file camera component.""" -from unittest.mock import patch - import pytest from homeassistant.components.camera import ( @@ -18,6 +16,8 @@ from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component +from tests.async_mock import patch + ENTITY_CAMERA = "camera.demo_camera" diff --git a/tests/components/demo/test_geo_location.py b/tests/components/demo/test_geo_location.py index 5d5f2fc810655a..0ba3a35b8917ba 100644 --- a/tests/components/demo/test_geo_location.py +++ b/tests/components/demo/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the demo platform.""" import unittest -from unittest.mock import patch from homeassistant.components import geo_location from homeassistant.components.demo.geo_location import ( @@ -11,6 +10,7 @@ from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, fire_time_changed, diff --git a/tests/components/demo/test_notify.py b/tests/components/demo/test_notify.py index e30d65112e8e12..7c7b7fa0aa1412 100644 --- a/tests/components/demo/test_notify.py +++ b/tests/components/demo/test_notify.py @@ -1,6 +1,5 @@ """The tests for the notify demo platform.""" import unittest -from unittest.mock import patch import pytest import voluptuous as vol @@ -11,6 +10,7 @@ from homeassistant.helpers import discovery from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant from tests.components.notify import common diff --git a/tests/components/denonavr/test_media_player.py b/tests/components/denonavr/test_media_player.py index 91bc2abf94d7ce..1547391a339235 100644 --- a/tests/components/denonavr/test_media_player.py +++ b/tests/components/denonavr/test_media_player.py @@ -1,6 +1,4 @@ """The tests for the denonavr media player platform.""" -from unittest.mock import patch - import pytest from homeassistant.components import media_player @@ -8,6 +6,8 @@ from homeassistant.const import ATTR_ENTITY_ID, CONF_HOST, CONF_NAME, CONF_PLATFORM from homeassistant.setup import async_setup_component +from tests.async_mock import patch + NAME = "fake" ENTITY_ID = f"{media_player.DOMAIN}.{NAME}" diff --git a/tests/components/derivative/test_sensor.py b/tests/components/derivative/test_sensor.py index 466f07a9deba4a..96fee84126aa60 100644 --- a/tests/components/derivative/test_sensor.py +++ b/tests/components/derivative/test_sensor.py @@ -1,11 +1,12 @@ """The tests for the derivative sensor platform.""" from datetime import timedelta -from unittest.mock import patch from homeassistant.const import POWER_WATT, TIME_HOURS, TIME_MINUTES, TIME_SECONDS from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch + async def test_state(hass): """Test derivative sensor state.""" diff --git a/tests/components/dialogflow/test_init.py b/tests/components/dialogflow/test_init.py index 5c2a71ad88fa99..c8c508a0d887aa 100644 --- a/tests/components/dialogflow/test_init.py +++ b/tests/components/dialogflow/test_init.py @@ -1,7 +1,6 @@ """The tests for the Dialogflow component.""" import copy import json -from unittest.mock import Mock import pytest @@ -10,6 +9,8 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component +from tests.async_mock import Mock + SESSION_ID = "a9b84cec-46b6-484e-8f31-f65dba03ae6d" INTENT_ID = "c6a74079-a8f0-46cd-b372-5a934d23591c" INTENT_NAME = "tests" diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index 936a1e6803766b..895a95bef7b3f2 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -9,7 +9,6 @@ import datetime from decimal import Decimal from itertools import chain, repeat -from unittest.mock import DEFAULT, Mock import pytest @@ -18,6 +17,7 @@ from homeassistant.const import ENERGY_KILO_WATT_HOUR, TIME_HOURS, VOLUME_CUBIC_METERS import tests.async_mock +from tests.async_mock import DEFAULT, Mock from tests.common import assert_setup_component diff --git a/tests/components/dynalite/test_init.py b/tests/components/dynalite/test_init.py index 8b1393cb32dd96..be646a1854d2d5 100644 --- a/tests/components/dynalite/test_init.py +++ b/tests/components/dynalite/test_init.py @@ -37,6 +37,7 @@ async def test_async_setup(hass): "1": { CONF_NAME: "Name1", dynalite.CONF_CHANNEL: {"4": {}}, + dynalite.CONF_PRESET: {"7": {}}, dynalite.CONF_NO_DEFAULT: True, }, "2": {CONF_NAME: "Name2"}, diff --git a/tests/components/emulated_hue/test_hue_api.py b/tests/components/emulated_hue/test_hue_api.py index 052228e7aabf6e..fa97cd2f417d9d 100644 --- a/tests/components/emulated_hue/test_hue_api.py +++ b/tests/components/emulated_hue/test_hue_api.py @@ -2,7 +2,6 @@ from datetime import timedelta from ipaddress import ip_address import json -from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import pytest @@ -27,6 +26,7 @@ HUE_API_USERNAME, HueAllGroupsStateView, HueAllLightsStateView, + HueConfigView, HueFullStateView, HueOneLightChangeView, HueOneLightStateView, @@ -35,6 +35,8 @@ from homeassistant.const import ( ATTR_ENTITY_ID, HTTP_NOT_FOUND, + HTTP_OK, + HTTP_UNAUTHORIZED, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, @@ -42,6 +44,7 @@ ) import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( async_fire_time_changed, async_mock_service, @@ -54,6 +57,26 @@ BRIDGE_URL_BASE = f"http://127.0.0.1:{BRIDGE_SERVER_PORT}" + "{}" JSON_HEADERS = {CONTENT_TYPE: const.CONTENT_TYPE_JSON} +ENTITY_IDS_BY_NUMBER = { + "1": "light.ceiling_lights", + "2": "light.bed_light", + "3": "script.set_kitchen_light", + "4": "light.kitchen_lights", + "5": "media_player.living_room", + "6": "media_player.bedroom", + "7": "media_player.walkman", + "8": "media_player.lounge_room", + "9": "fan.living_room_fan", + "10": "fan.ceiling_fan", + "11": "cover.living_room_window", + "12": "climate.hvac", + "13": "climate.heatpump", + "14": "climate.ecobee", + "15": "light.no_brightness", +} + +ENTITY_NUMBERS_BY_ID = {v: k for k, v in ENTITY_IDS_BY_NUMBER.items()} + @pytest.fixture def hass_hue(loop, hass): @@ -144,7 +167,6 @@ def hue_client(loop, hass_hue, aiohttp_client): config = Config( None, { - 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 @@ -162,6 +184,7 @@ def hue_client(loop, hass_hue, aiohttp_client): }, }, ) + config.numbers = ENTITY_IDS_BY_NUMBER HueUsernameView().register(web_app, web_app.router) HueAllLightsStateView(config).register(web_app, web_app.router) @@ -169,6 +192,7 @@ def hue_client(loop, hass_hue, aiohttp_client): HueOneLightChangeView(config).register(web_app, web_app.router) HueAllGroupsStateView(config).register(web_app, web_app.router) HueFullStateView(config).register(web_app, web_app.router) + HueConfigView(config).register(web_app, web_app.router) return loop.run_until_complete(aiohttp_client(web_app)) @@ -177,7 +201,7 @@ async def test_discover_lights(hue_client): """Test the discovery of lights.""" result = await hue_client.get("/api/username/lights") - assert result.status == 200 + assert result.status == HTTP_OK assert "application/json" in result.headers["content-type"] result_json = await result.json() @@ -204,7 +228,7 @@ async def test_discover_lights(hue_client): async def test_light_without_brightness_supported(hass_hue, hue_client): """Test that light without brightness is supported.""" light_without_brightness_json = await perform_get_light_state( - hue_client, "light.no_brightness", 200 + hue_client, "light.no_brightness", HTTP_OK ) assert light_without_brightness_json["state"][HUE_API_STATE_ON] is True @@ -223,7 +247,7 @@ async def test_light_without_brightness_can_be_turned_off(hass_hue, hue_client): ) no_brightness_result_json = await no_brightness_result.json() - assert no_brightness_result.status == 200 + assert no_brightness_result.status == HTTP_OK assert "application/json" in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 @@ -255,7 +279,7 @@ async def test_light_without_brightness_can_be_turned_on(hass_hue, hue_client): no_brightness_result_json = await no_brightness_result.json() - assert no_brightness_result.status == 200 + assert no_brightness_result.status == HTTP_OK assert "application/json" in no_brightness_result.headers["content-type"] assert len(no_brightness_result_json) == 1 @@ -283,7 +307,7 @@ async def test_reachable_for_state(hass_hue, hue_client, state, is_reachable): hass_hue.states.async_set(entity_id, state) - state_json = await perform_get_light_state(hue_client, entity_id, 200) + state_json = await perform_get_light_state(hue_client, entity_id, HTTP_OK) assert state_json["state"]["reachable"] == is_reachable, state_json @@ -292,7 +316,7 @@ async def test_discover_full_state(hue_client): """Test the discovery of full state.""" result = await hue_client.get(f"/api/{HUE_API_USERNAME}") - assert result.status == 200 + assert result.status == HTTP_OK assert "application/json" in result.headers["content-type"] result_json = await result.json() @@ -308,7 +332,7 @@ async def test_discover_full_state(hue_client): # Make sure array is correct size assert len(result_json) == 2 - assert len(config_json) == 4 + assert len(config_json) == 6 assert len(lights_json) >= 1 # Make sure the config wrapper added to the config is there @@ -319,6 +343,49 @@ async def test_discover_full_state(hue_client): assert "swversion" in config_json assert "01003542" in config_json["swversion"] + # Make sure the api version is correct + assert "apiversion" in config_json + assert "1.17.0" in config_json["apiversion"] + + # Make sure the correct username in config + assert "whitelist" in config_json + assert HUE_API_USERNAME in config_json["whitelist"] + assert "name" in config_json["whitelist"][HUE_API_USERNAME] + assert "HASS BRIDGE" in config_json["whitelist"][HUE_API_USERNAME]["name"] + + # Make sure the correct ip in config + assert "ipaddress" in config_json + assert "127.0.0.1:8300" in config_json["ipaddress"] + + # Make sure the device announces a link button + assert "linkbutton" in config_json + assert config_json["linkbutton"] is True + + +async def test_discover_config(hue_client): + """Test the discovery of configuration.""" + result = await hue_client.get(f"/api/{HUE_API_USERNAME}/config") + + assert result.status == 200 + assert "application/json" in result.headers["content-type"] + + config_json = await result.json() + + # Make sure array is correct size + assert len(config_json) == 6 + + # Make sure the config wrapper added to the config is there + assert "mac" in config_json + assert "00:00:00:00:00:00" in config_json["mac"] + + # Make sure the correct version in config + assert "swversion" in config_json + assert "01003542" in config_json["swversion"] + + # Make sure the api version is correct + assert "apiversion" in config_json + assert "1.17.0" in config_json["apiversion"] + # Make sure the correct username in config assert "whitelist" in config_json assert HUE_API_USERNAME in config_json["whitelist"] @@ -329,6 +396,10 @@ async def test_discover_full_state(hue_client): assert "ipaddress" in config_json assert "127.0.0.1:8300" in config_json["ipaddress"] + # Make sure the device announces a link button + assert "linkbutton" in config_json + assert config_json["linkbutton"] is True + async def test_get_light_state(hass_hue, hue_client): """Test the getting of light state.""" @@ -344,7 +415,9 @@ async def test_get_light_state(hass_hue, hue_client): blocking=True, ) - office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) + office_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTP_OK + ) assert office_json["state"][HUE_API_STATE_ON] is True assert office_json["state"][HUE_API_STATE_BRI] == 127 @@ -354,13 +427,18 @@ async def test_get_light_state(hass_hue, hue_client): # Check all lights view result = await hue_client.get("/api/username/lights") - assert result.status == 200 + assert result.status == HTTP_OK assert "application/json" in result.headers["content-type"] result_json = await result.json() - assert "light.ceiling_lights" in result_json - assert result_json["light.ceiling_lights"]["state"][HUE_API_STATE_BRI] == 127 + assert ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] in result_json + assert ( + result_json[ENTITY_NUMBERS_BY_ID["light.ceiling_lights"]]["state"][ + HUE_API_STATE_BRI + ] + == 127 + ) # Turn office light off await hass_hue.services.async_call( @@ -370,7 +448,9 @@ async def test_get_light_state(hass_hue, hue_client): blocking=True, ) - office_json = await perform_get_light_state(hue_client, "light.ceiling_lights", 200) + office_json = await perform_get_light_state( + hue_client, "light.ceiling_lights", HTTP_OK + ) assert office_json["state"][HUE_API_STATE_ON] is False # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 @@ -378,10 +458,10 @@ async def test_get_light_state(hass_hue, hue_client): assert office_json["state"][HUE_API_STATE_SAT] == 0 # Make sure bedroom light isn't accessible - await perform_get_light_state(hue_client, "light.bed_light", 401) + await perform_get_light_state(hue_client, "light.bed_light", HTTP_UNAUTHORIZED) # Make sure kitchen light isn't accessible - await perform_get_light_state(hue_client, "light.kitchen_lights", 401) + await perform_get_light_state(hue_client, "light.kitchen_lights", HTTP_UNAUTHORIZED) async def test_put_light_state(hass, hass_hue, hue_client): @@ -432,7 +512,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", 200 + hue_client, "light.ceiling_lights", HTTP_OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 123 assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -451,7 +531,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): # go through api to get the state back ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", 200 + hue_client, "light.ceiling_lights", HTTP_OK ) assert ceiling_json["state"][HUE_API_STATE_BRI] == 254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 4369 @@ -464,7 +544,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_result_json = await ceiling_result.json() - assert ceiling_result.status == 200 + assert ceiling_result.status == HTTP_OK assert "application/json" in ceiling_result.headers["content-type"] assert len(ceiling_result_json) == 1 @@ -473,7 +553,7 @@ async def test_put_light_state(hass, hass_hue, hue_client): ceiling_lights = hass_hue.states.get("light.ceiling_lights") assert ceiling_lights.state == STATE_OFF ceiling_json = await perform_get_light_state( - hue_client, "light.ceiling_lights", 200 + hue_client, "light.ceiling_lights", HTTP_OK ) # Removed assert HUE_API_STATE_BRI == 0 as Hue API states bri must be 1..254 assert ceiling_json["state"][HUE_API_STATE_HUE] == 0 @@ -483,13 +563,13 @@ async def test_put_light_state(hass, hass_hue, hue_client): bedroom_result = await perform_put_light_state( hass_hue, hue_client, "light.bed_light", True ) - assert bedroom_result.status == 401 + assert bedroom_result.status == HTTP_UNAUTHORIZED # Make sure we can't change the kitchen light state kitchen_result = await perform_put_light_state( - hass_hue, hue_client, "light.kitchen_light", True + hass_hue, hue_client, "light.kitchen_lights", True ) - assert kitchen_result.status == HTTP_NOT_FOUND + assert kitchen_result.status == HTTP_UNAUTHORIZED async def test_put_light_state_script(hass, hass_hue, hue_client): @@ -512,7 +592,7 @@ async def test_put_light_state_script(hass, hass_hue, hue_client): script_result_json = await script_result.json() - assert script_result.status == 200 + assert script_result.status == HTTP_OK assert len(script_result_json) == 2 kitchen_light = hass_hue.states.get("light.kitchen_lights") @@ -535,7 +615,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): hvac_result_json = await hvac_result.json() - assert hvac_result.status == 200 + assert hvac_result.status == HTTP_OK assert len(hvac_result_json) == 2 hvac = hass_hue.states.get("climate.hvac") @@ -546,7 +626,7 @@ async def test_put_light_state_climate_set_temperature(hass_hue, hue_client): ecobee_result = await perform_put_light_state( hass_hue, hue_client, "climate.ecobee", True ) - assert ecobee_result.status == 401 + assert ecobee_result.status == HTTP_UNAUTHORIZED async def test_put_light_state_media_player(hass_hue, hue_client): @@ -569,7 +649,7 @@ async def test_put_light_state_media_player(hass_hue, hue_client): mp_result_json = await mp_result.json() - assert mp_result.status == 200 + assert mp_result.status == HTTP_OK assert len(mp_result_json) == 2 walkman = hass_hue.states.get("media_player.walkman") @@ -604,7 +684,7 @@ async def test_close_cover(hass_hue, hue_client): hass_hue, hue_client, cover_id, True, 100 ) - assert cover_result.status == 200 + assert cover_result.status == HTTP_OK assert "application/json" in cover_result.headers["content-type"] for _ in range(7): @@ -624,6 +704,7 @@ async def test_close_cover(hass_hue, hue_client): async def test_set_position_cover(hass_hue, hue_client): """Test setting position cover .""" cover_id = "cover.living_room_window" + cover_number = ENTITY_NUMBERS_BY_ID[cover_id] # Turn the office light off first await hass_hue.services.async_call( cover.DOMAIN, @@ -651,19 +732,14 @@ async def test_set_position_cover(hass_hue, hue_client): hass_hue, hue_client, cover_id, False, brightness ) - assert cover_result.status == 200 + assert cover_result.status == HTTP_OK assert "application/json" in cover_result.headers["content-type"] cover_result_json = await cover_result.json() assert len(cover_result_json) == 2 - assert True, cover_result_json[0]["success"][ - "/lights/cover.living_room_window/state/on" - ] - assert ( - cover_result_json[1]["success"]["/lights/cover.living_room_window/state/bri"] - == level - ) + assert True, cover_result_json[0]["success"][f"/lights/{cover_number}/state/on"] + assert cover_result_json[1]["success"][f"/lights/{cover_number}/state/bri"] == level for _ in range(100): future = dt_util.utcnow() + timedelta(seconds=1) @@ -696,7 +772,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): fan_result_json = await fan_result.json() - assert fan_result.status == 200 + assert fan_result.status == HTTP_OK assert len(fan_result_json) == 2 living_room_fan = hass_hue.states.get("fan.living_room_fan") @@ -707,6 +783,7 @@ async def test_put_light_state_fan(hass_hue, hue_client): # pylint: disable=invalid-name async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): """Test the form with urlencoded content.""" + entity_number = ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] # Needed for Alexa await perform_put_test_on_ceiling_lights( hass_hue, hue_client, "application/x-www-form-urlencoded" @@ -715,7 +792,7 @@ async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): # Make sure we fail gracefully when we can't parse the data data = {"key1": "value1", "key2": "value2"} result = await hue_client.put( - "/api/username/lights/light.ceiling_lights/state", + f"/api/username/lights/{entity_number}/state", headers={"content-type": "application/x-www-form-urlencoded"}, data=data, ) @@ -725,22 +802,26 @@ async def test_put_with_form_urlencoded_content_type(hass_hue, hue_client): async def test_entity_not_found(hue_client): """Test for entity which are not found.""" - result = await hue_client.get("/api/username/lights/not.existant_entity") + result = await hue_client.get("/api/username/lights/98") assert result.status == HTTP_NOT_FOUND - result = await hue_client.put("/api/username/lights/not.existant_entity/state") + result = await hue_client.put("/api/username/lights/98/state") assert result.status == HTTP_NOT_FOUND async def test_allowed_methods(hue_client): """Test the allowed methods.""" - result = await hue_client.get("/api/username/lights/light.ceiling_lights/state") + result = await hue_client.get( + "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]/state" + ) assert result.status == 405 - result = await hue_client.put("/api/username/lights/light.ceiling_lights") + result = await hue_client.put( + "/api/username/lights/ENTITY_NUMBERS_BY_ID[light.ceiling_lights]" + ) assert result.status == 405 @@ -753,7 +834,9 @@ async def test_proper_put_state_request(hue_client): """Test the request to set the state.""" # Test proper on value parsing result = await hue_client.put( - "/api/username/lights/{}/state".format("light.ceiling_lights"), + "/api/username/lights/{}/state".format( + ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] + ), data=json.dumps({HUE_API_STATE_ON: 1234}), ) @@ -761,7 +844,9 @@ async def test_proper_put_state_request(hue_client): # Test proper brightness value parsing result = await hue_client.put( - "/api/username/lights/{}/state".format("light.ceiling_lights"), + "/api/username/lights/{}/state".format( + ENTITY_NUMBERS_BY_ID["light.ceiling_lights"] + ), data=json.dumps({HUE_API_STATE_ON: True, HUE_API_STATE_BRI: "Hello world!"}), ) @@ -773,7 +858,7 @@ async def test_get_empty_groups_state(hue_client): # Test proper on value parsing result = await hue_client.get("/api/username/groups") - assert result.status == 200 + assert result.status == HTTP_OK result_json = await result.json() @@ -801,7 +886,7 @@ async def perform_put_test_on_ceiling_lights( hass_hue, hue_client, "light.ceiling_lights", True, 56, content_type ) - assert office_result.status == 200 + assert office_result.status == HTTP_OK assert "application/json" in office_result.headers["content-type"] office_result_json = await office_result.json() @@ -816,11 +901,12 @@ async def perform_put_test_on_ceiling_lights( async def perform_get_light_state(client, entity_id, expected_status): """Test the getting of a light state.""" - result = await client.get(f"/api/username/lights/{entity_id}") + entity_number = ENTITY_NUMBERS_BY_ID[entity_id] + result = await client.get(f"/api/username/lights/{entity_number}") assert result.status == expected_status - if expected_status == 200: + if expected_status == HTTP_OK: assert "application/json" in result.headers["content-type"] return await result.json() @@ -850,8 +936,9 @@ async def perform_put_light_state( if saturation is not None: data[HUE_API_STATE_SAT] = saturation + entity_number = ENTITY_NUMBERS_BY_ID[entity_id] result = await client.put( - f"/api/username/lights/{entity_id}/state", + f"/api/username/lights/{entity_number}/state", headers=req_headers, data=json.dumps(data).encode(), ) @@ -867,6 +954,7 @@ async def test_external_ip_blocked(hue_client): getUrls = [ "/api/username/groups", "/api/username", + "/api/username/config", "/api/username/lights", "/api/username/lights/light.ceiling_lights", ] @@ -878,12 +966,26 @@ async def test_external_ip_blocked(hue_client): ): for getUrl in getUrls: result = await hue_client.get(getUrl) - assert result.status == 401 + assert result.status == HTTP_UNAUTHORIZED for postUrl in postUrls: result = await hue_client.post(postUrl) - assert result.status == 401 + assert result.status == HTTP_UNAUTHORIZED for putUrl in putUrls: result = await hue_client.put(putUrl) - assert result.status == 401 + assert result.status == HTTP_UNAUTHORIZED + + +async def test_unauthorized_user_blocked(hue_client): + """Test unauthorized_user blocked.""" + getUrls = [ + "/api/wronguser", + "/api/wronguser/config", + ] + for getUrl in getUrls: + result = await hue_client.get(getUrl) + assert result.status == HTTP_OK + + result_json = await result.json() + assert result_json[0]["error"]["description"] == "unauthorized user" diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 6fa6d9695390ca..b1cf2aacb1bc3b 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,8 +1,8 @@ """Test the Emulated Hue component.""" -from unittest.mock import MagicMock, Mock, patch - from homeassistant.components.emulated_hue import Config +from tests.async_mock import MagicMock, Mock, patch + def test_config_google_home_entity_id_to_number(): """Test config adheres to the type.""" diff --git a/tests/components/emulated_hue/test_upnp.py b/tests/components/emulated_hue/test_upnp.py index 889f6437b0ac59..32859ca00c1729 100644 --- a/tests/components/emulated_hue/test_upnp.py +++ b/tests/components/emulated_hue/test_upnp.py @@ -1,7 +1,6 @@ """The tests for the emulated Hue component.""" import json import unittest -from unittest.mock import patch from aiohttp.hdrs import CONTENT_TYPE import defusedxml.ElementTree as ET @@ -9,7 +8,9 @@ from homeassistant import const, setup from homeassistant.components import emulated_hue +from homeassistant.const import HTTP_OK +from tests.async_mock import patch from tests.common import get_test_home_assistant, get_test_instance_port HTTP_SERVER_PORT = get_test_instance_port() @@ -51,12 +52,14 @@ def test_description_xml(self): """Test the description.""" result = requests.get(BRIDGE_URL_BASE.format("/description.xml"), timeout=5) - assert result.status_code == 200 + assert result.status_code == HTTP_OK assert "text/xml" in result.headers["content-type"] # Make sure the XML is parsable try: - ET.fromstring(result.text) + root = ET.fromstring(result.text) + ns = {"s": "urn:schemas-upnp-org:device-1-0"} + assert root.find("./s:device/s:serialNumber", ns).text == "001788FFFE23BFC2" except: # noqa: E722 pylint: disable=bare-except self.fail("description.xml is not valid XML!") @@ -68,7 +71,7 @@ def test_create_username(self): BRIDGE_URL_BASE.format("/api"), data=json.dumps(request_json), timeout=5 ) - assert result.status_code == 200 + assert result.status_code == HTTP_OK assert "application/json" in result.headers["content-type"] resp_json = result.json() @@ -87,7 +90,7 @@ def test_unauthorized_view(self): timeout=5, ) - assert result.status_code == 200 + assert result.status_code == HTTP_OK assert "application/json" in result.headers["content-type"] resp_json = result.json() diff --git a/tests/components/emulated_roku/test_binding.py b/tests/components/emulated_roku/test_binding.py index 61c93690548f7f..5ff29194adf41e 100644 --- a/tests/components/emulated_roku/test_binding.py +++ b/tests/components/emulated_roku/test_binding.py @@ -1,6 +1,4 @@ """Tests for emulated_roku library bindings.""" -from unittest.mock import Mock, patch - from homeassistant.components.emulated_roku.binding import ( ATTR_APP_ID, ATTR_COMMAND_TYPE, @@ -14,7 +12,7 @@ EmulatedRoku, ) -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, Mock, patch async def test_events_fired_properly(hass): diff --git a/tests/components/emulated_roku/test_init.py b/tests/components/emulated_roku/test_init.py index 8d58519ddf9aaa..92952a5d840bbd 100644 --- a/tests/components/emulated_roku/test_init.py +++ b/tests/components/emulated_roku/test_init.py @@ -1,10 +1,8 @@ """Test emulated_roku component setup process.""" -from unittest.mock import Mock, patch - from homeassistant.components import emulated_roku from homeassistant.setup import async_setup_component -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, Mock, patch async def test_config_required_fields(hass): diff --git a/tests/components/facebox/test_image_processing.py b/tests/components/facebox/test_image_processing.py index 8506cd2d817120..b40211d5a91cf2 100644 --- a/tests/components/facebox/test_image_processing.py +++ b/tests/components/facebox/test_image_processing.py @@ -1,6 +1,4 @@ """The tests for the facebox component.""" -from unittest.mock import Mock, mock_open, patch - import pytest import requests import requests_mock @@ -23,6 +21,8 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, mock_open, patch + MOCK_IP = "192.168.0.1" MOCK_PORT = "8080" diff --git a/tests/components/fail2ban/test_sensor.py b/tests/components/fail2ban/test_sensor.py index fc8bfc318bb52c..b164cc93f2e253 100644 --- a/tests/components/fail2ban/test_sensor.py +++ b/tests/components/fail2ban/test_sensor.py @@ -1,6 +1,5 @@ """The tests for local file sensor platform.""" import unittest -from unittest.mock import Mock, patch from mock_open import MockOpen @@ -12,6 +11,7 @@ ) from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/fan/test_init.py b/tests/components/fan/test_init.py index d9935cc0063db5..f27a3ff8ab6945 100644 --- a/tests/components/fan/test_init.py +++ b/tests/components/fan/test_init.py @@ -31,7 +31,7 @@ def test_fanentity(self): assert self.fan.state == "off" assert len(self.fan.speed_list) == 0 assert self.fan.supported_features == 0 - assert {"speed_list": []} == self.fan.capability_attributes + assert self.fan.capability_attributes == {} # Test set_speed not required self.fan.oscillate(True) with pytest.raises(NotImplementedError): diff --git a/tests/components/feedreader/test_init.py b/tests/components/feedreader/test_init.py index 58a660fcb5d0f9..823bdf6eb6347f 100644 --- a/tests/components/feedreader/test_init.py +++ b/tests/components/feedreader/test_init.py @@ -6,7 +6,6 @@ import time import unittest from unittest import mock -from unittest.mock import patch from homeassistant.components import feedreader from homeassistant.components.feedreader import ( @@ -22,6 +21,7 @@ from homeassistant.core import callback from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture _LOGGER = getLogger(__name__) diff --git a/tests/components/ffmpeg/test_init.py b/tests/components/ffmpeg/test_init.py index 3c6a2fbb92d7c6..4187fe561cc297 100644 --- a/tests/components/ffmpeg/test_init.py +++ b/tests/components/ffmpeg/test_init.py @@ -1,6 +1,4 @@ """The tests for Home Assistant ffmpeg.""" -from unittest.mock import MagicMock - import homeassistant.components.ffmpeg as ffmpeg from homeassistant.components.ffmpeg import ( DOMAIN, @@ -12,6 +10,7 @@ from homeassistant.core import callback from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import MagicMock from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/fido/test_sensor.py b/tests/components/fido/test_sensor.py index 9a316a85735b52..b57232d25ad65e 100644 --- a/tests/components/fido/test_sensor.py +++ b/tests/components/fido/test_sensor.py @@ -1,11 +1,11 @@ """The test for the fido sensor platform.""" import logging import sys -from unittest.mock import MagicMock, patch from homeassistant.bootstrap import async_setup_component from homeassistant.components.fido import sensor as fido +from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component CONTRACT = "123456789" diff --git a/tests/components/file/test_notify.py b/tests/components/file/test_notify.py index bd5ae68cb3729a..e4ae125949a7cf 100644 --- a/tests/components/file/test_notify.py +++ b/tests/components/file/test_notify.py @@ -1,13 +1,13 @@ """The tests for the notify file platform.""" import os import unittest -from unittest.mock import call, mock_open, patch import homeassistant.components.notify as notify from homeassistant.components.notify import ATTR_TITLE_DEFAULT from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import call, mock_open, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/file/test_sensor.py b/tests/components/file/test_sensor.py index 3afdd8284fc496..416f3e8c721689 100644 --- a/tests/components/file/test_sensor.py +++ b/tests/components/file/test_sensor.py @@ -1,6 +1,5 @@ """The tests for local file sensor platform.""" import unittest -from unittest.mock import Mock, patch # Using third party package because of a bug reading binary data in Python 3.4 # https://bugs.python.org/issue23004 @@ -9,6 +8,7 @@ from homeassistant.const import STATE_UNKNOWN from homeassistant.setup import setup_component +from tests.async_mock import Mock, patch from tests.common import get_test_home_assistant, mock_registry diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index 06bf7cfaf1277b..238cb366f734eb 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -1,7 +1,6 @@ """The test for the data filter sensor platform.""" from datetime import timedelta import unittest -from unittest.mock import patch from homeassistant.components.filter.sensor import ( LowPassFilter, @@ -15,6 +14,7 @@ from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import ( assert_setup_component, get_test_home_assistant, diff --git a/tests/components/folder_watcher/test_init.py b/tests/components/folder_watcher/test_init.py index fa2cfc6d3f1041..997bac23f22e0e 100644 --- a/tests/components/folder_watcher/test_init.py +++ b/tests/components/folder_watcher/test_init.py @@ -1,10 +1,11 @@ """The tests for the folder_watcher component.""" import os -from unittest.mock import Mock, patch from homeassistant.components import folder_watcher from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch + async def test_invalid_path_setup(hass): """Test that an invalid path is not set up.""" diff --git a/tests/components/foobot/test_sensor.py b/tests/components/foobot/test_sensor.py index 37a0310d1634b5..0a145c88479262 100644 --- a/tests/components/foobot/test_sensor.py +++ b/tests/components/foobot/test_sensor.py @@ -2,7 +2,6 @@ import asyncio import re -from unittest.mock import MagicMock import pytest @@ -20,6 +19,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.setup import async_setup_component +from tests.async_mock import MagicMock from tests.common import load_fixture VALID_CONFIG = { diff --git a/tests/components/freebox/conftest.py b/tests/components/freebox/conftest.py index e813469cbbfb4d..7581b03ce729ff 100644 --- a/tests/components/freebox/conftest.py +++ b/tests/components/freebox/conftest.py @@ -1,8 +1,8 @@ """Test helpers for Freebox.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + @pytest.fixture(autouse=True) def mock_path(): diff --git a/tests/components/fritzbox/__init__.py b/tests/components/fritzbox/__init__.py index f19e05b84dfee4..066b9a30cb30c7 100644 --- a/tests/components/fritzbox/__init__.py +++ b/tests/components/fritzbox/__init__.py @@ -1,9 +1,9 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock - from homeassistant.components.fritzbox.const import DOMAIN from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import Mock + MOCK_CONFIG = { DOMAIN: { CONF_DEVICES: [ diff --git a/tests/components/fritzbox/conftest.py b/tests/components/fritzbox/conftest.py index 591c10375256d4..7dcee138382769 100644 --- a/tests/components/fritzbox/conftest.py +++ b/tests/components/fritzbox/conftest.py @@ -1,8 +1,8 @@ """Fixtures for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, patch - import pytest +from tests.async_mock import Mock, patch + @pytest.fixture(name="fritz") def fritz_fixture() -> Mock: diff --git a/tests/components/fritzbox/test_binary_sensor.py b/tests/components/fritzbox/test_binary_sensor.py index 89c1dea170440b..b3157a3be33a78 100644 --- a/tests/components/fritzbox/test_binary_sensor.py +++ b/tests/components/fritzbox/test_binary_sensor.py @@ -1,7 +1,6 @@ """Tests for AVM Fritz!Box binary sensor component.""" from datetime import timedelta from unittest import mock -from unittest.mock import Mock from requests.exceptions import HTTPError @@ -19,6 +18,7 @@ from . import MOCK_CONFIG, FritzDeviceBinarySensorMock +from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_climate.py b/tests/components/fritzbox/test_climate.py index 627eae5da91855..519e3afa31a690 100644 --- a/tests/components/fritzbox/test_climate.py +++ b/tests/components/fritzbox/test_climate.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box climate component.""" from datetime import timedelta -from unittest.mock import Mock, call from requests.exceptions import HTTPError @@ -42,6 +41,7 @@ from . import MOCK_CONFIG, FritzDeviceClimateMock +from tests.async_mock import Mock, call from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index 8bfd992347f51a..35b41d52118280 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box config flow.""" from unittest import mock -from unittest.mock import Mock, patch from pyfritzhome import LoginError import pytest @@ -17,6 +16,8 @@ from . import MOCK_CONFIG +from tests.async_mock import Mock, patch + MOCK_USER_DATA = MOCK_CONFIG[DOMAIN][CONF_DEVICES][0] MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 11067c1aa51bf5..55dab3626db338 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,6 +1,4 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, call - from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED @@ -10,6 +8,7 @@ from . import MOCK_CONFIG, FritzDeviceSwitchMock +from tests.async_mock import Mock, call from tests.common import MockConfigEntry diff --git a/tests/components/fritzbox/test_sensor.py b/tests/components/fritzbox/test_sensor.py index 6dde22f074e45d..7f97e8abfb1705 100644 --- a/tests/components/fritzbox/test_sensor.py +++ b/tests/components/fritzbox/test_sensor.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box sensor component.""" from datetime import timedelta -from unittest.mock import Mock from requests.exceptions import HTTPError @@ -21,6 +20,7 @@ from . import MOCK_CONFIG, FritzDeviceSensorMock +from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/fritzbox/test_switch.py b/tests/components/fritzbox/test_switch.py index 1c0f7b3f37a9d5..c9e05b2d481a5d 100644 --- a/tests/components/fritzbox/test_switch.py +++ b/tests/components/fritzbox/test_switch.py @@ -1,6 +1,5 @@ """Tests for AVM Fritz!Box switch component.""" from datetime import timedelta -from unittest.mock import Mock from requests.exceptions import HTTPError @@ -29,6 +28,7 @@ from . import MOCK_CONFIG, FritzDeviceSwitchMock +from tests.async_mock import Mock from tests.common import async_fire_time_changed ENTITY_ID = f"{DOMAIN}.fake_name" diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index 276b6f46871c1e..1c383d4343abe4 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,6 +1,4 @@ """Test the Garmin Connect config flow.""" -from unittest.mock import patch - from garminconnect import ( GarminConnectAuthenticationError, GarminConnectConnectionError, @@ -12,6 +10,7 @@ from homeassistant.components.garmin_connect.const import DOMAIN from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry MOCK_CONF = { diff --git a/tests/components/gdacs/__init__.py b/tests/components/gdacs/__init__.py index 6e61b86dbb7c4b..648ab08507b66d 100644 --- a/tests/components/gdacs/__init__.py +++ b/tests/components/gdacs/__init__.py @@ -1,5 +1,5 @@ """Tests for the GDACS component.""" -from unittest.mock import MagicMock +from tests.async_mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geo_rss_events/test_sensor.py b/tests/components/geo_rss_events/test_sensor.py index 25243afea78984..9f7cdd3faabd13 100644 --- a/tests/components/geo_rss_events/test_sensor.py +++ b/tests/components/geo_rss_events/test_sensor.py @@ -1,7 +1,6 @@ """The test for the geo rss events sensor platform.""" import unittest from unittest import mock -from unittest.mock import MagicMock, patch from homeassistant.components import sensor import homeassistant.components.geo_rss_events.sensor as geo_rss_events @@ -14,6 +13,7 @@ from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, patch from tests.common import ( assert_setup_component, fire_time_changed, diff --git a/tests/components/geofency/test_init.py b/tests/components/geofency/test_init.py index b988d613d6cd8a..bdb5b49d11b39d 100644 --- a/tests/components/geofency/test_init.py +++ b/tests/components/geofency/test_init.py @@ -1,7 +1,4 @@ """The tests for the Geofency device tracker platform.""" -# pylint: disable=redefined-outer-name -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow @@ -16,6 +13,9 @@ from homeassistant.setup import async_setup_component from homeassistant.util import slugify +# pylint: disable=redefined-outer-name +from tests.async_mock import Mock, patch + HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/geonetnz_quakes/__init__.py b/tests/components/geonetnz_quakes/__init__.py index 424c6372ea8c6f..82cb62b3939749 100644 --- a/tests/components/geonetnz_quakes/__init__.py +++ b/tests/components/geonetnz_quakes/__init__.py @@ -1,5 +1,5 @@ """Tests for the geonetnz_quakes component.""" -from unittest.mock import MagicMock +from tests.async_mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/geonetnz_volcano/__init__.py b/tests/components/geonetnz_volcano/__init__.py index 708b69e003190d..023cab46ec87ac 100644 --- a/tests/components/geonetnz_volcano/__init__.py +++ b/tests/components/geonetnz_volcano/__init__.py @@ -1,5 +1,5 @@ """The tests for the GeoNet NZ Volcano Feed integration.""" -from unittest.mock import MagicMock +from tests.async_mock import MagicMock def _generate_mock_feed_entry( diff --git a/tests/components/google/conftest.py b/tests/components/google/conftest.py index 20cb13130ec63f..6ec75ad53f68ed 100644 --- a/tests/components/google/conftest.py +++ b/tests/components/google/conftest.py @@ -1,8 +1,8 @@ """Test configuration and mocks for the google integration.""" -from unittest.mock import patch - import pytest +from tests.async_mock import patch + TEST_CALENDAR = { "id": "qwertyuiopasdfghjklzxcvbnm@import.calendar.google.com", "etag": '"3584134138943410"', diff --git a/tests/components/google/test_calendar.py b/tests/components/google/test_calendar.py index ad7b6b12001e56..92f03396965a00 100644 --- a/tests/components/google/test_calendar.py +++ b/tests/components/google/test_calendar.py @@ -1,6 +1,5 @@ """The tests for the google calendar platform.""" import copy -from unittest.mock import Mock, patch import httplib2 import pytest @@ -23,6 +22,7 @@ from homeassistant.util import slugify import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import async_mock_service GOOGLE_CONFIG = {CONF_CLIENT_ID: "client_id", CONF_CLIENT_SECRET: "client_secret"} diff --git a/tests/components/google/test_init.py b/tests/components/google/test_init.py index 59a9f9f5ab20ce..6f7ce74ce62189 100644 --- a/tests/components/google/test_init.py +++ b/tests/components/google/test_init.py @@ -1,11 +1,11 @@ """The tests for the Google Calendar component.""" -from unittest.mock import patch - import pytest import homeassistant.components.google as google from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(name="google_setup") def mock_google_setup(hass): diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index f133db26d89234..f4e54176b195e2 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -1,6 +1,5 @@ """Tests for the Google Assistant traits.""" import logging -from unittest.mock import Mock import pytest @@ -45,7 +44,7 @@ from . import BASIC_CONFIG, MockConfig -from tests.async_mock import patch +from tests.async_mock import Mock, patch from tests.common import async_mock_service _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/google_translate/test_tts.py b/tests/components/google_translate/test_tts.py index 6703852528c4d3..280258125dfb22 100644 --- a/tests/components/google_translate/test_tts.py +++ b/tests/components/google_translate/test_tts.py @@ -2,7 +2,6 @@ import asyncio import os import shutil -from unittest.mock import patch from homeassistant.components.media_player.const import ( ATTR_MEDIA_CONTENT_ID, @@ -12,6 +11,7 @@ import homeassistant.components.tts as tts from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant, mock_service from tests.components.tts.test_init import mutagen_mock # noqa: F401 diff --git a/tests/components/google_wifi/test_sensor.py b/tests/components/google_wifi/test_sensor.py index 22059706dc50bb..69db8de184bcc7 100644 --- a/tests/components/google_wifi/test_sensor.py +++ b/tests/components/google_wifi/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Google Wifi platform.""" from datetime import datetime, timedelta import unittest -from unittest.mock import Mock, patch import requests_mock @@ -11,6 +10,7 @@ from homeassistant.setup import setup_component from homeassistant.util import dt as dt_util +from tests.async_mock import Mock, patch from tests.common import assert_setup_component, get_test_home_assistant NAME = "foo" diff --git a/tests/components/gpslogger/test_init.py b/tests/components/gpslogger/test_init.py index 9135f583d19d4c..bfdc087193ede7 100644 --- a/tests/components/gpslogger/test_init.py +++ b/tests/components/gpslogger/test_init.py @@ -1,6 +1,4 @@ """The tests the for GPSLogger device tracker platform.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow @@ -16,6 +14,8 @@ from homeassistant.helpers.dispatcher import DATA_DISPATCHER from homeassistant.setup import async_setup_component +from tests.async_mock import Mock, patch + HOME_LATITUDE = 37.239622 HOME_LONGITUDE = -115.815811 diff --git a/tests/components/graphite/test_init.py b/tests/components/graphite/test_init.py index 88be3723936fe9..19b8c165f37d5c 100644 --- a/tests/components/graphite/test_init.py +++ b/tests/components/graphite/test_init.py @@ -2,7 +2,6 @@ import socket import unittest from unittest import mock -from unittest.mock import patch import homeassistant.components.graphite as graphite from homeassistant.const import ( @@ -15,6 +14,7 @@ import homeassistant.core as ha from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 467bd1ede955bb..c4d98ad37ccbe3 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from collections import OrderedDict import unittest -from unittest.mock import patch import homeassistant.components.group as group from homeassistant.const import ( @@ -17,6 +16,7 @@ ) from homeassistant.setup import async_setup_component, setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant from tests.components.group import common diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index ed807ed57d9b20..70dab4472eddb8 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -1,6 +1,4 @@ """The tests for the Group Light platform.""" -from unittest.mock import MagicMock - from homeassistant.components.group import DOMAIN import homeassistant.components.group.light as group from homeassistant.components.light import ( @@ -31,6 +29,7 @@ from homeassistant.setup import async_setup_component import tests.async_mock +from tests.async_mock import MagicMock async def test_default_state(hass): diff --git a/tests/components/group/test_notify.py b/tests/components/group/test_notify.py index f029ec9d2fa9e0..0925b318c9e77e 100644 --- a/tests/components/group/test_notify.py +++ b/tests/components/group/test_notify.py @@ -1,13 +1,13 @@ """The tests for the notify.group platform.""" import asyncio import unittest -from unittest.mock import MagicMock, patch import homeassistant.components.demo.notify as demo import homeassistant.components.group.notify as group import homeassistant.components.notify as notify from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, patch from tests.common import assert_setup_component, get_test_home_assistant diff --git a/tests/components/group/test_reproduce_state.py b/tests/components/group/test_reproduce_state.py index 58bbc94876e275..13422cd082624a 100644 --- a/tests/components/group/test_reproduce_state.py +++ b/tests/components/group/test_reproduce_state.py @@ -1,11 +1,12 @@ """The tests for reproduction of state.""" from asyncio import Future -from unittest.mock import patch from homeassistant.components.group.reproduce_state import async_reproduce_states from homeassistant.core import Context, State +from tests.async_mock import patch + async def test_reproduce_group(hass): """Test reproduce_state with group.""" diff --git a/tests/components/hangouts/test_config_flow.py b/tests/components/hangouts/test_config_flow.py index 93f909d3bd4dc7..9cdb57999518e8 100644 --- a/tests/components/hangouts/test_config_flow.py +++ b/tests/components/hangouts/test_config_flow.py @@ -1,11 +1,11 @@ """Tests for the Google Hangouts config flow.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components.hangouts import config_flow from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from tests.async_mock import patch + EMAIL = "test@test.com" PASSWORD = "1232456" diff --git a/tests/components/hassio/test_http.py b/tests/components/hassio/test_http.py index a5af24eb8680e7..7386cf57d0ca6f 100644 --- a/tests/components/hassio/test_http.py +++ b/tests/components/hassio/test_http.py @@ -1,9 +1,10 @@ """The tests for the hassio component.""" import asyncio -from unittest.mock import patch import pytest +from tests.async_mock import patch + async def test_forward_request(hassio_client, aioclient_mock): """Test fetching normal path.""" diff --git a/tests/components/hddtemp/test_sensor.py b/tests/components/hddtemp/test_sensor.py index cbaed220f11a7b..4583079829aeab 100644 --- a/tests/components/hddtemp/test_sensor.py +++ b/tests/components/hddtemp/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the hddtemp platform.""" import socket import unittest -from unittest.mock import patch from homeassistant.const import TEMP_CELSIUS from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant VALID_CONFIG_MINIMAL = {"sensor": {"platform": "hddtemp"}} diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index d399f5b67aac51..fd2922e94feae0 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -1,6 +1,5 @@ """The test for the here_travel_time sensor platform.""" import logging -from unittest.mock import patch import urllib import herepy @@ -43,6 +42,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed, load_fixture DOMAIN = "sensor" diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index b2687b2bd509f5..29e43c8428ef8f 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -2,13 +2,13 @@ # pylint: disable=protected-access,invalid-name from datetime import timedelta import unittest -from unittest.mock import patch, sentinel from homeassistant.components import history, recorder import homeassistant.core as ha from homeassistant.setup import async_setup_component, setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch, sentinel from tests.common import ( get_test_home_assistant, init_recorder_component, diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 588e0df81dbfda..d9f489d20b4887 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -2,7 +2,6 @@ # pylint: disable=protected-access from datetime import datetime, timedelta import unittest -from unittest.mock import patch import pytest import pytz @@ -14,6 +13,7 @@ from homeassistant.setup import setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import get_test_home_assistant, init_recorder_component diff --git a/tests/components/home_connect/__init__.py b/tests/components/home_connect/__init__.py new file mode 100644 index 00000000000000..2b61501c59a765 --- /dev/null +++ b/tests/components/home_connect/__init__.py @@ -0,0 +1 @@ +"""Tests for the Home Connect integration.""" diff --git a/tests/components/home_connect/test_config_flow.py b/tests/components/home_connect/test_config_flow.py new file mode 100644 index 00000000000000..be6c21fe0a75ec --- /dev/null +++ b/tests/components/home_connect/test_config_flow.py @@ -0,0 +1,54 @@ +"""Test the Home Connect config flow.""" +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.home_connect.const import ( + DOMAIN, + OAUTH2_AUTHORIZE, + OAUTH2_TOKEN, +) +from homeassistant.helpers import config_entry_oauth2_flow + +CLIENT_ID = "1234" +CLIENT_SECRET = "5678" + + +async def test_full_flow(hass, aiohttp_client, aioclient_mock): + """Check full flow.""" + assert await setup.async_setup_component( + hass, + "home_connect", + { + "home_connect": {"client_id": CLIENT_ID, "client_secret": CLIENT_SECRET}, + "http": {"base_url": "https://example.com"}, + }, + ) + + result = await hass.config_entries.flow.async_init( + "home_connect", context={"source": config_entries.SOURCE_USER} + ) + state = config_entry_oauth2_flow._encode_jwt(hass, {"flow_id": result["flow_id"]}) + + assert result["type"] == data_entry_flow.RESULT_TYPE_EXTERNAL_STEP + assert result["url"] == ( + f"{OAUTH2_AUTHORIZE}?response_type=code&client_id={CLIENT_ID}" + "&redirect_uri=https://example.com/auth/external/callback" + f"&state={state}" + ) + + client = await aiohttp_client(hass.http.app) + resp = await client.get(f"/auth/external/callback?code=abcd&state={state}") + assert resp.status == 200 + assert resp.headers["content-type"] == "text/html; charset=utf-8" + + aioclient_mock.post( + OAUTH2_TOKEN, + json={ + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + }, + ) + + result = await hass.config_entries.flow.async_configure(result["flow_id"]) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 diff --git a/tests/components/homeassistant/test_scene.py b/tests/components/homeassistant/test_scene.py index 1371db87b3d6ae..6234d425d8da4a 100644 --- a/tests/components/homeassistant/test_scene.py +++ b/tests/components/homeassistant/test_scene.py @@ -1,12 +1,11 @@ """Test Home Assistant scenes.""" -from unittest.mock import patch - import pytest import voluptuous as vol from homeassistant.components.homeassistant import scene as ha_scene from homeassistant.setup import async_setup_component +from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/components/homekit/common.py b/tests/components/homekit/common.py index 6453bbbc788037..8f38332f1124bc 100644 --- a/tests/components/homekit/common.py +++ b/tests/components/homekit/common.py @@ -1,5 +1,5 @@ """Collection of fixtures and functions for the HomeKit tests.""" -from unittest.mock import patch +from tests.async_mock import patch def patch_debounce(): diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 7093bebf9abacc..d23bda67cee323 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -1,12 +1,12 @@ """HomeKit session fixtures.""" -from unittest.mock import patch - from pyhap.accessory_driver import AccessoryDriver import pytest from homeassistant.components.homekit.const import EVENT_HOMEKIT_CHANGED from homeassistant.core import callback as ha_callback +from tests.async_mock import patch + @pytest.fixture(scope="session") def hk_driver(): diff --git a/tests/components/homekit/test_accessories.py b/tests/components/homekit/test_accessories.py index e2fb79f56ce0e9..55b7f764d76093 100644 --- a/tests/components/homekit/test_accessories.py +++ b/tests/components/homekit/test_accessories.py @@ -3,7 +3,6 @@ This includes tests for all mock object types. """ from datetime import datetime, timedelta -from unittest.mock import Mock, patch import pytest @@ -43,6 +42,7 @@ ) import homeassistant.util.dt as dt_util +from tests.async_mock import Mock, patch from tests.common import async_mock_service diff --git a/tests/components/homekit/test_get_accessories.py b/tests/components/homekit/test_get_accessories.py index 286fe51535e6ab..e2433d51065ad9 100644 --- a/tests/components/homekit/test_get_accessories.py +++ b/tests/components/homekit/test_get_accessories.py @@ -1,6 +1,4 @@ """Package to test the get_accessory method.""" -from unittest.mock import Mock, patch - import pytest import homeassistant.components.climate as climate @@ -31,6 +29,8 @@ ) from homeassistant.core import State +from tests.async_mock import Mock, patch + def test_not_supported(caplog): """Test if none is returned if entity isn't supported.""" @@ -264,3 +264,15 @@ def test_type_vacuum(type_name, entity_id, state, attrs): entity_state = State(entity_id, state, attrs) get_accessory(None, None, entity_state, 2, {}) assert mock_type.called + + +@pytest.mark.parametrize( + "type_name, entity_id, state, attrs", [("Camera", "camera.basic", "on", {})], +) +def test_type_camera(type_name, entity_id, state, attrs): + """Test if camera types are associated correctly.""" + mock_type = Mock() + with patch.dict(TYPES, {type_name: mock_type}): + entity_state = State(entity_id, state, attrs) + get_accessory(None, None, entity_state, 2, {}) + assert mock_type.called diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index e5bee83a0ebe23..7bdfa0b14bde2c 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -1,7 +1,6 @@ """Tests for the HomeKit component.""" import os from typing import Dict -from unittest.mock import ANY, Mock, patch import pytest from zeroconf import InterfaceChoice @@ -55,7 +54,7 @@ from .util import PATH_HOMEKIT, async_init_entry, async_init_integration -from tests.async_mock import AsyncMock +from tests.async_mock import ANY, AsyncMock, Mock, patch from tests.common import MockConfigEntry, mock_device_registry, mock_registry from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py new file mode 100644 index 00000000000000..97716df3f1e89b --- /dev/null +++ b/tests/components/homekit/test_type_cameras.py @@ -0,0 +1,352 @@ +"""Test different accessory types: Camera.""" + +from uuid import UUID + +from pyhap.accessory_driver import AccessoryDriver +import pytest + +from homeassistant.components import camera, ffmpeg +from homeassistant.components.homekit.accessories import HomeBridge +from homeassistant.components.homekit.const import ( + CONF_STREAM_SOURCE, + CONF_SUPPORT_AUDIO, +) +from homeassistant.components.homekit.type_cameras import Camera +from homeassistant.components.homekit.type_switches import Switch +from homeassistant.exceptions import HomeAssistantError +from homeassistant.setup import async_setup_component + +from tests.async_mock import AsyncMock, MagicMock, patch + + +@pytest.fixture() +def run_driver(hass): + """Return a custom AccessoryDriver instance for HomeKit accessory init.""" + with patch("pyhap.accessory_driver.Zeroconf"), patch( + "pyhap.accessory_driver.AccessoryEncoder" + ), patch("pyhap.accessory_driver.HAPServer"), patch( + "pyhap.accessory_driver.AccessoryDriver.publish" + ), patch( + "pyhap.accessory_driver.AccessoryDriver.persist" + ): + yield AccessoryDriver( + pincode=b"123-45-678", address="127.0.0.1", loop=hass.loop + ) + + +def _get_working_mock_ffmpeg(): + """Return a working ffmpeg.""" + ffmpeg = MagicMock() + ffmpeg.open = AsyncMock(return_value=True) + ffmpeg.close = AsyncMock(return_value=True) + ffmpeg.kill = AsyncMock(return_value=True) + return ffmpeg + + +def _get_failing_mock_ffmpeg(): + """Return an ffmpeg that fails to shutdown.""" + ffmpeg = MagicMock() + ffmpeg.open = AsyncMock(return_value=False) + ffmpeg.close = AsyncMock(side_effect=OSError) + ffmpeg.kill = AsyncMock(side_effect=OSError) + return ffmpeg + + +async def test_camera_stream_source_configured(hass, run_driver, events): + """Test a camera that can stream with a configured source.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {CONF_STREAM_SOURCE: "/dev/null", CONF_SUPPORT_AUDIO: True}, + ) + not_camera_acc = Switch(hass, run_driver, "Switch", entity_id, 4, {},) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + bridge.add_accessory(not_camera_acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + stream_service = acc.get_service("CameraRTPStreamManagement") + endpoints_config_char = stream_service.get_characteristic("SetupEndpoints") + assert endpoints_config_char.setter_callback + stream_config_char = stream_service.get_characteristic( + "SelectedRTPStreamConfiguration" + ) + assert stream_config_char.setter_callback + acc.set_endpoints( + "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" + ) + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value=None, + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await hass.async_block_till_done() + + session_info = { + "id": "mock", + "v_srtp_key": "key", + "a_srtp_key": "key", + "v_port": "0", + "a_port": "0", + "address": "0.0.0.0", + } + acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await acc.stop_stream(session_info) + # Calling a second time should not throw + await acc.stop_stream(session_info) + await hass.async_block_till_done() + + assert await hass.async_add_executor_job(acc.get_snapshot, 1024) + + # Verify the bridge only forwards get_snapshot for + # cameras and valid accessory ids + assert await hass.async_add_executor_job(bridge.get_snapshot, {"aid": 2}) + with pytest.raises(ValueError): + assert await hass.async_add_executor_job(bridge.get_snapshot, {"aid": 3}) + with pytest.raises(ValueError): + assert await hass.async_add_executor_job(bridge.get_snapshot, {"aid": 4}) + + +async def test_camera_stream_source_configured_with_failing_ffmpeg( + hass, run_driver, events +): + """Test a camera that can stream with a configured source with ffmpeg failing.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera( + hass, + run_driver, + "Camera", + entity_id, + 2, + {CONF_STREAM_SOURCE: "/dev/null", CONF_SUPPORT_AUDIO: True}, + ) + not_camera_acc = Switch(hass, run_driver, "Switch", entity_id, 4, {},) + bridge = HomeBridge("hass", run_driver, "Test Bridge") + bridge.add_accessory(acc) + bridge.add_accessory(not_camera_acc) + + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + stream_service = acc.get_service("CameraRTPStreamManagement") + endpoints_config_char = stream_service.get_characteristic("SetupEndpoints") + assert endpoints_config_char.setter_callback + stream_config_char = stream_service.get_characteristic( + "SelectedRTPStreamConfiguration" + ) + assert stream_config_char.setter_callback + acc.set_endpoints( + "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" + ) + + session_info = { + "id": "mock", + "v_srtp_key": "key", + "a_srtp_key": "key", + "v_port": "0", + "a_port": "0", + "address": "0.0.0.0", + } + acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_failing_mock_ffmpeg(), + ): + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await acc.stop_stream(session_info) + # Calling a second time should not throw + await acc.stop_stream(session_info) + await hass.async_block_till_done() + + +async def test_camera_stream_source_found(hass, run_driver, events): + """Test a camera that can stream and we get the source from the entity.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + stream_service = acc.get_service("CameraRTPStreamManagement") + endpoints_config_char = stream_service.get_characteristic("SetupEndpoints") + assert endpoints_config_char.setter_callback + stream_config_char = stream_service.get_characteristic( + "SelectedRTPStreamConfiguration" + ) + assert stream_config_char.setter_callback + acc.set_endpoints( + "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" + ) + + session_info = { + "id": "mock", + "v_srtp_key": "key", + "a_srtp_key": "key", + "v_port": "0", + "a_port": "0", + "address": "0.0.0.0", + } + acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await acc.stop_stream(session_info) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + return_value="rtsp://example.local", + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await acc.stop_stream(session_info) + await hass.async_block_till_done() + + +async def test_camera_stream_source_fails(hass, run_driver, events): + """Test a camera that can stream and we cannot get the source from the entity.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component( + hass, camera.DOMAIN, {camera.DOMAIN: {"platform": "demo"}} + ) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + stream_service = acc.get_service("CameraRTPStreamManagement") + endpoints_config_char = stream_service.get_characteristic("SetupEndpoints") + assert endpoints_config_char.setter_callback + stream_config_char = stream_service.get_characteristic( + "SelectedRTPStreamConfiguration" + ) + assert stream_config_char.setter_callback + acc.set_endpoints( + "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" + ) + + session_info = { + "id": "mock", + "v_srtp_key": "key", + "a_srtp_key": "key", + "v_port": "0", + "a_port": "0", + "address": "0.0.0.0", + } + acc.sessions[UUID("3303d503-17cc-469a-b672-92436a71a2f6")] = session_info + + with patch( + "homeassistant.components.demo.camera.DemoCamera.stream_source", + side_effect=OSError, + ), patch( + "homeassistant.components.homekit.type_cameras.HAFFmpeg", + return_value=_get_working_mock_ffmpeg(), + ): + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await acc.stop_stream(session_info) + await hass.async_block_till_done() + + +async def test_camera_with_no_stream(hass, run_driver, events): + """Test a camera that cannot stream.""" + await async_setup_component(hass, ffmpeg.DOMAIN, {ffmpeg.DOMAIN: {}}) + await async_setup_component(hass, camera.DOMAIN, {camera.DOMAIN: {}}) + + entity_id = "camera.demo_camera" + + hass.states.async_set(entity_id, None) + await hass.async_block_till_done() + acc = Camera(hass, run_driver, "Camera", entity_id, 2, {},) + await acc.run_handler() + + assert acc.aid == 2 + assert acc.category == 17 # Camera + + acc.set_endpoints( + "ARAzA9UDF8xGmrZykkNqcaL2AgEAAxoBAQACDTE5Mi4xNjguMjA4LjUDAi7IBAKkxwQlAQEAAhDN0+Y0tZ4jzoO0ske9UsjpAw6D76oVXnoi7DbawIG4CwUlAQEAAhCyGcROB8P7vFRDzNF2xrK1Aw6NdcLugju9yCfkWVSaVAYEDoAsAAcEpxV8AA==" + ) + acc.set_selected_stream_configuration( + "ARUCAQEBEDMD1QMXzEaatnKSQ2pxovYCNAEBAAIJAQECAgECAwEAAwsBAgAFAgLQAgMBHgQXAQFjAgQ768/RAwIrAQQEAAAAPwUCYgUDLAEBAwIMAQEBAgEAAwECBAEUAxYBAW4CBCzq28sDAhgABAQAAKBABgENBAEA" + ) + await hass.async_block_till_done() + + with pytest.raises(HomeAssistantError): + await hass.async_add_executor_job(acc.get_snapshot, 1024) diff --git a/tests/components/homekit/test_type_fans.py b/tests/components/homekit/test_type_fans.py index 4d2ace24eab87d..6d5b0f9841b6eb 100644 --- a/tests/components/homekit/test_type_fans.py +++ b/tests/components/homekit/test_type_fans.py @@ -1,6 +1,5 @@ """Test different accessory types: Fans.""" from collections import namedtuple -from unittest.mock import Mock from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -33,6 +32,7 @@ from homeassistant.core import CoreState from homeassistant.helpers import entity_registry +from tests.async_mock import Mock from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_type_media_players.py b/tests/components/homekit/test_type_media_players.py index cb2de7264a8b2f..9b8cf074d5786a 100644 --- a/tests/components/homekit/test_type_media_players.py +++ b/tests/components/homekit/test_type_media_players.py @@ -369,6 +369,26 @@ async def test_media_player_television_basic(hass, hk_driver, events, caplog): assert not caplog.messages or "Error" not in caplog.messages[-1] +async def test_media_player_television_supports_source_select_no_sources( + hass, hk_driver, events, caplog +): + """Test if basic tv that supports source select but is missing a source list.""" + entity_id = "media_player.television" + + # Supports turn_on', 'turn_off' + hass.states.async_set( + entity_id, + None, + {ATTR_DEVICE_CLASS: DEVICE_CLASS_TV, ATTR_SUPPORTED_FEATURES: 3469}, + ) + await hass.async_block_till_done() + acc = TelevisionMediaPlayer(hass, hk_driver, "MediaPlayer", entity_id, 2, None) + await acc.run_handler() + await hass.async_block_till_done() + + assert acc.support_select_source is False + + async def test_tv_restore(hass, hk_driver, events): """Test setting up an entity from state in the event registry.""" hass.state = CoreState.not_running diff --git a/tests/components/homekit/test_type_thermostats.py b/tests/components/homekit/test_type_thermostats.py index 82abed32c0e216..8a303ede876952 100644 --- a/tests/components/homekit/test_type_thermostats.py +++ b/tests/components/homekit/test_type_thermostats.py @@ -1,6 +1,5 @@ """Test different accessory types: Thermostats.""" from collections import namedtuple -from unittest.mock import patch from pyhap.const import HAP_REPR_AID, HAP_REPR_CHARS, HAP_REPR_IID, HAP_REPR_VALUE import pytest @@ -56,6 +55,7 @@ from homeassistant.core import CoreState from homeassistant.helpers import entity_registry +from tests.async_mock import patch from tests.common import async_mock_service from tests.components.homekit.common import patch_debounce diff --git a/tests/components/homekit/test_util.py b/tests/components/homekit/test_util.py index 2c8c93cee4c342..d5ff923270b605 100644 --- a/tests/components/homekit/test_util.py +++ b/tests/components/homekit/test_util.py @@ -23,6 +23,7 @@ from homeassistant.components.homekit.util import ( HomeKitSpeedMapping, SpeedRange, + cleanup_name_for_homekit, convert_to_float, density_to_air_quality, dismiss_setup_message, @@ -177,6 +178,19 @@ def test_convert_to_float(): assert convert_to_float(None) is None +def test_cleanup_name_for_homekit(): + """Ensure name sanitize works as expected.""" + + assert cleanup_name_for_homekit("abc") == "abc" + assert cleanup_name_for_homekit("a b c") == "a b c" + assert cleanup_name_for_homekit("ab_c") == "ab c" + assert ( + cleanup_name_for_homekit('ab!@#$%^&*()-=":.,> str: + """Get the UDN.""" + return self._udn + + @property + def manufacturer(self) -> str: + """Get manufacturer.""" + return "mock-manufacturer" + + @property + def name(self) -> str: + """Get name.""" + return "mock-name" + + @property + def model_name(self) -> str: + """Get the model name.""" + return "mock-model-name" + + @property + def device_type(self) -> str: + """Get the device type.""" + return "urn:schemas-upnp-org:device:InternetGatewayDevice:1" + + async def _async_add_port_mapping( + self, external_port: int, local_ip: str, internal_port: int + ) -> None: + """Add a port mapping.""" + entry = [external_port, local_ip, internal_port] + self.added_port_mappings.append(entry) + + async def _async_delete_port_mapping(self, external_port: int) -> None: + """Remove a port mapping.""" + entry = external_port + self.removed_port_mappings.append(entry) + + async def async_get_traffic_data(self) -> Mapping[str, any]: + """Get traffic data.""" + return { + TIMESTAMP: dt_util.utcnow(), + BYTES_RECEIVED: 0, + BYTES_SENT: 0, + PACKETS_RECEIVED: 0, + PACKETS_SENT: 0, + } diff --git a/tests/components/upnp/test_config_flow.py b/tests/components/upnp/test_config_flow.py new file mode 100644 index 00000000000000..c6e383bae554ea --- /dev/null +++ b/tests/components/upnp/test_config_flow.py @@ -0,0 +1,124 @@ +"""Test UPnP/IGD config flow.""" + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components import ssdp +from homeassistant.components.upnp.const import ( + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, + DISCOVERY_USN, + DOMAIN, +) +from homeassistant.components.upnp.device import Device +from homeassistant.helpers.typing import HomeAssistantType + +from .mock_device import MockDevice + +from tests.async_mock import AsyncMock, patch + + +async def test_flow_ssdp_discovery(hass: HomeAssistantType): + """Test config flow: discovered + configured through ssdp.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + discovery_infos = [ + { + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step ssdp. + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data={ + ssdp.ATTR_SSDP_ST: mock_device.device_type, + ssdp.ATTR_UPNP_UDN: mock_device.udn, + "friendlyName": mock_device.name, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "ssdp_confirm" + + # Confirm via step ssdp_confirm. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == mock_device.name + assert result["data"] == { + "st": mock_device.device_type, + "udn": mock_device.udn, + } + + +async def test_flow_user(hass: HomeAssistantType): + """Test config flow: discovered + configured through user.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + usn = f"{mock_device.udn}::{mock_device.device_type}" + discovery_infos = [ + { + DISCOVERY_USN: usn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step user. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # Confirmed via step user. + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"usn": usn}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == mock_device.name + assert result["data"] == { + "st": mock_device.device_type, + "udn": mock_device.udn, + } + + +async def test_flow_config(hass: HomeAssistantType): + """Test config flow: discovered + configured through configuration.yaml.""" + udn = "uuid:device_1" + mock_device = MockDevice(udn) + usn = f"{mock_device.udn}::{mock_device.device_type}" + discovery_infos = [ + { + DISCOVERY_USN: usn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_LOCATION: "dummy", + } + ] + + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", AsyncMock(return_value=discovery_infos)): + # Discovered via step import. + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == mock_device.name + assert result["data"] == { + "st": mock_device.device_type, + "udn": mock_device.udn, + } diff --git a/tests/components/upnp/test_init.py b/tests/components/upnp/test_init.py index 7c43c24cdc3e17..960d6dacfe59e3 100644 --- a/tests/components/upnp/test_init.py +++ b/tests/components/upnp/test_init.py @@ -1,147 +1,53 @@ """Test UPnP/IGD setup process.""" -from ipaddress import IPv4Address - from homeassistant.components import upnp +from homeassistant.components.upnp.const import ( + DISCOVERY_LOCATION, + DISCOVERY_ST, + DISCOVERY_UDN, +) from homeassistant.components.upnp.device import Device from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.setup import async_setup_component -from tests.async_mock import patch -from tests.common import MockConfigEntry, mock_coro - - -class MockDevice(Device): - """Mock device for Device.""" - - def __init__(self, udn): - """Initialize mock device.""" - igd_device = object() - super().__init__(igd_device) - self._udn = udn - self.added_port_mappings = [] - self.removed_port_mappings = [] - - @classmethod - async def async_create_device(cls, hass, ssdp_description): - """Return self.""" - return cls("UDN") - - @property - def udn(self) -> str: - """Get the UDN.""" - return self._udn - - @property - def manufacturer(self) -> str: - """Get manufacturer.""" - return "mock-manufacturer" - - @property - def name(self) -> str: - """Get name.""" - return "mock-name" - - @property - def model_name(self) -> str: - """Get the model name.""" - return "mock-model-name" +from .mock_device import MockDevice - @property - def device_type(self) -> str: - """Get the device type.""" - return "urn:schemas-upnp-org:device:InternetGatewayDevice:1" - - async def _async_add_port_mapping( - self, external_port: int, local_ip: str, internal_port: int - ) -> None: - """Add a port mapping.""" - entry = [external_port, local_ip, internal_port] - self.added_port_mappings.append(entry) - - async def _async_delete_port_mapping(self, external_port: int) -> None: - """Remove a port mapping.""" - entry = external_port - self.removed_port_mappings.append(entry) +from tests.async_mock import AsyncMock, patch +from tests.common import MockConfigEntry async def test_async_setup_entry_default(hass): """Test async_setup_entry.""" udn = "uuid:device_1" - entry = MockConfigEntry(domain=upnp.DOMAIN) + mock_device = MockDevice(udn) + discovery_infos = [ + { + DISCOVERY_UDN: mock_device.udn, + DISCOVERY_ST: mock_device.device_type, + DISCOVERY_LOCATION: "http://192.168.1.1/desc.xml", + } + ] + entry = MockConfigEntry( + domain=upnp.DOMAIN, data={"udn": mock_device.udn, "st": mock_device.device_type} + ) config = { # no upnp } - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover", return_value=mock_coro([]) - ) as async_discover: - await async_setup_component(hass, "upnp", config) - await hass.async_block_till_done() - - # mock homeassistant.components.upnp.device.Device - mock_device = MockDevice(udn) - discovery_infos = [ - {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} - ] - - create_device.return_value = mock_device - async_discover.return_value = discovery_infos - - assert await upnp.async_setup_entry(hass, entry) is True - - # ensure device is stored/used - assert hass.data[upnp.DOMAIN]["devices"][udn] == mock_device - - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) - await hass.async_block_till_done() - - # ensure no port-mappings created or removed - assert not mock_device.added_port_mappings - assert not mock_device.removed_port_mappings - - -async def test_async_setup_entry_port_mapping(hass): - """Test async_setup_entry.""" - # pylint: disable=invalid-name - udn = "uuid:device_1" - entry = MockConfigEntry(domain=upnp.DOMAIN) - - config = { - "http": {}, - "upnp": { - "local_ip": "192.168.1.10", - "port_mapping": True, - "ports": {"hass": "hass"}, - }, - } - with patch.object(Device, "async_create_device") as create_device, patch.object( - Device, "async_discover", return_value=mock_coro([]) - ) as async_discover: - await async_setup_component(hass, "http", config) + async_discover = AsyncMock(return_value=[]) + with patch.object( + Device, "async_create_device", AsyncMock(return_value=mock_device) + ), patch.object(Device, "async_discover", async_discover): + # initialisation of component, no device discovered await async_setup_component(hass, "upnp", config) await hass.async_block_till_done() - mock_device = MockDevice(udn) - discovery_infos = [ - {"udn": udn, "ssdp_description": "http://192.168.1.1/desc.xml"} - ] - - create_device.return_value = mock_device + # loading of config_entry, device discovered async_discover.return_value = discovery_infos - assert await upnp.async_setup_entry(hass, entry) is True # ensure device is stored/used assert hass.data[upnp.DOMAIN]["devices"][udn] == mock_device - # ensure add-port-mapping-methods called - assert mock_device.added_port_mappings == [ - [8123, IPv4Address("192.168.1.10"), 8123] - ] - hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) await hass.async_block_till_done() - - # ensure delete-port-mapping-methods called - assert mock_device.removed_port_mappings == [8123] diff --git a/tests/components/uptime/test_sensor.py b/tests/components/uptime/test_sensor.py index 0a9d227681b1be..111114d8aca60e 100644 --- a/tests/components/uptime/test_sensor.py +++ b/tests/components/uptime/test_sensor.py @@ -2,11 +2,11 @@ import asyncio from datetime import timedelta import unittest -from unittest.mock import patch from homeassistant.components.uptime.sensor import UptimeSensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant diff --git a/tests/components/usgs_earthquakes_feed/test_geo_location.py b/tests/components/usgs_earthquakes_feed/test_geo_location.py index 4823d2eb2da70d..9bd718d1933c45 100644 --- a/tests/components/usgs_earthquakes_feed/test_geo_location.py +++ b/tests/components/usgs_earthquakes_feed/test_geo_location.py @@ -1,6 +1,5 @@ """The tests for the USGS Earthquake Hazards Program Feed platform.""" import datetime -from unittest.mock import MagicMock, call, patch from homeassistant.components import geo_location from homeassistant.components.geo_location import ATTR_SOURCE @@ -32,6 +31,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, call, patch from tests.common import assert_setup_component, async_fire_time_changed CONFIG = { diff --git a/tests/components/utility_meter/test_init.py b/tests/components/utility_meter/test_init.py index f6c1e6c8ead2b8..7116077177a8aa 100644 --- a/tests/components/utility_meter/test_init.py +++ b/tests/components/utility_meter/test_init.py @@ -1,7 +1,6 @@ """The tests for the utility_meter component.""" from datetime import timedelta import logging -from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -19,6 +18,8 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch + _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 6118d74d0dd806..09145fc4e4ebe7 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -2,7 +2,6 @@ from contextlib import contextmanager from datetime import timedelta import logging -from unittest.mock import patch from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.utility_meter.const import ( @@ -20,6 +19,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -119,6 +119,17 @@ async def test_state(hass): assert state is not None assert state.state == "100" + await hass.services.async_call( + DOMAIN, + SERVICE_CALIBRATE_METER, + {ATTR_ENTITY_ID: "sensor.energy_bill_midpeak", ATTR_VALUE: "0.123"}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("sensor.energy_bill_midpeak") + assert state is not None + assert state.state == "0.123" + async def test_net_consumption(hass): """Test utility sensor state.""" diff --git a/tests/components/velbus/test_config_flow.py b/tests/components/velbus/test_config_flow.py index daeffb4ed1d2d7..3bccacc0a94faa 100644 --- a/tests/components/velbus/test_config_flow.py +++ b/tests/components/velbus/test_config_flow.py @@ -1,12 +1,11 @@ """Tests for the Velbus config flow.""" -from unittest.mock import Mock, patch - import pytest from homeassistant import data_entry_flow from homeassistant.components.velbus import config_flow from homeassistant.const import CONF_NAME, CONF_PORT +from tests.async_mock import Mock, patch from tests.common import MockConfigEntry PORT_SERIAL = "/dev/ttyACME100" diff --git a/tests/components/vera/test_binary_sensor.py b/tests/components/vera/test_binary_sensor.py index 72651d6eda4f74..4b0d41d9a1eb9b 100644 --- a/tests/components/vera/test_binary_sensor.py +++ b/tests/components/vera/test_binary_sensor.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_binary_sensor( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_climate.py b/tests/components/vera/test_climate.py index 9e5fa983ed053f..f52bf375d8eb1d 100644 --- a/tests/components/vera/test_climate.py +++ b/tests/components/vera/test_climate.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.components.climate.const import ( @@ -15,6 +13,8 @@ from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_climate( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_config_flow.py b/tests/components/vera/test_config_flow.py index 52ba55b509c8ac..3915d4d0577c4a 100644 --- a/tests/components/vera/test_config_flow.py +++ b/tests/components/vera/test_config_flow.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - from mock import patch from requests.exceptions import RequestException @@ -14,6 +12,7 @@ RESULT_TYPE_FORM, ) +from tests.async_mock import MagicMock from tests.common import MockConfigEntry @@ -44,8 +43,8 @@ async def test_async_step_user_success(hass: HomeAssistant) -> None: assert result["data"] == { CONF_CONTROLLER: "http://127.0.0.1:123", CONF_SOURCE: config_entries.SOURCE_USER, - CONF_LIGHTS: ["12", "13"], - CONF_EXCLUDE: ["14", "15"], + CONF_LIGHTS: [12, 13], + CONF_EXCLUDE: [14, 15], } assert result["result"].unique_id == controller.serial_number @@ -154,6 +153,6 @@ async def test_options(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] == { - CONF_LIGHTS: ["1", "2", "3", "4", "5", "6", "7"], - CONF_EXCLUDE: ["8", "9", "10", "11", "12", "13", "14"], + CONF_LIGHTS: [1, 2, 3, 4, 5, 6, 7], + CONF_EXCLUDE: [8, 9, 10, 11, 12, 13, 14], } diff --git a/tests/components/vera/test_cover.py b/tests/components/vera/test_cover.py index 62cd47f831cdc3..a2dae2bd7f808b 100644 --- a/tests/components/vera/test_cover.py +++ b/tests/components/vera/test_cover.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_cover( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index f1e13a5f2088c7..210037a2ca3c5c 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -1,8 +1,14 @@ """Vera tests.""" +import pytest import pyvera as pv from requests.exceptions import RequestException -from homeassistant.components.vera import CONF_CONTROLLER, DOMAIN +from homeassistant.components.vera import ( + CONF_CONTROLLER, + CONF_EXCLUDE, + CONF_LIGHTS, + DOMAIN, +) from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED from homeassistant.core import HomeAssistant @@ -110,3 +116,71 @@ def setup_callback(controller: pv.VeraController) -> None: entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(entry.entry_id) + + +@pytest.mark.parametrize( + ["options"], + [ + [{CONF_LIGHTS: [4, 10, 12, "AAA"], CONF_EXCLUDE: [1, "BBB"]}], + [{CONF_LIGHTS: ["4", "10", "12", "AAA"], CONF_EXCLUDE: ["1", "BBB"]}], + ], +) +async def test_exclude_and_light_ids( + hass: HomeAssistant, vera_component_factory: ComponentFactory, options +) -> None: + """Test device exclusion, marking switches as lights and fixing the data type.""" + vera_device1 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device1.device_id = 1 + vera_device1.vera_device_id = 1 + vera_device1.name = "dev1" + vera_device1.is_tripped = False + entity_id1 = "binary_sensor.dev1_1" + + vera_device2 = MagicMock(spec=pv.VeraBinarySensor) # type: pv.VeraBinarySensor + vera_device2.device_id = 2 + vera_device2.vera_device_id = 2 + vera_device2.name = "dev2" + vera_device2.is_tripped = False + entity_id2 = "binary_sensor.dev2_2" + + vera_device3 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch + vera_device3.device_id = 3 + vera_device3.name = "dev3" + vera_device3.category = pv.CATEGORY_SWITCH + vera_device3.is_switched_on = MagicMock(return_value=False) + entity_id3 = "switch.dev3_3" + + vera_device4 = MagicMock(spec=pv.VeraSwitch) # type: pv.VeraSwitch + vera_device4.device_id = 4 + vera_device4.name = "dev4" + vera_device4.category = pv.CATEGORY_SWITCH + vera_device4.is_switched_on = MagicMock(return_value=False) + entity_id4 = "light.dev4_4" + + component_data = await vera_component_factory.configure_component( + hass=hass, + controller_config=new_simple_controller_config( + devices=(vera_device1, vera_device2, vera_device3, vera_device4), + config={**{CONF_CONTROLLER: "http://127.0.0.1:123"}, **options}, + ), + ) + + # Assert the entries were setup correctly. + config_entry = next(iter(hass.config_entries.async_entries(DOMAIN))) + assert config_entry.options == { + CONF_LIGHTS: [4, 10, 12], + CONF_EXCLUDE: [1], + } + + update_callback = component_data.controller_data.update_callback + + update_callback(vera_device1) + update_callback(vera_device2) + update_callback(vera_device3) + update_callback(vera_device4) + await hass.async_block_till_done() + + assert hass.states.get(entity_id1) is None + assert hass.states.get(entity_id2) is not None + assert hass.states.get(entity_id3) is not None + assert hass.states.get(entity_id4) is not None diff --git a/tests/components/vera/test_light.py b/tests/components/vera/test_light.py index fefa07ffa6ee40..14194d0af52891 100644 --- a/tests/components/vera/test_light.py +++ b/tests/components/vera/test_light.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.components.light import ATTR_BRIGHTNESS, ATTR_HS_COLOR @@ -8,6 +6,8 @@ from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_light( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_lock.py b/tests/components/vera/test_lock.py index d1b2209294a375..901e09040e9d91 100644 --- a/tests/components/vera/test_lock.py +++ b/tests/components/vera/test_lock.py @@ -1,6 +1,4 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED @@ -8,6 +6,8 @@ from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_lock( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_scene.py b/tests/components/vera/test_scene.py index 732a331681bdb2..8f96b7a133ad59 100644 --- a/tests/components/vera/test_scene.py +++ b/tests/components/vera/test_scene.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_scene( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/vera/test_sensor.py b/tests/components/vera/test_sensor.py index c915c5ead0fd17..cb50ad82789abe 100644 --- a/tests/components/vera/test_sensor.py +++ b/tests/components/vera/test_sensor.py @@ -1,6 +1,5 @@ """Vera tests.""" from typing import Any, Callable, Tuple -from unittest.mock import MagicMock import pyvera as pv @@ -9,6 +8,8 @@ from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def run_sensor_test( hass: HomeAssistant, diff --git a/tests/components/vera/test_switch.py b/tests/components/vera/test_switch.py index c41afad4759f8f..2a8bfe68185f1c 100644 --- a/tests/components/vera/test_switch.py +++ b/tests/components/vera/test_switch.py @@ -1,12 +1,12 @@ """Vera tests.""" -from unittest.mock import MagicMock - import pyvera as pv from homeassistant.core import HomeAssistant from .common import ComponentFactory, new_simple_controller_config +from tests.async_mock import MagicMock + async def test_switch( hass: HomeAssistant, vera_component_factory: ComponentFactory diff --git a/tests/components/verisure/test_ethernet_status.py b/tests/components/verisure/test_ethernet_status.py index 611adde19d9fd3..139ac01a1c6c1d 100644 --- a/tests/components/verisure/test_ethernet_status.py +++ b/tests/components/verisure/test_ethernet_status.py @@ -1,11 +1,12 @@ """Test Verisure ethernet status.""" from contextlib import contextmanager -from unittest.mock import patch from homeassistant.components.verisure import DOMAIN as VERISURE_DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.setup import async_setup_component +from tests.async_mock import patch + CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/verisure/test_lock.py b/tests/components/verisure/test_lock.py index d41bbab20379b2..decce67dc11899 100644 --- a/tests/components/verisure/test_lock.py +++ b/tests/components/verisure/test_lock.py @@ -1,7 +1,6 @@ """Tests for the Verisure platform.""" from contextlib import contextmanager -from unittest.mock import call, patch from homeassistant.components.lock import ( DOMAIN as LOCK_DOMAIN, @@ -12,6 +11,8 @@ from homeassistant.const import STATE_UNLOCKED from homeassistant.setup import async_setup_component +from tests.async_mock import call, patch + NO_DEFAULT_LOCK_CODE_CONFIG = { "verisure": { "username": "test", diff --git a/tests/components/version/test_sensor.py b/tests/components/version/test_sensor.py index 164b4090e5ff82..471043ae3ae33d 100644 --- a/tests/components/version/test_sensor.py +++ b/tests/components/version/test_sensor.py @@ -1,8 +1,8 @@ """The test for the version sensor platform.""" -from unittest.mock import patch - from homeassistant.setup import async_setup_component +from tests.async_mock import patch + MOCK_VERSION = "10.0" diff --git a/tests/components/vesync/test_config_flow.py b/tests/components/vesync/test_config_flow.py index 39b847effc5645..aedf94da4abfe4 100644 --- a/tests/components/vesync/test_config_flow.py +++ b/tests/components/vesync/test_config_flow.py @@ -1,10 +1,9 @@ """Test for vesync config flow.""" -from unittest.mock import patch - from homeassistant import data_entry_flow from homeassistant.components.vesync import DOMAIN, config_flow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from tests.async_mock import patch from tests.common import MockConfigEntry diff --git a/tests/components/vizio/test_media_player.py b/tests/components/vizio/test_media_player.py index b4a2148b8daa52..7a2ff1d1c7ac77 100644 --- a/tests/components/vizio/test_media_player.py +++ b/tests/components/vizio/test_media_player.py @@ -3,7 +3,6 @@ from datetime import timedelta import logging from typing import Any, Dict, List, Optional -from unittest.mock import call import pytest from pytest import raises @@ -73,7 +72,7 @@ VOLUME_STEP, ) -from tests.async_mock import patch +from tests.async_mock import call, patch from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) diff --git a/tests/components/vultr/test_binary_sensor.py b/tests/components/vultr/test_binary_sensor.py index f57926f30c843c..609cdbf6a9ed27 100644 --- a/tests/components/vultr/test_binary_sensor.py +++ b/tests/components/vultr/test_binary_sensor.py @@ -1,7 +1,6 @@ """Test the Vultr binary sensor platform.""" import json import unittest -from unittest.mock import patch import pytest import requests_mock @@ -20,6 +19,7 @@ ) from homeassistant.const import CONF_NAME, CONF_PLATFORM +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_init.py b/tests/components/vultr/test_init.py index e371e785c92844..6035ac547afd0a 100644 --- a/tests/components/vultr/test_init.py +++ b/tests/components/vultr/test_init.py @@ -2,13 +2,13 @@ from copy import deepcopy import json import unittest -from unittest.mock import patch import requests_mock from homeassistant import setup import homeassistant.components.vultr as vultr +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture VALID_CONFIG = {"vultr": {"api_key": "ABCDEFG1234567"}} diff --git a/tests/components/vultr/test_sensor.py b/tests/components/vultr/test_sensor.py index 80fd05a41ccddf..1ced0fec82f195 100644 --- a/tests/components/vultr/test_sensor.py +++ b/tests/components/vultr/test_sensor.py @@ -1,7 +1,6 @@ """The tests for the Vultr sensor platform.""" import json import unittest -from unittest.mock import patch import pytest import requests_mock @@ -17,6 +16,7 @@ DATA_GIGABYTES, ) +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/vultr/test_switch.py b/tests/components/vultr/test_switch.py index 6a5c382a2d2b59..594617bdfd9a81 100644 --- a/tests/components/vultr/test_switch.py +++ b/tests/components/vultr/test_switch.py @@ -1,7 +1,6 @@ """Test the Vultr switch platform.""" import json import unittest -from unittest.mock import patch import pytest import requests_mock @@ -20,6 +19,7 @@ ) from homeassistant.const import CONF_NAME, CONF_PLATFORM +from tests.async_mock import patch from tests.common import get_test_home_assistant, load_fixture from tests.components.vultr.test_init import VALID_CONFIG diff --git a/tests/components/wake_on_lan/test_switch.py b/tests/components/wake_on_lan/test_switch.py index e0f12f9c7f8ee2..ed4045bcb4479a 100644 --- a/tests/components/wake_on_lan/test_switch.py +++ b/tests/components/wake_on_lan/test_switch.py @@ -1,11 +1,11 @@ """The tests for the wake on lan switch platform.""" import unittest -from unittest.mock import patch import homeassistant.components.switch as switch from homeassistant.const import STATE_OFF, STATE_ON from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import get_test_home_assistant, mock_service from tests.components.switch import common diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 733ed32da787c4..7d4ce563b039c7 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -1,10 +1,10 @@ """Test the webhook component.""" -from unittest.mock import Mock - import pytest from homeassistant.setup import async_setup_component +from tests.async_mock import Mock + @pytest.fixture def mock_client(hass, hass_client): diff --git a/tests/components/webostv/test_media_player.py b/tests/components/webostv/test_media_player.py index cc889eca064c84..e4395769562595 100644 --- a/tests/components/webostv/test_media_player.py +++ b/tests/components/webostv/test_media_player.py @@ -25,7 +25,7 @@ from homeassistant.setup import async_setup_component if sys.version_info >= (3, 8, 0): - from unittest.mock import patch + from tests.async_mock import patch else: from tests.async_mock import patch diff --git a/tests/components/websocket_api/test_init.py b/tests/components/websocket_api/test_init.py index 041c0e76533f9d..2d656de8eeb0b3 100644 --- a/tests/components/websocket_api/test_init.py +++ b/tests/components/websocket_api/test_init.py @@ -1,11 +1,11 @@ """Tests for the Home Assistant Websocket API.""" -from unittest.mock import Mock, patch - from aiohttp import WSMsgType import voluptuous as vol from homeassistant.components.websocket_api import const, messages +from tests.async_mock import Mock, patch + async def test_invalid_message_format(websocket_client): """Test sending invalid JSON.""" diff --git a/tests/components/withings/test_common.py b/tests/components/withings/test_common.py index f0f3a16033248c..f0528c36005b75 100644 --- a/tests/components/withings/test_common.py +++ b/tests/components/withings/test_common.py @@ -1,6 +1,5 @@ """Tests for the Withings component.""" from datetime import timedelta -from unittest.mock import patch import pytest from withings_api import WithingsApi @@ -13,7 +12,7 @@ from homeassistant.exceptions import PlatformNotReady from homeassistant.util import dt -from tests.async_mock import MagicMock +from tests.async_mock import MagicMock, patch @pytest.fixture(name="withings_api") diff --git a/tests/components/workday/test_binary_sensor.py b/tests/components/workday/test_binary_sensor.py index 29d5e4f03ef899..c476c9fd0e04a7 100644 --- a/tests/components/workday/test_binary_sensor.py +++ b/tests/components/workday/test_binary_sensor.py @@ -1,6 +1,5 @@ """Tests the Home Assistant workday binary sensor.""" from datetime import date -from unittest.mock import patch import pytest import voluptuous as vol @@ -8,6 +7,7 @@ import homeassistant.components.workday.binary_sensor as binary_sensor from homeassistant.setup import setup_component +from tests.async_mock import patch from tests.common import assert_setup_component, get_test_home_assistant FUNCTION_PATH = "homeassistant.components.workday.binary_sensor.get_date" diff --git a/tests/components/xiaomi_miio/test_config_flow.py b/tests/components/xiaomi_miio/test_config_flow.py index 023deb93c81c03..619293be67626f 100644 --- a/tests/components/xiaomi_miio/test_config_flow.py +++ b/tests/components/xiaomi_miio/test_config_flow.py @@ -1,13 +1,11 @@ """Test the Xiaomi Miio config flow.""" -from unittest.mock import Mock - from miio import DeviceException from homeassistant import config_entries from homeassistant.components.xiaomi_miio import config_flow, const from homeassistant.const import CONF_HOST, CONF_NAME, CONF_TOKEN -from tests.async_mock import patch +from tests.async_mock import Mock, patch TEST_HOST = "1.2.3.4" TEST_TOKEN = "12345678901234567890123456789012" diff --git a/tests/components/yamaha/test_media_player.py b/tests/components/yamaha/test_media_player.py index 6b101167c85c20..2f47ddc7355b13 100644 --- a/tests/components/yamaha/test_media_player.py +++ b/tests/components/yamaha/test_media_player.py @@ -1,11 +1,11 @@ """The tests for the Yamaha Media player platform.""" import unittest -from unittest.mock import MagicMock, patch import homeassistant.components.media_player as mp from homeassistant.components.yamaha import media_player as yamaha from homeassistant.setup import setup_component +from tests.async_mock import MagicMock, patch from tests.common import get_test_home_assistant diff --git a/tests/components/yessssms/test_notify.py b/tests/components/yessssms/test_notify.py index 992185bf1024a5..d940e782be4402 100644 --- a/tests/components/yessssms/test_notify.py +++ b/tests/components/yessssms/test_notify.py @@ -1,7 +1,6 @@ """The tests for the notify yessssms platform.""" import logging import unittest -from unittest.mock import patch import pytest import requests_mock @@ -16,6 +15,8 @@ ) from homeassistant.setup import async_setup_component +from tests.async_mock import patch + @pytest.fixture(name="config") def config_data(): diff --git a/tests/components/yr/test_sensor.py b/tests/components/yr/test_sensor.py index d676e88bfc3223..d8dcbe367de5b4 100644 --- a/tests/components/yr/test_sensor.py +++ b/tests/components/yr/test_sensor.py @@ -1,11 +1,11 @@ """The tests for the Yr sensor platform.""" from datetime import datetime -from unittest.mock import patch from homeassistant.bootstrap import async_setup_component from homeassistant.const import DEGREE, SPEED_METERS_PER_SECOND, UNIT_PERCENTAGE import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import assert_setup_component, load_fixture NOW = datetime(2016, 6, 9, 1, tzinfo=dt_util.UTC) diff --git a/tests/components/zha/common.py b/tests/components/zha/common.py index 9b139317825585..b16ae1d488e0db 100644 --- a/tests/components/zha/common.py +++ b/tests/components/zha/common.py @@ -1,6 +1,5 @@ """Common test objects.""" import time -from unittest.mock import Mock from zigpy.device import Device as zigpy_dev from zigpy.endpoint import Endpoint as zigpy_ep @@ -14,7 +13,7 @@ import homeassistant.components.zha.core.const as zha_const from homeassistant.util import slugify -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, Mock class FakeEndpoint: @@ -33,7 +32,7 @@ def __init__(self, manufacturer, model, epid=1): self.model = model self.profile_id = zigpy.profiles.zha.PROFILE_ID self.device_type = None - self.request = AsyncMock() + self.request = AsyncMock(return_value=[0]) def add_input_cluster(self, cluster_id): """Add an input cluster.""" @@ -61,6 +60,7 @@ def unique_id(self): FakeEndpoint.add_to_group = zigpy_ep.add_to_group +FakeEndpoint.remove_from_group = zigpy_ep.remove_from_group def patch_cluster(cluster): diff --git a/tests/components/zha/test_api.py b/tests/components/zha/test_api.py index b67a39cd3aba28..88fd1e8437feb0 100644 --- a/tests/components/zha/test_api.py +++ b/tests/components/zha/test_api.py @@ -244,21 +244,26 @@ async def test_list_groupable_devices(zha_client, device_groupable): assert msg["id"] == 10 assert msg["type"] == const.TYPE_RESULT - devices = msg["result"] - assert len(devices) == 1 - - for device in devices: - assert device[ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" - assert device[ATTR_MANUFACTURER] is not None - assert device[ATTR_MODEL] is not None - assert device[ATTR_NAME] is not None - assert device[ATTR_QUIRK_APPLIED] is not None - assert device["entities"] is not None - - for entity_reference in device["entities"]: + device_endpoints = msg["result"] + assert len(device_endpoints) == 1 + + for endpoint in device_endpoints: + assert endpoint["device"][ATTR_IEEE] == "01:2d:6f:00:0a:90:69:e8" + assert endpoint["device"][ATTR_MANUFACTURER] is not None + assert endpoint["device"][ATTR_MODEL] is not None + assert endpoint["device"][ATTR_NAME] is not None + assert endpoint["device"][ATTR_QUIRK_APPLIED] is not None + assert endpoint["device"]["entities"] is not None + assert endpoint["endpoint_id"] is not None + assert endpoint["entities"] is not None + + for entity_reference in endpoint["device"]["entities"]: assert entity_reference[ATTR_NAME] is not None assert entity_reference["entity_id"] is not None + for entity_reference in endpoint["entities"]: + assert entity_reference["original_name"] is not None + # Make sure there are no groupable devices when the device is unavailable # Make device unavailable device_groupable.set_available(False) @@ -269,8 +274,8 @@ async def test_list_groupable_devices(zha_client, device_groupable): assert msg["id"] == 11 assert msg["type"] == const.TYPE_RESULT - devices = msg["result"] - assert len(devices) == 0 + device_endpoints = msg["result"] + assert len(device_endpoints) == 0 async def test_add_group(zha_client): diff --git a/tests/components/zha/test_fan.py b/tests/components/zha/test_fan.py index 399982df37a360..91819e6f457be3 100644 --- a/tests/components/zha/test_fan.py +++ b/tests/components/zha/test_fan.py @@ -1,6 +1,4 @@ """Test zha fan.""" -from unittest.mock import call - import pytest import zigpy.profiles.zha as zha import zigpy.zcl.clusters.general as general @@ -35,6 +33,8 @@ send_attributes_report, ) +from tests.async_mock import call + IEEE_GROUPABLE_DEVICE = "01:2d:6f:00:0a:90:69:e8" IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e8" @@ -62,6 +62,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) diff --git a/tests/components/zha/test_gateway.py b/tests/components/zha/test_gateway.py index c5ae9142ff0aae..379e4d56492c3f 100644 --- a/tests/components/zha/test_gateway.py +++ b/tests/components/zha/test_gateway.py @@ -1,6 +1,8 @@ """Test ZHA Gateway.""" +import asyncio import logging import time +from unittest.mock import patch import pytest import zigpy.profiles.zha as zha @@ -8,6 +10,7 @@ import zigpy.zcl.clusters.lighting as lighting from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.zha.core.group import GroupMember from .common import async_enable_traffic, async_find_group_entity_id, get_zha_gateway @@ -52,6 +55,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -127,17 +131,16 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord 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] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) 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 + assert member.device.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 @@ -157,18 +160,24 @@ async def test_gateway_group_methods(hass, device_light_1, device_light_2, coord # test creating a group with 1 member zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", [device_light_1.ieee] + "Test Group", [GroupMember(device_light_1.ieee, 1)] ) await hass.async_block_till_done() assert zha_group is not None assert len(zha_group.members) == 1 for member in zha_group.members: - assert member.ieee in [device_light_1.ieee] + assert member.device.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) + with patch("zigpy.zcl.Cluster.request", side_effect=asyncio.TimeoutError): + await zha_group.members[0].async_remove_from_group() + assert len(zha_group.members) == 1 + for member in zha_group.members: + assert member.device.ieee in [device_light_1.ieee] + async def test_updating_device_store(hass, zigpy_dev_basic, zha_dev_basic): """Test saving data after a delay.""" diff --git a/tests/components/zha/test_light.py b/tests/components/zha/test_light.py index 915ace7c7f2b59..09c6d97808cd22 100644 --- a/tests/components/zha/test_light.py +++ b/tests/components/zha/test_light.py @@ -1,6 +1,5 @@ """Test zha light.""" from datetime import timedelta -from unittest.mock import MagicMock, call, sentinel import pytest import zigpy.profiles.zha as zha @@ -10,6 +9,7 @@ import zigpy.zcl.foundation as zcl_f from homeassistant.components.light import DOMAIN, FLASH_LONG, FLASH_SHORT +from homeassistant.components.zha.core.group import GroupMember from homeassistant.components.zha.light import FLASH_EFFECTS from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE import homeassistant.util.dt as dt_util @@ -23,14 +23,14 @@ send_attributes_report, ) -from tests.async_mock import AsyncMock, patch +from tests.async_mock import AsyncMock, MagicMock, call, patch, sentinel 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" -IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e8" +IEEE_GROUPABLE_DEVICE2 = "02:2d:6f:00:0a:90:69:e9" +IEEE_GROUPABLE_DEVICE3 = "03:2d:6f:00:0a:90:69:e7" LIGHT_ON_OFF = { 1: { @@ -78,13 +78,14 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): zigpy_device = zigpy_device_mock( { 1: { - "in_clusters": [], + "in_clusters": [general.Groups.cluster_id], "out_clusters": [], "device_type": zha.DeviceType.COLOR_DIMMABLE_LIGHT, } }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -110,6 +111,7 @@ async def device_light_1(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE, + nwk=0xB79D, ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -135,6 +137,7 @@ async def device_light_2(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE2, + nwk=0xC79E, ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -160,6 +163,7 @@ async def device_light_3(hass, zigpy_device_mock, zha_device_joined): } }, ieee=IEEE_GROUPABLE_DEVICE3, + nwk=0xB89F, ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) @@ -290,10 +294,12 @@ async def async_test_on_off_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: 0, 0: 1, 2: 3}) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON # turn off at light await send_attributes_report(hass, cluster, {1: 1, 0: 0, 2: 3}) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_OFF @@ -301,6 +307,7 @@ async def async_test_on_from_light(hass, cluster, entity_id): """Test on off functionality from the light.""" # turn on at light await send_attributes_report(hass, cluster, {1: -1, 0: 1, 2: 2}) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == STATE_ON @@ -411,6 +418,7 @@ async def async_test_dimmer_from_light(hass, cluster, entity_id, level, expected await send_attributes_report( hass, cluster, {1: level + 10, 0: level, 2: level - 10 or 22} ) + await hass.async_block_till_done() assert hass.states.get(entity_id).state == expected_state # hass uses None for brightness of 0 in state attributes if level == 0: @@ -439,7 +447,23 @@ async def async_test_flash_from_hass(hass, cluster, entity_id, flash): ) -async def async_test_zha_group_light_entity( +@patch( + "zigpy.zcl.clusters.lighting.Color.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.Identify.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.LevelControl.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +@patch( + "zigpy.zcl.clusters.general.OnOff.request", + new=AsyncMock(return_value=[sentinel.data, zcl_f.Status.SUCCESS]), +) +async def test_zha_group_light_entity( hass, device_light_1, device_light_2, device_light_3, coordinator ): """Test the light entity for a ZHA group.""" @@ -450,119 +474,180 @@ async def async_test_zha_group_light_entity( 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] + members = [GroupMember(device_light_1.ieee, 1), GroupMember(device_light_2.ieee, 1)] + + assert coordinator.is_coordinator # test creating a group with 2 members - zha_group = await zha_gateway.async_create_zigpy_group( - "Test Group", member_ieee_addresses - ) + zha_group = await zha_gateway.async_create_zigpy_group("Test Group", members) 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 + assert member.device.ieee in member_ieee_addresses + assert member.group == zha_group + assert member.endpoint is not None - entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) - assert hass.states.get(entity_id) is not None + device_1_entity_id = await find_entity_id(DOMAIN, device_light_1, hass) + device_2_entity_id = await find_entity_id(DOMAIN, device_light_2, hass) + device_3_entity_id = await find_entity_id(DOMAIN, device_light_3, hass) + + assert ( + device_1_entity_id != device_2_entity_id + and device_1_entity_id != device_3_entity_id + ) + assert device_2_entity_id != device_3_entity_id + + group_entity_id = async_find_group_entity_id(hass, DOMAIN, zha_group) + assert hass.states.get(group_entity_id) is not None + + assert device_1_entity_id in zha_group.member_entity_ids + assert device_2_entity_id in zha_group.member_entity_ids + assert device_3_entity_id not in zha_group.member_entity_ids 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 - dev3_cluster_on_off = device_light_3.endpoints[1].on_off + dev1_cluster_on_off = device_light_1.device.endpoints[1].on_off + dev2_cluster_on_off = device_light_2.device.endpoints[1].on_off + dev3_cluster_on_off = device_light_3.device.endpoints[1].on_off + + dev1_cluster_level = device_light_1.device.endpoints[1].level # test that the lights were created and that they are unavailable - assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + assert hass.states.get(group_entity_id).state == STATE_UNAVAILABLE # allow traffic to flow through the gateway and device - await async_enable_traffic(hass, zha_group.members) + await async_enable_traffic(hass, [device_light_1, device_light_2, device_light_3]) + await hass.async_block_till_done() # 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) + assert hass.states.get(group_entity_id).state == STATE_OFF # test turning the lights on and off from the HA - await async_test_on_off_from_hass(hass, group_cluster_on_off, entity_id) + await async_test_on_off_from_hass(hass, group_cluster_on_off, group_entity_id) # test short flashing the lights from the HA await async_test_flash_from_hass( - hass, group_cluster_identify, entity_id, FLASH_SHORT + hass, group_cluster_identify, group_entity_id, FLASH_SHORT ) + # test turning the lights on and off from the light + await async_test_on_off_from_light(hass, dev1_cluster_on_off, group_entity_id) + # 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 + hass, group_cluster_on_off, group_cluster_level, group_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_on_from_light(hass, dev1_cluster_on_off, group_entity_id) await async_test_dimmer_from_light( - hass, group_cluster_level, entity_id, 150, STATE_ON + hass, dev1_cluster_level, group_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 + hass, group_cluster_identify, group_entity_id, FLASH_LONG ) + assert len(zha_group.members) == 2 # 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() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is on - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_ON + assert hass.states.get(device_2_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON - await dev1_cluster_on_off.off() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is still on - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON - await dev2_cluster_on_off.off() + await send_attributes_report(hass, dev2_cluster_on_off, {0: 0}) + await hass.async_block_till_done() # test that group light is now off - assert hass.states.get(entity_id).state == STATE_OFF + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_entity_id).state == STATE_OFF - await dev1_cluster_on_off.on() + await send_attributes_report(hass, dev1_cluster_on_off, {0: 1}) + await hass.async_block_till_done() # test that group light is now back on - assert hass.states.get(entity_id).state == STATE_ON + assert hass.states.get(device_1_entity_id).state == STATE_ON + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_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 + # turn it off to test a new member add being tracked + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await hass.async_block_till_done() + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(group_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 + await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) + await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) + await hass.async_block_till_done() + assert device_3_entity_id in zha_group.member_entity_ids + assert len(zha_group.members) == 3 + + assert hass.states.get(device_1_entity_id).state == STATE_OFF + assert hass.states.get(device_2_entity_id).state == STATE_OFF + assert hass.states.get(device_3_entity_id).state == STATE_ON + assert hass.states.get(group_entity_id).state == STATE_ON # make the group have only 1 member and now there should be no entity - await zha_group.async_remove_members([device_light_2.ieee, device_light_3.ieee]) + await zha_group.async_remove_members( + [GroupMember(device_light_2.ieee, 1), GroupMember(device_light_3.ieee, 1)] + ) assert len(zha_group.members) == 1 - assert hass.states.get(entity_id).state is None + assert hass.states.get(group_entity_id) is None + assert device_2_entity_id not in zha_group.member_entity_ids + assert device_3_entity_id not in zha_group.member_entity_ids + # make sure the entity registry entry is still there - assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None # add a member back and ensure that the group entity was created again - 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 + await zha_group.async_add_members([GroupMember(device_light_3.ieee, 1)]) + await send_attributes_report(hass, dev3_cluster_on_off, {0: 1}) + await hass.async_block_till_done() + assert len(zha_group.members) == 2 + assert hass.states.get(group_entity_id).state == STATE_ON # add a 3rd member and ensure we still have an entity and we track the new one - await dev1_cluster_on_off.off() - await dev3_cluster_on_off.off() - assert hass.states.get(entity_id).state == STATE_OFF + await send_attributes_report(hass, dev1_cluster_on_off, {0: 0}) + await send_attributes_report(hass, dev3_cluster_on_off, {0: 0}) + await hass.async_block_till_done() + assert hass.states.get(group_entity_id).state == STATE_OFF + # this will test that _reprobe_group is used correctly - await zha_group.async_add_members([device_light_2.ieee]) - await dev2_cluster_on_off.on() - assert hass.states.get(entity_id).state == STATE_ON + await zha_group.async_add_members( + [GroupMember(device_light_2.ieee, 1), GroupMember(coordinator.ieee, 1)] + ) + await send_attributes_report(hass, dev2_cluster_on_off, {0: 1}) + await hass.async_block_till_done() + assert len(zha_group.members) == 4 + assert hass.states.get(group_entity_id).state == STATE_ON + + await zha_group.async_remove_members([GroupMember(coordinator.ieee, 1)]) + await hass.async_block_till_done() + assert hass.states.get(group_entity_id).state == STATE_ON + assert len(zha_group.members) == 3 # remove the group and ensure that there is no entity and that the entity registry is cleaned up - assert zha_gateway.ha_entity_registry.async_get(entity_id) is not None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is not None await zha_gateway.async_remove_zigpy_group(zha_group.group_id) - assert hass.states.get(entity_id).state is None - assert zha_gateway.ha_entity_registry.async_get(entity_id) is None + assert hass.states.get(group_entity_id) is None + assert zha_gateway.ha_entity_registry.async_get(group_entity_id) is None diff --git a/tests/components/zha/test_switch.py b/tests/components/zha/test_switch.py index ed5d228ab889bf..7bdf2ccc4d24f2 100644 --- a/tests/components/zha/test_switch.py +++ b/tests/components/zha/test_switch.py @@ -53,6 +53,7 @@ async def coordinator(hass, zigpy_device_mock, zha_device_joined): }, ieee="00:15:8d:00:02:32:4f:32", nwk=0x0000, + node_descriptor=b"\xf8\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff", ) zha_device = await zha_device_joined(zigpy_device) zha_device.set_available(True) diff --git a/tests/components/zwave/conftest.py b/tests/components/zwave/conftest.py index f80c55f776793d..50edcfec1571ad 100644 --- a/tests/components/zwave/conftest.py +++ b/tests/components/zwave/conftest.py @@ -1,8 +1,7 @@ """Fixtures for Z-Wave tests.""" -from unittest.mock import MagicMock, patch - import pytest +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockNetwork, MockOption diff --git a/tests/components/zwave/test_binary_sensor.py b/tests/components/zwave/test_binary_sensor.py index 54270cdc3f4d19..8ac6370ad35e09 100644 --- a/tests/components/zwave/test_binary_sensor.py +++ b/tests/components/zwave/test_binary_sensor.py @@ -1,9 +1,9 @@ """Test Z-Wave binary sensors.""" import datetime -from unittest.mock import patch from homeassistant.components.zwave import binary_sensor, const +from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_cover.py b/tests/components/zwave/test_cover.py index e8b784feefe730..cde0957e2b352f 100644 --- a/tests/components/zwave/test_cover.py +++ b/tests/components/zwave/test_cover.py @@ -1,6 +1,4 @@ """Test Z-Wave cover devices.""" -from unittest.mock import MagicMock - from homeassistant.components.cover import SUPPORT_CLOSE, SUPPORT_OPEN from homeassistant.components.zwave import ( CONF_INVERT_OPENCLOSE_BUTTONS, @@ -9,6 +7,7 @@ cover, ) +from tests.async_mock import MagicMock from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index b71758469d0feb..19733b045dca36 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -3,7 +3,6 @@ from collections import OrderedDict from datetime import datetime import unittest -from unittest.mock import MagicMock, patch import pytest from pytz import utc @@ -23,7 +22,7 @@ from homeassistant.helpers.entity_registry import async_get_registry from homeassistant.setup import setup_component -from tests.async_mock import AsyncMock +from tests.async_mock import AsyncMock, MagicMock, patch from tests.common import async_fire_time_changed, get_test_home_assistant, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue diff --git a/tests/components/zwave/test_light.py b/tests/components/zwave/test_light.py index fc62ef880f65a8..1b973294daff74 100644 --- a/tests/components/zwave/test_light.py +++ b/tests/components/zwave/test_light.py @@ -1,6 +1,4 @@ """Test Z-Wave lights.""" -from unittest.mock import MagicMock, patch - from homeassistant.components import zwave from homeassistant.components.light import ( ATTR_BRIGHTNESS, @@ -16,6 +14,7 @@ ) from homeassistant.components.zwave import const, light +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed @@ -234,7 +233,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert not device.is_on - with patch.object(light, "Timer", MagicMock()) as mock_timer: + with patch.object(light, "Timer") as mock_timer: value.data = 46 value_changed(value) @@ -246,7 +245,7 @@ def test_dimmer_refresh_value(mock_openzwave): assert mock_timer().start.called assert len(mock_timer().start.mock_calls) == 1 - with patch.object(light, "Timer", MagicMock()) as mock_timer_2: + with patch.object(light, "Timer") as mock_timer_2: value_changed(value) assert not device.is_on assert mock_timer().cancel.called diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index d5b6d0a0d275dd..2f82bcb2764aed 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -1,9 +1,8 @@ """Test Z-Wave locks.""" -from unittest.mock import MagicMock, patch - from homeassistant import config_entries from homeassistant.components.zwave import const, lock +from tests.async_mock import MagicMock, patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave/test_node_entity.py b/tests/components/zwave/test_node_entity.py index 4d117179328d56..8306899ce02e1e 100644 --- a/tests/components/zwave/test_node_entity.py +++ b/tests/components/zwave/test_node_entity.py @@ -1,12 +1,12 @@ """Test Z-Wave node entity.""" import unittest -from unittest.mock import MagicMock, patch import pytest from homeassistant.components.zwave import const, node_entity from homeassistant.const import ATTR_ENTITY_ID +from tests.async_mock import MagicMock, patch import tests.mock.zwave as mock_zwave diff --git a/tests/components/zwave/test_switch.py b/tests/components/zwave/test_switch.py index 4293a4a23fd04a..b61c456ccb9507 100644 --- a/tests/components/zwave/test_switch.py +++ b/tests/components/zwave/test_switch.py @@ -1,8 +1,7 @@ """Test Z-Wave switches.""" -from unittest.mock import patch - from homeassistant.components.zwave import switch +from tests.async_mock import patch from tests.mock.zwave import MockEntityValues, MockNode, MockValue, value_changed diff --git a/tests/components/zwave_mqtt/__init__.py b/tests/components/zwave_mqtt/__init__.py new file mode 100644 index 00000000000000..95d36355b29d91 --- /dev/null +++ b/tests/components/zwave_mqtt/__init__.py @@ -0,0 +1 @@ +"""Tests for the Z-Wave MQTT integration.""" diff --git a/tests/components/zwave_mqtt/common.py b/tests/components/zwave_mqtt/common.py new file mode 100644 index 00000000000000..ef85d2e5533f28 --- /dev/null +++ b/tests/components/zwave_mqtt/common.py @@ -0,0 +1,57 @@ +"""Helpers for tests.""" +import json + +from homeassistant import config_entries +from homeassistant.components.zwave_mqtt.const import DOMAIN + +from tests.async_mock import Mock, patch +from tests.common import MockConfigEntry + + +async def setup_zwave(hass, entry=None, fixture=None): + """Set up Z-Wave and load a dump.""" + hass.config.components.add("mqtt") + + if entry is None: + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + ) + + entry.add_to_hass(hass) + + with patch("homeassistant.components.mqtt.async_subscribe") as mock_subscribe: + mock_subscribe.return_value = Mock() + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert "zwave_mqtt" in hass.config.components + assert len(mock_subscribe.mock_calls) == 1 + receive_message = mock_subscribe.mock_calls[0][1][2] + + if fixture is not None: + for line in fixture.split("\n"): + topic, payload = line.strip().split(",", 1) + receive_message(Mock(topic=topic, payload=payload)) + + await hass.async_block_till_done() + + return receive_message + + +class MQTTMessage: + """Represent a mock MQTT message.""" + + def __init__(self, topic, payload): + """Set up message.""" + self.topic = topic + self.payload = payload + + def decode(self): + """Decode message payload from a string to a json dict.""" + self.payload = json.loads(self.payload) + + def encode(self): + """Encode message payload into a string.""" + self.payload = json.dumps(self.payload) diff --git a/tests/components/zwave_mqtt/conftest.py b/tests/components/zwave_mqtt/conftest.py new file mode 100644 index 00000000000000..b32d6f72ce811e --- /dev/null +++ b/tests/components/zwave_mqtt/conftest.py @@ -0,0 +1,51 @@ +"""Helpers for tests.""" +import json + +import pytest + +from .common import MQTTMessage + +from tests.async_mock import patch +from tests.common import load_fixture + + +@pytest.fixture(name="generic_data", scope="session") +def generic_data_fixture(): + """Load generic MQTT data and return it.""" + return load_fixture(f"zwave_mqtt/generic_network_dump.csv") + + +@pytest.fixture(name="sent_messages") +def sent_messages_fixture(): + """Fixture to capture sent messages.""" + sent_messages = [] + + with patch( + "homeassistant.components.mqtt.async_publish", + side_effect=lambda hass, topic, payload: sent_messages.append( + {"topic": topic, "payload": json.loads(payload)} + ), + ): + yield sent_messages + + +@pytest.fixture(name="switch_msg") +async def switch_msg_fixture(hass): + """Return a mock MQTT msg with a switch actuator message.""" + switch_json = json.loads( + await hass.async_add_executor_job(load_fixture, "zwave_mqtt/switch.json") + ) + message = MQTTMessage(topic=switch_json["topic"], payload=switch_json["payload"]) + message.encode() + return message + + +@pytest.fixture(name="sensor_msg") +async def sensor_msg_fixture(hass): + """Return a mock MQTT msg with a sensor change message.""" + sensor_json = json.loads( + await hass.async_add_executor_job(load_fixture, "zwave_mqtt/sensor.json") + ) + message = MQTTMessage(topic=sensor_json["topic"], payload=sensor_json["payload"]) + message.encode() + return message diff --git a/tests/components/zwave_mqtt/test_config_flow.py b/tests/components/zwave_mqtt/test_config_flow.py new file mode 100644 index 00000000000000..fbdf4012009fba --- /dev/null +++ b/tests/components/zwave_mqtt/test_config_flow.py @@ -0,0 +1,53 @@ +"""Test the Z-Wave over MQTT config flow.""" +from homeassistant import config_entries, setup +from homeassistant.components.zwave_mqtt.config_flow import TITLE +from homeassistant.components.zwave_mqtt.const import DOMAIN + +from tests.async_mock import patch +from tests.common import MockConfigEntry + + +async def test_user_create_entry(hass): + """Test the user step creates an entry.""" + hass.config.components.add("mqtt") + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] is None + + with patch( + "homeassistant.components.zwave_mqtt.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.zwave_mqtt.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"] == TITLE + assert result2["data"] == {} + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_mqtt_not_setup(hass): + """Test that mqtt is required.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "mqtt_required" + + +async def test_one_instance_allowed(hass): + """Test that only one instance is allowed.""" + entry = MockConfigEntry(domain=DOMAIN, data={}, title=TITLE) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "abort" + assert result["reason"] == "one_instance_allowed" diff --git a/tests/components/zwave_mqtt/test_init.py b/tests/components/zwave_mqtt/test_init.py new file mode 100644 index 00000000000000..0b77905ab9be21 --- /dev/null +++ b/tests/components/zwave_mqtt/test_init.py @@ -0,0 +1,62 @@ +"""Test integration initialization.""" +from homeassistant import config_entries +from homeassistant.components.zwave_mqtt import DOMAIN, PLATFORMS, const + +from .common import setup_zwave + +from tests.common import MockConfigEntry + + +async def test_init_entry(hass, generic_data): + """Test setting up config entry.""" + await setup_zwave(hass, fixture=generic_data) + + # Verify integration + platform loaded. + assert "zwave_mqtt" in hass.config.components + for platform in PLATFORMS: + assert platform in hass.config.components, platform + assert f"{platform}.{DOMAIN}" in hass.config.components, f"{platform}.{DOMAIN}" + + # Verify services registered + assert hass.services.has_service(DOMAIN, const.SERVICE_ADD_NODE) + assert hass.services.has_service(DOMAIN, const.SERVICE_REMOVE_NODE) + + +async def test_unload_entry(hass, generic_data, switch_msg, caplog): + """Test unload the config entry.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave", + connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, + ) + entry.add_to_hass(hass) + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + + receive_message = await setup_zwave(hass, entry=entry, fixture=generic_data) + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(hass.states.async_entity_ids("switch")) == 1 + + await hass.config_entries.async_unload(entry.entry_id) + + assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert len(hass.states.async_entity_ids("switch")) == 0 + + # Send a message for a switch from the broker to check that + # all entity topic subscribers are unsubscribed. + receive_message(switch_msg) + await hass.async_block_till_done() + + assert len(hass.states.async_entity_ids("switch")) == 0 + + # Load the integration again and check that there are no errors when + # adding the entities. + # This asserts that we have unsubscribed the entity addition signals + # when unloading the integration previously. + await setup_zwave(hass, entry=entry, fixture=generic_data) + await hass.async_block_till_done() + + assert entry.state == config_entries.ENTRY_STATE_LOADED + assert len(hass.states.async_entity_ids("switch")) == 1 + for record in caplog.records: + assert record.levelname != "ERROR" diff --git a/tests/components/zwave_mqtt/test_scenes.py b/tests/components/zwave_mqtt/test_scenes.py new file mode 100644 index 00000000000000..10e1f94b229beb --- /dev/null +++ b/tests/components/zwave_mqtt/test_scenes.py @@ -0,0 +1,88 @@ +"""Test Z-Wave (central) Scenes.""" +from .common import MQTTMessage, setup_zwave + +from tests.common import async_capture_events + + +async def test_scenes(hass, generic_data, sent_messages): + """Test setting up config entry.""" + + receive_message = await setup_zwave(hass, fixture=generic_data) + events = async_capture_events(hass, "zwave_mqtt.scene_activated") + + # Publish fake scene event on mqtt + message = MQTTMessage( + topic="OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/", + payload={ + "Label": "Scene", + "Value": 16, + "Units": "", + "Min": -2147483648, + "Max": 2147483647, + "Type": "Int", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", + "Index": 0, + "Node": 7, + "Genre": "User", + "Help": "", + "ValueIDKey": 122339347, + "ReadOnly": False, + "WriteOnly": False, + "ValueSet": False, + "ValuePolled": False, + "ChangeVerified": False, + "Event": "valueChanged", + "TimeStamp": 1579630367, + }, + ) + message.encode() + receive_message(message) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 1 + assert events[0].data["scene_value_id"] == 16 + + # Publish fake central scene event on mqtt + message = MQTTMessage( + topic="OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/", + payload={ + "Label": "Scene 1", + "Value": { + "List": [ + {"Value": 0, "Label": "Inactive"}, + {"Value": 1, "Label": "Pressed 1 Time"}, + {"Value": 2, "Label": "Key Released"}, + {"Value": 3, "Label": "Key Held down"}, + ], + "Selected": "Pressed 1 Time", + "Selected_id": 1, + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", + "Index": 1, + "Node": 61, + "Genre": "User", + "Help": "", + "ValueIDKey": 281476005806100, + "ReadOnly": False, + "WriteOnly": False, + "ValueSet": False, + "ValuePolled": False, + "ChangeVerified": False, + "Event": "valueChanged", + "TimeStamp": 1579640710, + }, + ) + message.encode() + receive_message(message) + # wait for the event + await hass.async_block_till_done() + assert len(events) == 2 + assert events[1].data["scene_id"] == 1 + assert events[1].data["scene_label"] == "Scene 1" + assert events[1].data["scene_value_label"] == "Pressed 1 Time" diff --git a/tests/components/zwave_mqtt/test_sensor.py b/tests/components/zwave_mqtt/test_sensor.py new file mode 100644 index 00000000000000..ab7fd26b0b6c3b --- /dev/null +++ b/tests/components/zwave_mqtt/test_sensor.py @@ -0,0 +1,76 @@ +"""Test Z-Wave Sensors.""" +from homeassistant.components.sensor import ( + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_POWER, + DEVICE_CLASS_PRESSURE, + DOMAIN as SENSOR_DOMAIN, +) +from homeassistant.components.zwave_mqtt.const import DOMAIN +from homeassistant.const import ATTR_DEVICE_CLASS + +from .common import setup_zwave + + +async def test_sensor(hass, generic_data): + """Test setting up config entry.""" + await setup_zwave(hass, fixture=generic_data) + + # Test standard sensor + state = hass.states.get("sensor.smart_plug_electric_v") + assert state is not None + assert state.state == "123.9" + assert state.attributes["unit_of_measurement"] == "V" + + # Test device classes + state = hass.states.get("sensor.trisensor_relative_humidity") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_HUMIDITY + state = hass.states.get("sensor.trisensor_pressure") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_PRESSURE + state = hass.states.get("sensor.trisensor_fake_power") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + state = hass.states.get("sensor.trisensor_fake_energy") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + state = hass.states.get("sensor.trisensor_fake_electric") + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_POWER + + # Test ZWaveListSensor disabled by default + registry = await hass.helpers.entity_registry.async_get_registry() + entity_id = "sensor.water_sensor_6_instance_1_water" + state = hass.states.get(entity_id) + assert state is None + + entry = registry.async_get(entity_id) + assert entry + assert entry.disabled + assert entry.disabled_by == "integration" + + # Test enabling entity + updated_entry = registry.async_update_entity( + entry.entity_id, **{"disabled_by": None} + ) + assert updated_entry != entry + assert updated_entry.disabled is False + + +async def test_sensor_enabled(hass, generic_data, sensor_msg): + """Test enabling an advanced sensor.""" + + registry = await hass.helpers.entity_registry.async_get_registry() + + entry = registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "1-36-1407375493578772", + suggested_object_id="water_sensor_6_instance_1_water", + disabled_by=None, + ) + assert entry.disabled is False + + receive_msg = await setup_zwave(hass, fixture=generic_data) + receive_msg(sensor_msg) + await hass.async_block_till_done() + + state = hass.states.get(entry.entity_id) + assert state is not None + assert state.state == "0" + assert state.attributes["label"] == "Clear" diff --git a/tests/components/zwave_mqtt/test_switch.py b/tests/components/zwave_mqtt/test_switch.py new file mode 100644 index 00000000000000..84929dabe0abe8 --- /dev/null +++ b/tests/components/zwave_mqtt/test_switch.py @@ -0,0 +1,41 @@ +"""Test Z-Wave Switches.""" +from .common import setup_zwave + + +async def test_switch(hass, generic_data, sent_messages, switch_msg): + """Test setting up config entry.""" + receive_message = await setup_zwave(hass, fixture=generic_data) + + # Test loaded + state = hass.states.get("switch.smart_plug_switch") + assert state is not None + assert state.state == "off" + + # Test turning on + await hass.services.async_call( + "switch", "turn_on", {"entity_id": "switch.smart_plug_switch"}, blocking=True + ) + assert len(sent_messages) == 1 + msg = sent_messages[0] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": True, "ValueIDKey": 541671440} + + # Feedback on state + switch_msg.decode() + switch_msg.payload["Value"] = True + switch_msg.encode() + receive_message(switch_msg) + await hass.async_block_till_done() + + state = hass.states.get("switch.smart_plug_switch") + assert state is not None + assert state.state == "on" + + # Test turning off + await hass.services.async_call( + "switch", "turn_off", {"entity_id": "switch.smart_plug_switch"}, blocking=True + ) + assert len(sent_messages) == 2 + msg = sent_messages[1] + assert msg["topic"] == "OpenZWave/1/command/setvalue/" + assert msg["payload"] == {"Value": False, "ValueIDKey": 541671440} diff --git a/tests/fixtures/zwave_mqtt/generic_network_dump.csv b/tests/fixtures/zwave_mqtt/generic_network_dump.csv new file mode 100644 index 00000000000000..debb329a8f7c2b --- /dev/null +++ b/tests/fixtures/zwave_mqtt/generic_network_dump.csv @@ -0,0 +1,282 @@ +OpenZWave/1/status/,{ "OpenZWave_Version": "1.6.1008", "OZWDeamon_Version": "0.1", "QTOpenZWave_Version": "1.0.0", "QT_Version": "5.12.5", "Status": "driverAllNodesQueried", "TimeStamp": 1579566933, "ManufacturerSpecificDBReady": true, "homeID": 3245146787, "getControllerNodeId": 1, "getSUCNodeId": 1, "isPrimaryController": true, "isBridgeController": false, "hasExtendedTXStatistics": true, "getControllerLibraryVersion": "Z-Wave 3.95", "getControllerLibraryType": "Static Controller", "getControllerPath": "/dev/zwave"} +OpenZWave/1/node/1/,{ "NodeID": 1, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": false, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:005A:0101", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw090.png", "Description": "Aeotec Z-Stick Gen5 is a USB controller. When connected to a host controller via USB, it enables the host controller to take part in the Z-Wave network. Products that are Z-Wave certified can be used and communicate with other Z-Wave certified devices.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1355/Z Stick Gen5 manual 1.pdf", "ProductPageURL": "", "InclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Inclusion” button on your PC/host Controller application.", "ExclusionHelp": "Plug the Z-Stick into USB port of your host Controller and then click the “Exclusion” button on your PC/host Controller application.", "ResetHelp": "Use this procedure only in the event that the primary controller is missing or otherwise inoperable. Press and hold the Action Button on Z-Stick for 20 seconds and then release.", "WakeupHelp": "N/A", "ProductSupportURL": "", "Frequency": "", "Name": "Z-Stick Gen5", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566911, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW090 Z-Stick Gen5 US", "NodeBasicString": "Static Controller", "NodeBasic": 2, "NodeGenericString": "Static Controller", "NodeGeneric": 2, "NodeSpecificString": "Static PC Controller", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0101", "NodeProductID": "0x005a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 0, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "Unknown Type (0x0000)", "NodeDeviceType": 0, "NodeRole": 0, "NodeRoleString": "Central Controller", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 31, 32, 33, 36, 37, 39 ]} +OpenZWave/1/node/1/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/32/value/17301521/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 1, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 17301521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/22799473140563988/,{ "Label": "LED indicator configuration", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 1, "Genre": "Config", "Help": "Enable/Disable LED indicator when plugged in", "ValueIDKey": 22799473140563988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/61924494903345172/,{ "Label": "Configuration of the RF power level", "Value": { "List": [ { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" } ], "Selected": "10" }, "Units": "", "Min": 1, "Max": 10, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 220, "Node": 1, "Genre": "Config", "Help": "1~10, other= ignore. A total of 10 levels, level 1 as the weak output power, and so on, 10 for most output power level", "ValueIDKey": 61924494903345172, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/68116944390979604/,{ "Label": "Security network enabled", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 242, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68116944390979604, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/68398419367690259/,{ "Label": "Security network key", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 243, "Node": 1, "Genre": "Config", "Help": "", "ValueIDKey": 68398419367690259, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/70931694158086164/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 1, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694158086164, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/112/value/71776119088218131/,{ "Label": "Reset default configuration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 1, "Genre": "Config", "Help": "Reset to the default configuration", "ValueIDKey": 71776119088218131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/31227923/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 1, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 31227923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/281475007938579/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 1, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475007938579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/562949984649235/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 1, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562949984649235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/844424961359895/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 1, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844424961359895, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/1/instance/1/commandclass/114/value/1125899938070551/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 1, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125899938070551, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/,{ "NodeID": 32, "NodeQueryStage": "Complete", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": true, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0208:0005:0101", "ZWAProductURL": "", "ProductPic": "images/hank/hkzw-so01-smartplug.png", "Description": "Smart Plug is a Z-Wave Switch plugin module specifically used to enable Z-Wave command and control (on/off) of any plug-in tool. It can report wattage consumption or kWh energy usage.Smart Plug is also a security Z-Wave device and supports the Over The Air (OTA) feature for the product’s firmware upgrade.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/1789/HKZW-SO01_manual.pdf", "ProductPageURL": "", "InclusionHelp": "Set the Z-Wave network main controller into learning mode. Short press the Z-button, the LED will keep turning on or off, which indicates the inclusion is successful.", "ExclusionHelp": "Set the Z-Wave network main controller into remove mode. Triple click the Z-button, the LED will blink slowly, which indicates the inclusion is successful.", "ResetHelp": "Press and hold the Z-button for more than 20 seconds, the LED will blink faster and faster when the button is pressed. If holding time more than 20seconds, the LED indicator will be on for 3 seconds then it blinking slowly, which indicates the reseting is successful. Use this procedure only in the event that the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "Smart Plug", "ProductPicBase64": "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" }, "Event": "nodeQueriesComplete", "TimeStamp": 1579566933, "NodeManufacturerName": "HANK Electronics Ltd", "NodeProductName": "HKZW-SO01 Smart Plug", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Binary Switch", "NodeGeneric": 16, "NodeSpecificString": "Binary Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0208", "NodeProductType": "0x0101", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1, "NodeName": "", "NodeLocation": "", "NodeDeviceTypeString": "On/Off Power Switch", "NodeDeviceType": 1792, "NodeRole": 5, "NodeRoleString": "Always On Slave", "NodePlusType": 0, "NodePlusTypeString": "Z-Wave+ node", "Neighbors": [ 1, 33, 36, 37, 39 ]} +OpenZWave/1/node/32/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/37/,{ "Instance": 1, "CommandClassId": 37, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/,{ "Label": "Switch", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", "Index": 0, "Node": 32, "Genre": "User", "Help": "Turn On/Off Device", "ValueIDKey": 541671440, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/39/value/550092820/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 550092820, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/43/value/541769747/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541769747, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/43/value/281475518480403/,{ "Label": "Duration", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 1, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 281475518480403, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/,{ "Instance": 1, "CommandClassId": 50, "CommandClass": "COMMAND_CLASS_METER", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/541884434/,{ "Label": "Electric - kWh", "Value": 0.06199999898672104, "Units": "kWh", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 0, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 541884434, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566931} +OpenZWave/1/node/32/instance/1/commandclass/50/value/562950495305746/,{ "Label": "Electric - W", "Value": 0.0, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 2, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 562950495305746, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/1125900448727058/,{ "Label": "Electric - V", "Value": 123.90499877929688, "Units": "V", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 4, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1125900448727058, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579566933} +OpenZWave/1/node/32/instance/1/commandclass/50/value/1407375425437714/,{ "Label": "Electric - A", "Value": 0.0, "Units": "A", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 5, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 1407375425437714, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/72057594579812368/,{ "Label": "Exporting", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 256, "Node": 32, "Genre": "User", "Help": "", "ValueIDKey": 72057594579812368, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/50/value/72339069564911640/,{ "Label": "Reset", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_METER", "Index": 257, "Node": 32, "Genre": "System", "Help": "", "ValueIDKey": 72339069564911640, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/value/550993937/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 32, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 550993937, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/value/281475527704598/,{ "Label": "InstallerIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 32, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475527704598, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/94/value/562950504415254/,{ "Label": "UserIcon", "Value": 1792, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 32, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950504415254, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/5629500081307668/,{ "Label": "Overload Protection", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 32, "Genre": "Config", "Help": "Smart Plug keep detecting the load power, once the current exceeds 16.5a for more than 5s, smart plug's relay will turn off", "ValueIDKey": 5629500081307668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/5910975058018324/,{ "Label": "Device status after power failure", "Value": { "List": [ { "Value": 0, "Label": "Memorize" }, { "Value": 1, "Label": "On" }, { "Value": 2, "Label": "Off" } ], "Selected": "Memorize" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 32, "Genre": "Config", "Help": "Define how the plug reacts after the power supply is back on. 0 - Smart Plug memorizes its state after a power failure. 1 - Smart Plug does not memorize its state after a power failure. Connected device will be on after the power supply is reconnected. 2 - Smart Plug does not memorize its state after a power failure. Connected device will be off after the power supply is reconnected.", "ValueIDKey": 5910975058018324, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/6755399988150292/,{ "Label": "Notification when load status change", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Basic" }, { "Value": 2, "Label": "Basic without Z-WAVE Command" } ], "Selected": "Basic" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 32, "Genre": "Config", "Help": "Smart Plug can send notifications to association device(Group Lifeline) when state of smart plug's load change 0 - The function is disabled 1 - Send Basic report. 2 - Send Basic report only when Load condition is not changed by Z-WAVE Command", "ValueIDKey": 6755399988150292, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/7599824918282260/,{ "Label": "Indicator Modes", "Value": { "List": [ { "Value": 0, "Label": "Enabled" }, { "Value": 1, "Label": "Disabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 27, "Node": 32, "Genre": "Config", "Help": "After smart plug being included into a Z-Wave network, the LED in the device will indicator the state of load. 0 - The LED will follow the status(on/off) of its load 1 - When the state of Switch's load changed, THe LED will follow the status(on/off) of its load, but the red LED will turn off after 5 seconds if there is no any switch action.", "ValueIDKey": 7599824918282260, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/42502722030403606/,{ "Label": "Threshold of power report", "Value": 50, "Units": "W", "Min": 0, "Max": 65535, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 151, "Node": 32, "Genre": "Config", "Help": "Power threshold to be interpereted, when the change value of load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline)", "ValueIDKey": 42502722030403606, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/42784197007114257/,{ "Label": "Percentage threshold of power report", "Value": 10, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 152, "Node": 32, "Genre": "Config", "Help": "Power percentage threshold to be interpreted, when change value of the load power exceeds the setting threshold, the smart plug will send meter report to association device(Group Lifeline).", "ValueIDKey": 42784197007114257, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48132221564616723/,{ "Label": "Power report frequency", "Value": 30, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 171, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48132221564616723, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48413696541327379/,{ "Label": "Energy report frequency", "Value": 300, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 172, "Node": 32, "Genre": "Config", "Help": "The interval of sending power report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48413696541327379, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48695171518038035/,{ "Label": "Voltage report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 173, "Node": 32, "Genre": "Config", "Help": "The interval of sending voltage report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48695171518038035, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/112/value/48976646494748691/,{ "Label": "Electricity report frequency", "Value": 0, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 174, "Node": 32, "Genre": "Config", "Help": "The interval of sending electricity report to association device(Group Lifeline). 0 - The function is disabled.", "ValueIDKey": 48976646494748691, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/551321619/,{ "Label": "Loaded Config Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 32, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 551321619, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/281475528032275/,{ "Label": "Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 32, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475528032275, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/562950504742931/,{ "Label": "Latest Available Config File Revision", "Value": 2, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 32, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950504742931, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/844425481453591/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 32, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425481453591, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/114/value/1125900458164247/,{ "Label": "Serial Number", "Value": "0107020900000000000607034800010001000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 32, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900458164247, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/551338004/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 32, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 551338004, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/281475528048657/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 32, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475528048657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/562950504759320/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 32, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950504759320, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/844425481469969/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 32, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425481469969, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1125900458180628/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 32, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900458180628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1407375434891286/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 32, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375434891286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1688850411601944/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 32, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850411601944, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/1970325388312600/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 32, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325388312600, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/2251800365023252/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 32, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800365023252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/115/value/2533275341733910/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 32, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275341733910, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/value/551649303/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 32, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 551649303, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/value/281475528359959/,{ "Label": "Protocol Version", "Value": "4.24", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 32, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475528359959, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/instance/1/commandclass/134/value/562950505070615/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 32, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950505070615, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/32/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.0", "255.0" ], "TimeStamp": 1579566915} +OpenZWave/1/node/36/,{ "NodeID": 36, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0086:007A:0102", "ZWAProductURL": "", "ProductPic": "images/aeotec/zw122.png", "Description": "Aeotec by Aeon Labs Water Sensor 6 brings intelligence to a new level, one that is suited to both safety and convenience. It contains 4 sensing points, which would be more accurately to detect the presence and absence of water or detect whether there is water leak in some places of your home. The Water Sensor 6 has an inbuilt buzzer that can play alarm sounds to let you know when the water is detected. The Water Sensor 6 is also a security Z-Wave device that supports Over The Air (OTA) for firmware updates.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2437/Aeon Labs Water Sensor 6 manual.pdf", "ProductPageURL": "", "InclusionHelp": "Turn the primary controller of Z-Wave network into inclusion mode, short press the product’s Action Button that you can find on the product.", "ExclusionHelp": "Turn the primary controller of Z-Wave network into exclusion mode, short press the product’s Action Button that you can find on the product.", "ResetHelp": "Press and hold the Action Button that you can find on the product for 20 seconds and then release. This procedure should only be used when the primary controller is inoperable.", "WakeupHelp": "Pressing the Action Button once will trigger sending the Wake up notification command. If press and hold the Z-Wave button for 3 seconds, the Water Sensor will wake up for 10 minutes.", "ProductSupportURL": "", "Frequency": "", "Name": "Water Sensor 6", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "AEON Labs", "NodeProductName": "ZW122 Water Sensor 6", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0086", "NodeProductType": "0x0102", "NodeProductID": "0x007a", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 4} +OpenZWave/1/node/36/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/32/value/604504081/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 36, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 604504081, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/49/value/281475585687570/,{ "Label": "Air Temperature", "Value": 17.100000381469728, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 36, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475585687570, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/49/value/72057594655293460/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 36, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594655293460, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/value/618102801/,{ "Label": "Instance 1: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102801, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/value/281475594813462/,{ "Label": "Instance 1: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813462, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/94/value/562950571524118/,{ "Label": "Instance 1: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524118, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/562950567624724/,{ "Label": "Waking up for 10 minutes when re-power on", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 36, "Genre": "Config", "Help": "Enable/Disable waking up for 10 minutes when re-power on (battery mode) the Water Sensor.", "ValueIDKey": 562950567624724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/2251800427888657/,{ "Label": "Timeout of awake after the Wake Up CC is sent out", "Value": 30, "Units": "seconds", "Min": 8, "Max": 127, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 8, "Node": 36, "Genre": "Config", "Help": "Set the timeout of awake after the Wake Up CC is sent out. Available rang is 8 to 127 seconds.", "ValueIDKey": 2251800427888657, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/2533275404599316/,{ "Label": "Current power mode", "Value": { "List": [ { "Value": 0, "Label": "USB power, sleeping mode after re-power on" }, { "Value": 1, "Label": "USB power, keep awake for 10 minutes after re-power on" }, { "Value": 2, "Label": "USB power, always awake state" }, { "Value": 256, "Label": "Battery power, sleeping mode after re-power on" }, { "Value": 257, "Label": "Battery power, keep awake for 10 minutes after re-power on" }, { "Value": 258, "Label": "Battery power, always awake state" } ], "Selected": "USB power, sleeping mode after re-power on" }, "Units": "", "Min": 0, "Max": 258, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 9, "Node": 36, "Genre": "Config", "Help": "Report the current power mode and the product state for battery power mode", "ValueIDKey": 2533275404599316, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/2814750381309971/,{ "Label": "Alarm time for the Buzzer", "Value": 1968650, "Units": "", "Min": 655360, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 36, "Genre": "Config", "Help": "Set the alarm time for the Buzzer when the sensor is triggered. 1 to 255 Repeated cycle of Buzzer alarm. 256 to 65535 the time of Buzzer keeping ON state (MSB). 65536 to 2147483647 The time of Buzzer keeping OFF state.", "ValueIDKey": 2814750381309971, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/10977524705918993/,{ "Label": "Set the low battery value", "Value": 20, "Units": "%", "Min": 10, "Max": 50, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 39, "Node": 36, "Genre": "Config", "Help": "10% to 50%", "ValueIDKey": 10977524705918993, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/13510799496314897/,{ "Label": "Sensor report", "Value": 55, "Units": "", "Min": 0, "Max": 55, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 48, "Node": 36, "Genre": "Config", "Help": "Enable/disable the sensor report: Bit 7 - Bit 6 - Bit 5 Notification Report for Overheat alarm. Bit 4 Notification Report for Under heat alarm. Bit 3 - Bit 2 Configuration Report for Tilt sensor. Bit 1 Notification Report for Vibration event. Bit 0 Notification Report for Water Leak event. Note: if the value = 1+2+4+16+32=55, which means if any sensor will report alarm.", "ValueIDKey": 13510799496314897, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/13792274473025555/,{ "Label": "Upper limit value", "Value": 26214400, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 49, "Node": 36, "Genre": "Config", "Help": "Set the upper limit value (overheat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value. Default: 0x01900000 => 40.0C", "ValueIDKey": 13792274473025555, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/14073749449736211/,{ "Label": "Lower limit value", "Value": 0, "Units": "", "Min": 65536, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 50, "Node": 36, "Genre": "Config", "Help": "Set the lower limit value (under heat). 0 Celsius unit 1 Fahrenheit unit 65536 to 2147483647 Temperature value", "ValueIDKey": 14073749449736211, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/16044074286710806/,{ "Label": "Recover limit value of temperature sensor", "Value": 5120, "Units": "", "Min": 100, "Max": 4080, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 57, "Node": 36, "Genre": "Config", "Help": "Set the recover limit value of temperature sensor. Note: 1. When the current measurement less than or equal (Upper limit - Recover limit), the upper limit report is enabled and then it would send out a sensor report when the next measurement is more than the upper limit. After that the upper limit report would be disabled again until the measurement less than or equal (Upper limit - Recover limit). 2. When the current measurement greater than or equal (Lower limit + Recover limit), the lower limit report is enabled and then it would send out a sensor report when the next measurement is less than the lower limit. After that the lower limit report would be disabled again until the measurement >= (Lower limit + Recover limit). 3. High byte is the recover limit value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 4. Recover limit range: 1.0 to 25.5 C/F (0x0100 to 0xFF00 or 0x0101 to 0xFF01). E.g. The default recover limit value is 2.0 C/F (0x1400/0x1401), when the measurement is less than (Upper limit - 2), the upper limit report would be enabled one time or when the measurement is more than (Lower limit + 2), the lower limit report would be enabled one time.", "ValueIDKey": 16044074286710806, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/18014399123685396/,{ "Label": "Unit of the automatic temperature report", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 64, "Node": 36, "Genre": "Config", "Help": "Set the default unit of the automatic temperature report in parameter 101-103", "ValueIDKey": 18014399123685396, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/23643898657898516/,{ "Label": "Get the state of tilt sensor", "Value": { "List": [ { "Value": 0, "Label": "Horizontal" }, { "Value": 1, "Label": "Vertical" } ], "Selected": "Horizontal" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 84, "Node": 36, "Genre": "Config", "Help": "Get the state of tilt sensor", "ValueIDKey": 23643898657898516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/24206848611319828/,{ "Label": "Buzzer", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Enabled" } ], "Selected": "Enabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 86, "Node": 36, "Genre": "Config", "Help": "Enable/ disable the buzzer.", "ValueIDKey": 24206848611319828, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/24488323588030490/,{ "Label": "Sensor is triggered the buzzer will alarm", "Value": [ { "Label": "Vibration", "Help": "If the vibration is triggered, the buzzer will alarm.", "Value": 1, "Position": 1 }, { "Label": "Tilt Sensor", "Help": "If the Tilt Sensor is triggered, the buzzer will alarm.", "Value": 1, "Position": 2 }, { "Label": "UnderHeat", "Help": "If the Under Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 4 }, { "Label": "OverHeat", "Help": "If the Over Heat Temperature is triggered, the buzzer will alarm.", "Value": 1, "Position": 5 } ], "Units": "", "Min": 0, "Max": 55, "Type": "BitSet", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 87, "Node": 36, "Genre": "Config", "Help": "What Sensors Trigger the Buzzer", "ValueIDKey": 24488323588030490, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/24769798564741140/,{ "Label": "Probe 1 Basic Set on grp 3", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 88, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 3 when the Sensor probe 1 is triggered.", "ValueIDKey": 24769798564741140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/25051273541451796/,{ "Label": "Probe 2 Basic Set on grp 4", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Presence/absence of water 0xFF/0x00" }, { "Value": 2, "Label": "Presence/absence of water 0x00/0xFF" } ], "Selected": "Send nothing" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 89, "Node": 36, "Genre": "Config", "Help": "To set which value of the Basic Set will be sent to the associated nodes in association Group 4 when the Sensor probe 2 is triggered.", "ValueIDKey": 25051273541451796, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/26458648425005076/,{ "Label": "Battery report selection", "Value": { "List": [ { "Value": 0, "Label": "USB power level" }, { "Value": 1, "Label": "CR123A battery level" } ], "Selected": "USB power level" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 94, "Node": 36, "Genre": "Config", "Help": "To set which power source level is reported via the Battery CC.", "ValueIDKey": 26458648425005076, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/28428973261979668/,{ "Label": "Unsolicited report", "Value": { "List": [ { "Value": 0, "Label": "Send Nothing" }, { "Value": 1, "Label": "Battery Report" }, { "Value": 2, "Label": "Multilevel sensor report for temperature" }, { "Value": 3, "Label": "Battery Report and Multilevel sensor report for temperature" } ], "Selected": "Battery Report and Multilevel sensor report for temperature" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 101, "Node": 36, "Genre": "Config", "Help": "To set what unsolicited report would be sent to the Lifeline group.", "ValueIDKey": 28428973261979668, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/31243723029086227/,{ "Label": "Unsolicited report interval time", "Value": 3600, "Units": "seconds", "Min": 5, "Max": 2678400, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 111, "Node": 36, "Genre": "Config", "Help": "To set the interval time of sending reports in Report group 1", "ValueIDKey": 31243723029086227, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/37999122470141972/,{ "Label": "Water leak event report selection", "Value": { "List": [ { "Value": 0, "Label": "Send nothing" }, { "Value": 1, "Label": "Send notification report to association group 1" }, { "Value": 2, "Label": "Send configuration 0x88 report to association group 2" }, { "Value": 3, "Label": "Send notification report to association group 1 and Send configuration 0x88 report to association group 2" } ], "Selected": "Send notification report to association group 1" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 135, "Node": 36, "Genre": "Config", "Help": "To set which sensor report can be sent when the water leak event is triggered and if the receiving device is a non-multichannel device.", "ValueIDKey": 37999122470141972, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/38280597446852628/,{ "Label": "Report Type to Send", "Value": { "List": [ { "Value": 0, "Label": "Absence of water is triggered by probe 1 and 2" }, { "Value": 1, "Label": "Presence of water is triggered by probe 1" }, { "Value": 2, "Label": "Presence of water is triggered by probe 2" }, { "Value": 3, "Label": "Presence of water is triggered by probe 1 and 2" } ], "Selected": "Absence of water is triggered by probe 1 and 2" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 136, "Node": 36, "Genre": "Config", "Help": "When the parameter 0x87 is set to 2 or 3, it can get the sensor probes status through this configuration value.", "ValueIDKey": 38280597446852628, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/56576470933045270/,{ "Label": "Temperature sensor calibration", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 201, "Node": 36, "Genre": "Config", "Help": "Temperature calibration (the available value range is [-128, 127] or [-12.8C, 12.7C]). Note: 1. High byte is the calibration value. Low byte is the unit (0x00=Celsius, 0x01=Fahrenheit). 2. The calibration value (high byte) contains one decimal point. E.g. if the value is set to 20 (0x1400), the calibration value is 2.0 C (EU/AU version) or if the value is set to 20 (0x1401), the calibration value is 2.0 F(US version). 3. The calibration value (high byte) = standard value - measure value. E.g. If measure value =25.3C and the standard value = 23.2C, so the calibration value= 23.2C - 25.3C= -2.1C (0xEB). If the measure value =30.1C and the standard value = 33.2C, so the calibration value= 33.2C - 30.1C=3.1C (0x1F).", "ValueIDKey": 56576470933045270, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/70931694745288724/,{ "Label": "Lock/Unlock Configuration", "Value": { "List": [ { "Value": 0, "Label": "Unlock" }, { "Value": 1, "Label": "Lock" } ], "Selected": "Unlock" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 252, "Node": 36, "Genre": "Config", "Help": "Lock/ unlock all configuration parameters", "ValueIDKey": 70931694745288724, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/112/value/71776119675420692/,{ "Label": "Reset To Factory Defaults", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "Reset to factory default setting" }, { "Value": 1431655765, "Label": "Reset to factory default setting and removed from the z-wave network" } ], "Selected": "Reset to factory default setting" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 255, "Node": 36, "Genre": "Config", "Help": "Reset to factory defaults", "ValueIDKey": 71776119675420692, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/,{ "Label": "Instance 1: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578772, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/113/value/72057594647953425/,{ "Label": "Instance 1: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953425, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/618430483/,{ "Label": "Loaded Config Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 36, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 618430483, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/281475595141139/,{ "Label": "Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 36, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475595141139, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/562950571851795/,{ "Label": "Latest Available Config File Revision", "Value": 10, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 36, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950571851795, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/844425548562455/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 36, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425548562455, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/114/value/1125900525273111/,{ "Label": "Serial Number", "Value": "0d000100010108010100000004030800000000", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 36, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900525273111, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/618446868/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 36, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 618446868, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/281475595157521/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 36, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475595157521, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/562950571868184/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 36, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950571868184, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/844425548578833/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 36, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425548578833, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1125900525289492/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 36, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900525289492, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1407375502000150/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 36, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375502000150, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1688850478710808/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 36, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850478710808, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/1970325455421464/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 36, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325455421464, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/2251800432132116/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 36, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800432132116, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/115/value/2533275408842774/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 36, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275408842774, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/128/value/610271249/,{ "Label": "Battery Level", "Value": 100, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 36, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 610271249, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/281475595436051/,{ "Label": "Minimum Wake-up Interval", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 36, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475595436051, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/562950572146707/,{ "Label": "Maximum Wake-up Interval", "Value": 16777200, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 36, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950572146707, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/844425548857363/,{ "Label": "Default Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 36, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425548857363, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/1125900525568019/,{ "Label": "Wake-up Interval Step", "Value": 240, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 36, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900525568019, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/132/value/618725395/,{ "Label": "Wake-up Interval", "Value": 3600, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 36, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 618725395, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/value/618758167/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 36, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 618758167, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/value/281475595468823/,{ "Label": "Protocol Version", "Value": "4.54", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 36, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475595468823, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/1/commandclass/134/value/562950572179479/,{ "Label": "Application Version", "Value": "1.05", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 36, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950572179479, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/,{ "Instance": 2, "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/,{ "Instance": 2, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/value/618102817/,{ "Label": "Instance 2: ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 36, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 618102817, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/value/281475594813478/,{ "Label": "Instance 2: InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 36, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475594813478, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/94/value/562950571524134/,{ "Label": "Instance 2: UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 2, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 36, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950571524134, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/113/,{ "Instance": 2, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/113/value/1407375493578788/,{ "Label": "Instance 2: Water", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 2, "Label": "Water Leak at Unknown Location" } ], "Selected": "Clear" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 5, "Node": 36, "Genre": "User", "Help": "Water Alerts", "ValueIDKey": 1407375493578788, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/instance/2/commandclass/113/value/72057594647953441/,{ "Label": "Instance 2: Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 2, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 36, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594647953441, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 5, "Members": [ "1.1" ], "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/2/,{ "Name": "Send the configuration parameter 0x88", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/3/,{ "Name": "Send Basic Set when the Sensor probe 1 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/36/association/4/,{ "Name": "Send Basic Set when the Sensor probe 2 is triggered", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/37/,{ "NodeID": 37, "NodeQueryStage": "CacheLoad", "isListening": false, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0005:0002", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa005.png", "Description": "Aeotec TriSensor is a universal Z-Wave Plus compatible product, consists of temperature, lighting and motion sensors, powered by a CR123A battery. It can be included and operated in any Z-Wave network with other Z-Wave certified devices from other manufacturers and/or other applications. By the built-in motion sensor, an alam will be sent to the gateway when the motion sensor is triggered.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2919/TriSensor user manual 20180416.pdf", "ProductPageURL": "", "InclusionHelp": "Press once TriSensor’s Action Button. If it is the first installation, the yellow LED will keep solid until whole network processing is complete. If successful, the LED will flash white -> green -> white -> green, after 2 seconds finished. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once. If it is the S2 encryption network, please enter the first 5 digits of DSK.", "ExclusionHelp": "Press once TriSensor’s Action Button, the Purple LED will keep solid until whole network processing is complete. If the exclusion is successful, the LED will flash white -> green - >white -> green and then LED will pulse a blue. If failed, the yellow LED lasts for 30 seconds, then the green LED flashes once.", "ResetHelp": "1. Power up the device. 2. Press and hold the button for 15s until Red Led is blinking,then release the button. Note: Please use this procedure only when the network primary controller is missing or otherwise inoperable.", "WakeupHelp": "Press and hold the button at least 2s until Red Led is on and then release the button,device will send wakeup notification to controller if device is in a Z-Wave network.", "ProductSupportURL": "", "Frequency": "", "Name": "TriSensor", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA005 TriSensor", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Notification Sensor", "NodeGeneric": 7, "NodeSpecificString": "Notification Sensor", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0102", "NodeProductID": "0x0005", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 3} +OpenZWave/1/node/37/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/32/,{ "Instance": 1, "CommandClassId": 32, "CommandClass": "COMMAND_CLASS_BASIC", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/32/value/621281297/,{ "Label": "Basic", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BASIC", "Index": 0, "Node": 37, "Genre": "Basic", "Help": "Basic status of the node", "ValueIDKey": 621281297, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/48/,{ "Instance": 1, "CommandClassId": 48, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/48/value/625737744/,{ "Label": "Sensor", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_BINARY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Binary Sensor State", "ValueIDKey": 625737744, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/,{ "Instance": 1, "CommandClassId": 49, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/281475602464786/,{ "Label": "Air Temperature", "Value": 20.700000762939454, "Units": "C", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 1, "Node": 37, "Genre": "User", "Help": "Air Temperature Sensor Value", "ValueIDKey": 281475602464786, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/844425555886098/,{ "Label": "Illuminance", "Value": 0.0, "Units": "Lux", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Luminance Sensor Value", "ValueIDKey": 844425555886098, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/1234567890/,{ "Label": "Relative Humidity", "Value": 56.7, "Units": "%", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 3, "Node": 37, "Genre": "User", "Help": "Humidity Sensor Value", "ValueIDKey": 1234567890, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678901/,{ "Label": "Pressure", "Value": 123, "Units": "inHg", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Pressure Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/72057594672070676/,{ "Label": "Air Temperature Units", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 256, "Node": 37, "Genre": "System", "Help": "Air Temperature Sensor Available Units", "ValueIDKey": 72057594672070676, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678902/,{ "Label": "Fake Power", "Value": 123, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 90, "Node": 37, "Genre": "User", "Help": "Power Sensor Value", "ValueIDKey": 12345678902, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678903/,{ "Label": "Fake Energy", "Value": 456, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 91, "Node": 37, "Genre": "User", "Help": "Energy Sensor Value", "ValueIDKey": 12345678903, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678904/,{ "Label": "Fake Electric", "Value": 789, "Units": "W", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 92, "Node": 37, "Genre": "User", "Help": "Electric Sensor Value", "ValueIDKey": 12345678904, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/12345678905/,{ "Label": "Fake String", "Value": "fake", "Units": "", "Min": 0, "Max": 0, "Type": "Decimal", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 8, "Node": 37, "Genre": "User", "Help": "Fake String Sensor Value", "ValueIDKey": 12345678901, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/49/value/72620544625491988/,{ "Label": "Illuminance Units", "Value": { "List": [ { "Value": 1, "Label": "Lux" } ], "Selected": "Lux" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SENSOR_MULTILEVEL", "Index": 258, "Node": 37, "Genre": "System", "Help": "Luminance Sensor Available Units", "ValueIDKey": 72620544625491988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/value/634880017/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 37, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 634880017, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/value/281475611590678/,{ "Label": "InstallerIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 37, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475611590678, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/94/value/562950588301334/,{ "Label": "UserIcon", "Value": 3079, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 37, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950588301334, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/281475607691286/,{ "Label": "Motion Re-trigger Time", "Value": 30, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the delay time before PIR sensor can be triggered again to reset motion timeout counter. Value = 0 will disable PIR sensor from triggering until motion timeout has finished.", "ValueIDKey": 281475607691286, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/562950584401942/,{ "Label": "Motion clear time", "Value": 240, "Units": "Second", "Min": 1, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 37, "Genre": "Config", "Help": "This configures the clear time when your motion sensor times out and sends a no motion status.", "ValueIDKey": 562950584401942, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/844425561112596/,{ "Label": "Motion Sensitivity", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "1" }, { "Value": 2, "Label": "2" }, { "Value": 3, "Label": "3" }, { "Value": 4, "Label": "4" }, { "Value": 5, "Label": "5" }, { "Value": 6, "Label": "6" }, { "Value": 7, "Label": "7" }, { "Value": 8, "Label": "8" }, { "Value": 9, "Label": "9" }, { "Value": 10, "Label": "10" }, { "Value": 11, "Label": "11" } ], "Selected": "8" }, "Units": "", "Min": 0, "Max": 11, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the sensitivity that motion detect. 0 - PIR sensor disabled. 1 - Lowest sensitivity. 11 - Highest sensitivity.", "ValueIDKey": 844425561112596, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1125900537823252/,{ "Label": "Binary Sensor Report", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 37, "Genre": "Config", "Help": "Enable/disable sensor binary report when motion event is detected or cleared", "ValueIDKey": 1125900537823252, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1407375514533908/,{ "Label": "Disable BASIC_SET to Associated nodes", "Value": { "List": [ { "Value": 0, "Label": "Disabled All Group Basic Set Command" }, { "Value": 1, "Label": "Enabled Group 2" }, { "Value": 2, "Label": "Enabled Group 3 " }, { "Value": 3, "Label": "Enabled Group 2 and Group 3" } ], "Selected": "Enabled Group 2 and Group 3" }, "Units": "", "Min": 0, "Max": 3, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 5, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the enabled or disabled send BASIC_SET command to nodes that associated in group 2 and group 3.", "ValueIDKey": 1407375514533908, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1688850491244564/,{ "Label": "Basic Set Value Settings for Group 2", "Value": { "List": [ { "Value": 0, "Label": "0xFF when motion is triggered and 0x00 when motion is cleared" }, { "Value": 1, "Label": "0x00 when motion is triggered and 0xFF when motion is cleared" }, { "Value": 2, "Label": "0xFF when motion is triggered" }, { "Value": 3, "Label": "0x00 when motion is triggered" }, { "Value": 4, "Label": "0x00 when motion event is cleared" }, { "Value": 5, "Label": "0xFF when motion event is cleared" } ], "Selected": "0xFF when motion is triggered and 0x00 when motion is cleared" }, "Units": "", "Min": 0, "Max": 5, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 6, "Node": 37, "Genre": "Config", "Help": "Define Basic Set Value when motion event is triggered and / or cleared", "ValueIDKey": 1688850491244564, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/1970325467955222/,{ "Label": "Temperature Alarm Value", "Value": 239, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 7, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the threshold value that alarm level for temperature. When the current ambient temperature value is larger than this configuration value, device will send a BASIC_SET = 0xFF to nodes associated in group 3. If current temperature value is less than this value, device will send a BASIC_SET = 0x00 to nodes associated in group 3. Value = [Value] x 0.1(Celsius / Fahrenheit) Available Settings: -400 to 850 (40.0 to 85.0 Celsius) or -400 to 1185 (-40.0 to 118.5 Fahrenheit). Default value: 239 (23.9 Celsius) or 750 (75.0 Fahrenheit)", "ValueIDKey": 1970325467955222, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/2814750398087188/,{ "Label": "LED over TriSensor", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Enable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 10, "Node": 37, "Genre": "Config", "Help": "Enable or Disable LED over TriSensor This completely disables all LED reaction regardless of Parameter 9 - 13 settings", "ValueIDKey": 2814750398087188, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3096225374797844/,{ "Label": "Motion report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Green" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 11, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a motion report.", "ValueIDKey": 3096225374797844, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3377700351508500/,{ "Label": "Temperature report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 12, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a temperature report.", "ValueIDKey": 3377700351508500, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3659175328219156/,{ "Label": "Light report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 13, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a light report.", "ValueIDKey": 3659175328219156, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/3940650304929812/,{ "Label": "Battery report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 14, "Node": 37, "Genre": "Config", "Help": "It is possible to change the color of what the LED blinks when your TriSensor sends a battery report.", "ValueIDKey": 3940650304929812, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/4222125281640468/,{ "Label": "Wakeup report LED", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Red" }, { "Value": 2, "Label": "Green" }, { "Value": 3, "Label": "Blue" }, { "Value": 4, "Label": "Yellow" }, { "Value": 5, "Label": "Pink" }, { "Value": 6, "Label": "Cyan" }, { "Value": 7, "Label": "Purple" }, { "Value": 8, "Label": "Orange" } ], "Selected": "Disabled" }, "Units": "", "Min": 0, "Max": 8, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 15, "Node": 37, "Genre": "Config", "Help": "This setting changes the color of the LED when your TriSensor sends a wakeup report.", "ValueIDKey": 4222125281640468, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/5629500165193748/,{ "Label": "Temperature Scale Setting", "Value": { "List": [ { "Value": 0, "Label": "Celsius" }, { "Value": 1, "Label": "Fahrenheit" } ], "Selected": "Celsius" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 20, "Node": 37, "Genre": "Config", "Help": "Configure temperature sensor scale type, Temperature to report in Celsius or Fahrenheit", "ValueIDKey": 5629500165193748, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/5910975141904406/,{ "Label": "Temperature Threshold reporting", "Value": 20, "Units": "0.1", "Min": 0, "Max": 250, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 21, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in temperature to induce an automatic report for temperature sensor. Scale is identical setting in Parameter No.20. 0-> Disable Threshold Report for Temperature Sensor. Setting of value 20 can be a change of -2.0 or +2.0 (C or F depending on Parameter No.20) to induce automatic report or setting a value of 2 will be a change of 0.2(C or F). Available Settings: 0 to 250.", "ValueIDKey": 5910975141904406, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6192450118615062/,{ "Label": "Light intensity Threshold Value to Report", "Value": 100, "Units": "Lux", "Min": 0, "Max": 10000, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 22, "Node": 37, "Genre": "Config", "Help": "Change threshold value for change in lux to induce an automatic report for light sensor.", "ValueIDKey": 6192450118615062, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6473925095325718/,{ "Label": "Temperature Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 23, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for temperature sensor report. This value is larger, the battery life is longer. And the temperature value changed is not obvious.", "ValueIDKey": 6473925095325718, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/6755400072036374/,{ "Label": "Light Sensor Report Interval", "Value": 3600, "Units": "Second", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 24, "Node": 37, "Genre": "Config", "Help": "This parameter is configured the time interval for light sensor report. This value is larger, the battery life is longer. And the light intensity changed is not obvious.", "ValueIDKey": 6755400072036374, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/8444249932300310/,{ "Label": "Temperature Offset Value", "Value": 0, "Units": "0.1", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 30, "Node": 37, "Genre": "Config", "Help": "The current measuring temperature value can be add and minus a value by this setting. The scale can be decided by Parameter Number 20. Temperature Offset Value = [Value] * 0.1(Celsius / Fahrenheit) Available Settings: -200 to 200.", "ValueIDKey": 8444249932300310, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/8725724909010966/,{ "Label": "Light Intensity Offset Value", "Value": 0, "Units": "Lux", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 31, "Node": 37, "Genre": "Config", "Help": "The current measuring light intensity value can be add and minus a value by this setting. Available Settings: -1000 to 1000.", "ValueIDKey": 8725724909010966, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/112/value/28147498302046230/,{ "Label": "Light Sensor Calibrated Coefficient", "Value": 1024, "Units": "", "Min": 0, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 100, "Node": 37, "Genre": "Config", "Help": "This configuration defines the calibrated scale for ambient light intensity. Because the method and position that the sensor mounted and the cover of sensor will bring measurement error, user can get more real light intensity by this parameter setting. User should run the steps as blows for calibrating 1) Set this parameter value to default (Assumes the sensor has been added in a Z- Wave Network). 2) Place a digital light meter close to sensor and keep the same direction, monitor the light intensity value (Vm) which report to controller and record it. The same time user should record the value (Vs) of light meter. 3) The scale calibration formula: k = Vm / Vs. 4) The value of k is then multiplied by 1024 and rounded to the nearest whole number. 5) Set the value getting in 5) to this parameter, calibrate finished. For example, Vm = 300, Vs = 2600, then k = 2600 / 300 = 8.6667 k = 8.6667 * 1024 = 8874.7 => 8875. The parameter should be set to 8875.", "ValueIDKey": 28147498302046230, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/113/,{ "Instance": 1, "CommandClassId": 113, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/113/value/1970325463777300/,{ "Label": "Home Security", "Value": { "List": [ { "Value": 0, "Label": "Clear" }, { "Value": 8, "Label": "Motion Detected at Unknown Location" } ], "Selected": "Clear", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 7, "Node": 37, "Genre": "User", "Help": "Home Security Alerts", "ValueIDKey": 1970325463777300, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/113/value/72057594664730641/,{ "Label": "Previous Event Cleared", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_NOTIFICATION", "Index": 256, "Node": 37, "Genre": "User", "Help": "Previous Event that was sent", "ValueIDKey": 72057594664730641, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/635207699/,{ "Label": "Loaded Config Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 37, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 635207699, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/281475611918355/,{ "Label": "Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 37, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475611918355, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/562950588629011/,{ "Label": "Latest Available Config File Revision", "Value": 6, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 37, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950588629011, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/844425565339671/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 37, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425565339671, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/114/value/1125900542050327/,{ "Label": "Serial Number", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 37, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900542050327, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/635224084/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 37, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 635224084, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/281475611934737/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 37, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475611934737, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/562950588645400/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 37, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950588645400, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/844425565356049/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 37, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425565356049, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1125900542066708/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 37, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900542066708, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1407375518777366/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 37, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375518777366, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1688850495488024/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 37, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850495488024, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/1970325472198680/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 37, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325472198680, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/2251800448909332/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 37, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800448909332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/115/value/2533275425619990/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 37, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275425619990, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/128/,{ "Instance": 1, "CommandClassId": 128, "CommandClass": "COMMAND_CLASS_BATTERY", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/128/value/627048465/,{ "Label": "Battery Level", "Value": 90, "Units": "%", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_BATTERY", "Index": 0, "Node": 37, "Genre": "User", "Help": "Current Battery Level", "ValueIDKey": 627048465, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/,{ "Instance": 1, "CommandClassId": 132, "CommandClass": "COMMAND_CLASS_WAKE_UP", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/281475612213267/,{ "Label": "Minimum Wake-up Interval", "Value": 1800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 1, "Node": 37, "Genre": "System", "Help": "Minimum Time in seconds the device will wake up", "ValueIDKey": 281475612213267, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/562950588923923/,{ "Label": "Maximum Wake-up Interval", "Value": 64800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 2, "Node": 37, "Genre": "System", "Help": "Maximum Time in seconds the device will wake up", "ValueIDKey": 562950588923923, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/844425565634579/,{ "Label": "Default Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 3, "Node": 37, "Genre": "System", "Help": "The Default Wake-Up Interval the device will wake up", "ValueIDKey": 844425565634579, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/1125900542345235/,{ "Label": "Wake-up Interval Step", "Value": 60, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 4, "Node": 37, "Genre": "System", "Help": "Step Size on Wake-up interval", "ValueIDKey": 1125900542345235, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/132/value/635502611/,{ "Label": "Wake-up Interval", "Value": 28800, "Units": "Seconds", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_WAKE_UP", "Index": 0, "Node": 37, "Genre": "System", "Help": "How often the Device will Wake up to check for pending commands", "ValueIDKey": 635502611, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/value/635535383/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 37, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 635535383, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/value/281475612246039/,{ "Label": "Protocol Version", "Value": "4.61", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 37, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475612246039, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/instance/1/commandclass/134/value/562950588956695/,{ "Label": "Application Version", "Value": "2.15", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 37, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950588956695, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/37/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0", "1.1" ], "TimeStamp": 1579566891} +OpenZWave/1/node/37/association/2/,{ "Name": "BasicSet report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/37/association/3/,{ "Name": "Temperature Alarm report", "Help": "", "MaxAssociations": 5, "Members": [], "TimeStamp": 1579566891} +OpenZWave/1/node/39/,{ "NodeID": 39, "NodeQueryStage": "CacheLoad", "isListening": true, "isFlirs": false, "isBeaming": true, "isRouting": true, "isSecurityv1": false, "isZWavePlus": false, "isNIFRecieved": true, "isAwake": true, "isFailed": false, "MetaData": { "OZWInfoURL": "http://www.openzwave.com/device-database/0371:0002:0103", "ZWAProductURL": "", "ProductPic": "images/aeotec/zwa002.png", "Description": "✓ Standard form factor and appearance of the light bulb with 800 lm output ✓ RGBW: dimmable from 5% to 100%, tunable from 1800K to 6500K, and 16 million colors ✓ Possible to be included in groups, scenes, or schedules ✓ Suitable for indoor lighting: Corridors, Bedroom, Living Room, etc.", "ProductManualURL": "https://Products.Z-WaveAlliance.org/ProductManual/File?folder=&filename=Manuals/2881/AA LED Bulb 6 说明书(RGBW-AL001)_转曲-2dd.pdf", "ProductPageURL": "", "InclusionHelp": "Add for inclusion 1. Ensure the led bulb has been excluded outside the network. 2. Triggered by OFF ->ON (between 0.5-2 seconds each time) 3. LED solid yellow Color (0xFFFF00) during the pairing(Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to a Warm White LED at 100%  Success: Blinks between 100% White and Green 0x00FF00 color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ExclusionHelp": "Remove for exclusion 1. Assuming led bulb was added to controller. 2. Triggered by OFF -> ON -> OFF -> ON -> OFF -> ON (between 0.5-2 seconds each time). 3. LED Solid Purple/Violet Color (0xEE82EE) during the unpairing process. (Timeout is 10 seconds).  Failure: Blinks between 100% White and Red 0x0000FF color for 3 seconds (at a rate of 200ms per flash), Once 3 seconds have passed, the LED should return to the last color ( memory status(color cc set)) of LED Bulb.  Success: Blinks between 100% White and Blue 0x0000FF color for 3 seconds (at a rate of 200ms per flash). Once 3 seconds have passed, the LED should return to a Warm White LED at 100%.", "ResetHelp": "Reset the Device. 1. Assuming led bulb was added to controller and was power on. 2. RGBW bulb re-power 6 times (between 0.5-2 seconds each time). Note: ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON -> OFF -> ON 3. If the 6th power on, the led bulb change to Yellow color(into pairing process ), which means that the reset factory settings are successf. Using this action in case of the primary controller is missing or inoperable.", "WakeupHelp": "", "ProductSupportURL": "", "Frequency": "", "Name": "LED Bulb 6:Multi-Colour", "ProductPicBase64": "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" }, "Event": "nodeNaming", "TimeStamp": 1579566891, "NodeManufacturerName": "Aeotec Limited", "NodeProductName": "ZWA002 LED Bulb 6 Multi-Color", "NodeBasicString": "Routing Slave", "NodeBasic": 4, "NodeGenericString": "Multilevel Switch", "NodeGeneric": 17, "NodeSpecificString": "Multilevel Power Switch", "NodeSpecific": 1, "NodeManufacturerID": "0x0371", "NodeProductType": "0x0103", "NodeProductID": "0x0002", "NodeBaudRate": 100000, "NodeVersion": 4, "NodeGroups": 1} +OpenZWave/1/node/39/instance/1/,{ "Instance": 1, "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/,{ "Instance": 1, "CommandClassId": 38, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/1407375551070225/,{ "Label": "Dimming Duration", "Value": 255, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "Duration taken when changing the Level of a Device", "ValueIDKey": 1407375551070225, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/659128337/,{ "Label": "Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 0, "Node": 39, "Genre": "User", "Help": "The Current Level of the Device", "ValueIDKey": 659128337, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/281475635839000/,{ "Label": "Bright", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 1, "Node": 39, "Genre": "User", "Help": "Increase the Brightness of the Device", "ValueIDKey": 281475635839000, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/562950612549656/,{ "Label": "Dim", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 2, "Node": 39, "Genre": "User", "Help": "Decrease the Brightness of the Device", "ValueIDKey": 562950612549656, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/844425597648912/,{ "Label": "Ignore Start Level", "Value": true, "Units": "", "Min": 0, "Max": 0, "Type": "Bool", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Ignore the Start Level of the Device when increasing/decreasing brightness", "ValueIDKey": 844425597648912, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/38/value/1125900574359569/,{ "Label": "Start Level", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_MULTILEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "Start Level when Changing the Brightness of a Device", "ValueIDKey": 1125900574359569, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/39/,{ "Instance": 1, "CommandClassId": 39, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/39/value/667533332/,{ "Label": "Switch All", "Value": { "List": [ { "Value": 0, "Label": "Disabled" }, { "Value": 1, "Label": "Off Enabled" }, { "Value": 2, "Label": "On Enabled" }, { "Value": 255, "Label": "On and Off Enabled" } ], "Selected": "On and Off Enabled" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_SWITCH_ALL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Switch All Devices On/Off", "ValueIDKey": 667533332, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/,{ "Instance": 1, "CommandClassId": 51, "CommandClass": "COMMAND_CLASS_COLOR", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/562950621151251/,{ "Label": "Color Channels", "Value": 31, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 2, "Node": 39, "Genre": "System", "Help": "Color Capabilities of the device", "ValueIDKey": 562950621151251, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/659341335/,{ "Label": "Color", "Value": "#000000FF00", "Units": "#RRGGBBWWCW", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 0, "Node": 39, "Genre": "User", "Help": "Color (in RGB format)", "ValueIDKey": 659341335, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/51/value/281475636051988/,{ "Label": "Color Index", "Value": { "List": [ { "Value": 0, "Label": "Off" }, { "Value": 1, "Label": "Cool White" }, { "Value": 2, "Label": "Warm White" }, { "Value": 3, "Label": "Red" }, { "Value": 4, "Label": "Lime" }, { "Value": 5, "Label": "Blue" }, { "Value": 6, "Label": "Yellow" }, { "Value": 7, "Label": "Cyan" }, { "Value": 8, "Label": "Magenta" }, { "Value": 9, "Label": "Silver" }, { "Value": 10, "Label": "Gray" }, { "Value": 11, "Label": "Maroon" }, { "Value": 12, "Label": "Olive" }, { "Value": 13, "Label": "Green" }, { "Value": 14, "Label": "Purple" }, { "Value": 15, "Label": "Teal" }, { "Value": 16, "Label": "Navy" }, { "Value": 17, "Label": "Custom" } ], "Selected": "Warm White" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_COLOR", "Index": 1, "Node": 39, "Genre": "User", "Help": "Preset Color", "ValueIDKey": 281475636051988, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/,{ "Instance": 1, "CommandClassId": 94, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/668434449/,{ "Label": "ZWave+ Version", "Value": 1, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 0, "Node": 39, "Genre": "System", "Help": "ZWave+ Version Supported on the Device", "ValueIDKey": 668434449, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/281475645145110/,{ "Label": "InstallerIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 1, "Node": 39, "Genre": "System", "Help": "Icon File to use for the Installer Application", "ValueIDKey": 281475645145110, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/94/value/562950621855766/,{ "Label": "UserIcon", "Value": 1536, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_ZWAVEPLUS_INFO", "Index": 2, "Node": 39, "Genre": "System", "Help": "Icon File to use for the User Application", "ValueIDKey": 562950621855766, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/,{ "Instance": 1, "CommandClassId": 112, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/281475641245716/,{ "Label": "User custom mode LED animations", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Blink Colors in order mode" }, { "Value": 2, "Label": "Randomized blink color mode" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 2, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 1, "Node": 39, "Genre": "Config", "Help": "User custom mode for LED animations", "ValueIDKey": 281475641245716, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/562950617956372/,{ "Label": "Strobe over Custom Color", "Value": { "List": [ { "Value": 0, "Label": "Disable" }, { "Value": 1, "Label": "Enable" } ], "Selected": "Disable" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 2, "Node": 39, "Genre": "Config", "Help": "Enable/Disable Strobe over Custom Color.", "ValueIDKey": 562950617956372, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/844425594667027/,{ "Label": "Set the rate of change to next color in Custom Mode", "Value": 50, "Units": "ms", "Min": 5, "Max": 8640000, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 3, "Node": 39, "Genre": "Config", "Help": "Set the rate of change to next color in Custom Mode.", "ValueIDKey": 844425594667027, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/1125900571377681/,{ "Label": "Set color that LED Bulb blinks", "Value": 1, "Units": "", "Min": 1, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 4, "Node": 39, "Genre": "Config", "Help": "Set color that LED Bulb blinks in Blink Mode.", "ValueIDKey": 1125900571377681, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/4503600291905553/,{ "Label": "Ramp rate when dimming using Multilevel Switch", "Value": 20, "Units": "100ms", "Min": 0, "Max": 100, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 16, "Node": 39, "Genre": "Config", "Help": "Specifying the ramp rate when dimming using Multilevel Switch V1 CC in 100ms.", "ValueIDKey": 4503600291905553, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/22517998801387540/,{ "Label": "Notification", "Value": { "List": [ { "Value": 0, "Label": "Nothing" }, { "Value": 1, "Label": "Basic CC report" } ], "Selected": "Basic CC report" }, "Units": "", "Min": 0, "Max": 1, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 80, "Node": 39, "Genre": "Config", "Help": "Enable to send notifications to associated devices (Group 1) when the state of LED Bulb is changed.", "ValueIDKey": 22517998801387540, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/22799473778098198/,{ "Label": "Warm White temperature", "Value": 2700, "Units": "k", "Min": 2700, "Max": 4999, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 81, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in warm white color component. available value: 2700k to 4999k", "ValueIDKey": 22799473778098198, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/112/value/23080948754808854/,{ "Label": "cold white temperature", "Value": 6500, "Units": "k", "Min": 5000, "Max": 6500, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_CONFIGURATION", "Index": 82, "Node": 39, "Genre": "Config", "Help": "Adjusting the color temperature in cold white color component. available value:5000k to 6500k", "ValueIDKey": 23080948754808854, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/,{ "Instance": 1, "CommandClassId": 114, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/668762131/,{ "Label": "Loaded Config Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 0, "Node": 39, "Genre": "System", "Help": "Revision of the Config file currently loaded", "ValueIDKey": 668762131, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/281475645472787/,{ "Label": "Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 1, "Node": 39, "Genre": "System", "Help": "Revision of the Config file on the File System", "ValueIDKey": 281475645472787, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/562950622183443/,{ "Label": "Latest Available Config File Revision", "Value": 3, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 2, "Node": 39, "Genre": "System", "Help": "Latest Revision of the Config file available for download", "ValueIDKey": 562950622183443, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/844425598894103/,{ "Label": "Device ID", "Value": "", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 3, "Node": 39, "Genre": "System", "Help": "Manufacturer Specific Device ID/Model", "ValueIDKey": 844425598894103, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/114/value/1125900575604759/,{ "Label": "Serial Number", "Value": "00001cd6bda18c83", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_MANUFACTURER_SPECIFIC", "Index": 4, "Node": 39, "Genre": "System", "Help": "Device Serial Number", "ValueIDKey": 1125900575604759, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/,{ "Instance": 1, "CommandClassId": 115, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/668778516/,{ "Label": "Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 0, "Node": 39, "Genre": "System", "Help": "Output RF PowerLevel", "ValueIDKey": 668778516, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/281475645489169/,{ "Label": "Timeout", "Value": 0, "Units": "seconds", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 1, "Node": 39, "Genre": "System", "Help": "Timeout till the PowerLevel is reset to Normal", "ValueIDKey": 281475645489169, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/562950622199832/,{ "Label": "Set Powerlevel", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 2, "Node": 39, "Genre": "System", "Help": "Apply the Output PowerLevel and Timeout Values", "ValueIDKey": 562950622199832, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/844425598910481/,{ "Label": "Test Node", "Value": 0, "Units": "", "Min": 0, "Max": 255, "Type": "Byte", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 3, "Node": 39, "Genre": "System", "Help": "Node to Perform a test against", "ValueIDKey": 844425598910481, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1125900575621140/,{ "Label": "Test Powerlevel", "Value": { "List": [ { "Value": 0, "Label": "Normal" }, { "Value": 1, "Label": "-1dB" }, { "Value": 2, "Label": "-2dB" }, { "Value": 3, "Label": "-3dB" }, { "Value": 4, "Label": "-4dB" }, { "Value": 5, "Label": "-5dB" }, { "Value": 6, "Label": "-6dB" }, { "Value": 7, "Label": "-7dB" }, { "Value": 8, "Label": "-8dB" }, { "Value": 9, "Label": "-9dB" } ], "Selected": "Normal" }, "Units": "dB", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 4, "Node": 39, "Genre": "System", "Help": "PowerLevel to use for the Test", "ValueIDKey": 1125900575621140, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1407375552331798/,{ "Label": "Frame Count", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 5, "Node": 39, "Genre": "System", "Help": "How Many Messages to send to the Note for the Test", "ValueIDKey": 1407375552331798, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1688850529042456/,{ "Label": "Test", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 6, "Node": 39, "Genre": "System", "Help": "Perform a PowerLevel Test against the a Node", "ValueIDKey": 1688850529042456, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/1970325505753112/,{ "Label": "Report", "Value": false, "Units": "", "Min": 0, "Max": 0, "Type": "Button", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 7, "Node": 39, "Genre": "System", "Help": "Get the results of the latest PowerLevel Test against a Node", "ValueIDKey": 1970325505753112, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/2251800482463764/,{ "Label": "Test Status", "Value": { "List": [ { "Value": 0, "Label": "Failed" }, { "Value": 1, "Label": "Success" }, { "Value": 2, "Label": "In Progress" } ], "Selected": "Failed" }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 8, "Node": 39, "Genre": "System", "Help": "The Current Status of the last PowerNode Test Executed", "ValueIDKey": 2251800482463764, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/115/value/2533275459174422/,{ "Label": "Acked Frames", "Value": 0, "Units": "", "Min": -32768, "Max": 32767, "Type": "Short", "Instance": 1, "CommandClass": "COMMAND_CLASS_POWERLEVEL", "Index": 9, "Node": 39, "Genre": "System", "Help": "Number of Messages successfully Acked by the Target Node", "ValueIDKey": 2533275459174422, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/,{ "Instance": 1, "CommandClassId": 134, "CommandClass": "COMMAND_CLASS_VERSION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/669089815/,{ "Label": "Library Version", "Value": "3", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 0, "Node": 39, "Genre": "System", "Help": "Z-Wave Library Version", "ValueIDKey": 669089815, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/281475645800471/,{ "Label": "Protocol Version", "Value": "4.38", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 1, "Node": 39, "Genre": "System", "Help": "Z-Wave Protocol Version", "ValueIDKey": 281475645800471, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/134/value/562950622511127/,{ "Label": "Application Version", "Value": "2.00", "Units": "", "Min": 0, "Max": 0, "Type": "String", "Instance": 1, "CommandClass": "COMMAND_CLASS_VERSION", "Index": 2, "Node": 39, "Genre": "System", "Help": "Application Version", "ValueIDKey": 562950622511127, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueAdded", "TimeStamp": 1579566891} +OpenZWave/1/node/39/association/1/,{ "Name": "Lifeline", "Help": "", "MaxAssociations": 1, "Members": [ "1.0" ], "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/43/,{ "Instance": 1, "CommandClassId": 43, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "TimeStamp": 1579566891} +OpenZWave/1/node/39/instance/1/commandclass/43/value/562950622511127/,{ "Label": "Scene", "Value": 0, "Units": "", "Min": -2147483648, "Max": 2147483647, "Type": "Int", "Instance": 1, "CommandClass": "COMMAND_CLASS_SCENE_ACTIVATION", "Index": 0, "Node": 7, "Genre": "User", "Help": "", "ValueIDKey": 122339347, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579630367} +OpenZWave/1/node/39/instance/1/commandclass/91/,{ "Instance": 1, "CommandClassId": 91, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "TimeStamp": 1579630630} +OpenZWave/1/node/39/instance/1/commandclass/91/value/281476005806100/,{ "Label": "Scene 1", "Value": { "List": [ { "Value": 0, "Label": "Inactive" }, { "Value": 1, "Label": "Pressed 1 Time" }, { "Value": 2, "Label": "Key Released" }, { "Value": 3, "Label": "Key Held down" } ], "Selected": "Inactive", "Selected_id": 0 }, "Units": "", "Min": 0, "Max": 0, "Type": "List", "Instance": 1, "CommandClass": "COMMAND_CLASS_CENTRAL_SCENE", "Index": 1, "Node": 61, "Genre": "User", "Help": "", "ValueIDKey": 281476005806100, "ReadOnly": false, "WriteOnly": false, "ValueSet": false, "ValuePolled": false, "ChangeVerified": false, "Event": "valueChanged", "TimeStamp": 1579640710} \ No newline at end of file diff --git a/tests/fixtures/zwave_mqtt/sensor.json b/tests/fixtures/zwave_mqtt/sensor.json new file mode 100644 index 00000000000000..17b86f90809709 --- /dev/null +++ b/tests/fixtures/zwave_mqtt/sensor.json @@ -0,0 +1,38 @@ +{ + "topic": "OpenZWave/1/node/36/instance/1/commandclass/113/value/1407375493578772/", + "payload": { + "Label": "Instance 1: Water", + "Value": { + "List": [ + { + "Value": 0, + "Label": "Clear" + }, + { + "Value": 2, + "Label": "Water Leak at Unknown Location" + } + ], + "Selected": "Clear", + "Selected_id": 0 + }, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "List", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_NOTIFICATION", + "Index": 5, + "Node": 36, + "Genre": "User", + "Help": "Water Alerts", + "ValueIDKey": 1407375493578772, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} \ No newline at end of file diff --git a/tests/fixtures/zwave_mqtt/switch.json b/tests/fixtures/zwave_mqtt/switch.json new file mode 100644 index 00000000000000..0d3fc37e9b2421 --- /dev/null +++ b/tests/fixtures/zwave_mqtt/switch.json @@ -0,0 +1,25 @@ +{ + "topic": "OpenZWave/1/node/32/instance/1/commandclass/37/value/541671440/", + "payload": { + "Label": "Switch", + "Value": false, + "Units": "", + "Min": 0, + "Max": 0, + "Type": "Bool", + "Instance": 1, + "CommandClass": "COMMAND_CLASS_SWITCH_BINARY", + "Index": 0, + "Node": 32, + "Genre": "User", + "Help": "Turn On/Off Device", + "ValueIDKey": 541671440, + "ReadOnly": false, + "WriteOnly": false, + "ValueSet": false, + "ValuePolled": false, + "ChangeVerified": false, + "Event": "valueAdded", + "TimeStamp": 1579566891 + } +} diff --git a/tests/helpers/test_check_config.py b/tests/helpers/test_check_config.py index 7a5d606bcc8b6e..ffc05544694368 100644 --- a/tests/helpers/test_check_config.py +++ b/tests/helpers/test_check_config.py @@ -1,6 +1,5 @@ """Test check_config helper.""" import logging -from unittest.mock import patch from homeassistant.config import YAML_CONFIG_FILE from homeassistant.helpers.check_config import ( @@ -8,6 +7,7 @@ async_check_ha_config_file, ) +from tests.async_mock import patch from tests.common import patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 03c3965fad7dbf..c4b87b667faad5 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -1,12 +1,12 @@ """Test the condition helper.""" -from unittest.mock import patch - import pytest from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import condition from homeassistant.util import dt +from tests.async_mock import patch + async def test_invalid_condition(hass): """Test if invalid condition raises.""" diff --git a/tests/helpers/test_config_entry_oauth2_flow.py b/tests/helpers/test_config_entry_oauth2_flow.py index a72f3f51ee78f9..9826d60025f8f8 100644 --- a/tests/helpers/test_config_entry_oauth2_flow.py +++ b/tests/helpers/test_config_entry_oauth2_flow.py @@ -2,13 +2,13 @@ import asyncio import logging import time -from unittest.mock import patch import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.helpers import config_entry_oauth2_flow +from tests.async_mock import patch from tests.common import MockConfigEntry, mock_platform TEST_DOMAIN = "oauth2_test" diff --git a/tests/helpers/test_config_validation.py b/tests/helpers/test_config_validation.py index 1d0824628492ff..bee463092fff16 100644 --- a/tests/helpers/test_config_validation.py +++ b/tests/helpers/test_config_validation.py @@ -3,7 +3,6 @@ import enum import os from socket import _GLOBAL_DEFAULT_TIMEOUT -from unittest.mock import Mock, patch import uuid import pytest @@ -12,6 +11,8 @@ import homeassistant import homeassistant.helpers.config_validation as cv +from tests.async_mock import Mock, patch + def test_boolean(): """Test boolean validation.""" diff --git a/tests/helpers/test_deprecation.py b/tests/helpers/test_deprecation.py index 38410c3bf0fdf1..c7e903f7b167dd 100644 --- a/tests/helpers/test_deprecation.py +++ b/tests/helpers/test_deprecation.py @@ -1,8 +1,8 @@ """Test deprecation helpers.""" -from unittest.mock import MagicMock, patch - from homeassistant.helpers.deprecation import deprecated_substitute, get_deprecated +from tests.async_mock import MagicMock, patch + class MockBaseClass: """Mock base class for deprecated testing.""" diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index f81d20ecd662a9..ef5f92de79c6a4 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1,14 +1,14 @@ """Tests for the Device Registry.""" import asyncio -from unittest.mock import patch import pytest -from homeassistant.core import callback -from homeassistant.helpers import device_registry +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import CoreState, callback +from homeassistant.helpers import device_registry, entity_registry -import tests.async_mock -from tests.common import flush_store, mock_device_registry +from tests.async_mock import patch +from tests.common import MockConfigEntry, flush_store, mock_device_registry @pytest.fixture @@ -149,6 +149,7 @@ async def test_loading_from_storage(hass, hass_storage): "model": "model", "name": "name", "sw_version": "version", + "entry_type": "service", "area_id": "12345A", "name_by_user": "Test Friendly Name", } @@ -168,6 +169,7 @@ async def test_loading_from_storage(hass, hass_storage): assert entry.id == "abcdefghijklm" assert entry.area_id == "12345A" assert entry.name_by_user == "Test Friendly Name" + assert entry.entry_type == "service" assert isinstance(entry.config_entries, set) @@ -304,6 +306,9 @@ async def test_loading_saving_data(hass, registry): identifiers={("hue", "0123")}, manufacturer="manufacturer", model="via", + name="Original Name", + sw_version="Orig SW 1", + entry_type="device", ) orig_light = registry.async_get_or_create( @@ -317,6 +322,10 @@ async def test_loading_saving_data(hass, registry): assert len(registry.devices) == 2 + orig_via = registry.async_update_device( + orig_via.id, area_id="mock-area-id", name_by_user="mock-name-by-user" + ) + # Now load written data in new registry registry2 = device_registry.DeviceRegistry(hass) await flush_store(registry._store) @@ -474,7 +483,7 @@ async def test_update_remove_config_entries(hass, registry, update_events): async def test_loading_race_condition(hass): """Test only one storage load called when concurrent loading occurred .""" - with tests.async_mock.patch( + with patch( "homeassistant.helpers.device_registry.DeviceRegistry.async_load" ) as mock_load: results = await asyncio.gather( @@ -502,3 +511,76 @@ async def test_update_sw_version(registry): assert mock_save.call_count == 1 assert updated_entry != entry assert updated_entry.sw_version == sw_version + + +async def test_cleanup_device_registry(hass, registry): + """Test cleanup works.""" + config_entry = MockConfigEntry(domain="hue") + config_entry.add_to_hass(hass) + + d1 = registry.async_get_or_create( + identifiers={("hue", "d1")}, config_entry_id=config_entry.entry_id + ) + registry.async_get_or_create( + identifiers={("hue", "d2")}, config_entry_id=config_entry.entry_id + ) + d3 = registry.async_get_or_create( + identifiers={("hue", "d3")}, config_entry_id=config_entry.entry_id + ) + registry.async_get_or_create( + identifiers={("something", "d4")}, config_entry_id="non_existing" + ) + + ent_reg = await entity_registry.async_get_registry(hass) + ent_reg.async_get_or_create("light", "hue", "e1", device_id=d1.id) + ent_reg.async_get_or_create("light", "hue", "e2", device_id=d1.id) + ent_reg.async_get_or_create("light", "hue", "e3", device_id=d3.id) + + device_registry.async_cleanup(hass, registry, ent_reg) + + assert registry.async_get_device({("hue", "d1")}, set()) is not None + assert registry.async_get_device({("hue", "d2")}, set()) is None + assert registry.async_get_device({("hue", "d3")}, set()) is not None + assert registry.async_get_device({("something", "d4")}, set()) is None + + +async def test_cleanup_startup(hass): + """Test we run a cleanup on startup.""" + hass.state = CoreState.not_running + await device_registry.async_get_registry(hass) + + with patch( + "homeassistant.helpers.device_registry.Debouncer.async_call" + ) as mock_call: + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_call.mock_calls) == 1 + + +async def test_cleanup_entity_registry_change(hass): + """Test we run a cleanup when entity registry changes.""" + await device_registry.async_get_registry(hass) + ent_reg = await entity_registry.async_get_registry(hass) + + with patch( + "homeassistant.helpers.device_registry.Debouncer.async_call" + ) as mock_call: + entity = ent_reg.async_get_or_create("light", "hue", "e1") + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 0 + + # Normal update does not trigger + ent_reg.async_update_entity(entity.entity_id, name="updated") + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 0 + + # Device ID update triggers + ent_reg.async_get_or_create("light", "hue", "e1", device_id="bla") + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 1 + + # Removal also triggers + ent_reg.async_remove(entity.entity_id) + await hass.async_block_till_done() + assert len(mock_call.mock_calls) == 2 diff --git a/tests/helpers/test_entity.py b/tests/helpers/test_entity.py index dd734bb0dcb01b..70b72b1752f022 100644 --- a/tests/helpers/test_entity.py +++ b/tests/helpers/test_entity.py @@ -3,7 +3,6 @@ import asyncio from datetime import timedelta import threading -from unittest.mock import MagicMock, PropertyMock, patch import pytest @@ -13,6 +12,7 @@ from homeassistant.helpers import entity, entity_registry from homeassistant.helpers.entity_values import EntityValues +from tests.async_mock import MagicMock, PropertyMock, patch from tests.common import get_test_home_assistant, mock_registry @@ -139,7 +139,7 @@ async def async_update(): mock_entity.entity_id = "comp_test.test_entity" mock_entity.async_update = async_update - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await mock_entity.async_update_ha_state(True) assert mock_call.called assert len(mock_call.mock_calls) == 2 @@ -169,7 +169,7 @@ async def async_update(): mock_entity.entity_id = "comp_test.test_entity" mock_entity.async_update = async_update - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await mock_entity.async_update_ha_state(True) assert mock_call.called assert len(mock_call.mock_calls) == 2 @@ -198,7 +198,7 @@ async def async_update(): mock_entity.entity_id = "comp_test.test_entity" mock_entity.async_update = async_update - with patch.object(hass.loop, "call_later", MagicMock()) as mock_call: + with patch.object(hass.loop, "call_later") as mock_call: await mock_entity.async_device_update(warning=False) assert not mock_call.called diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 3f03dfece11b4c..eb24ea971a7165 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -704,6 +704,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): "model": "test-model", "name": "test-name", "sw_version": "test-sw", + "entry_type": "service", "via_device": ("hue", "via-id"), }, ), @@ -730,6 +731,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): assert device.model == "test-model" assert device.name == "test-name" assert device.sw_version == "test-sw" + assert device.entry_type == "service" assert device.via_device_id == via.id diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index b2bfd1f48ffacd..285f43b6d4dc4b 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -1,6 +1,5 @@ """Tests for the Entity Registry.""" import asyncio -from unittest.mock import patch import pytest @@ -9,6 +8,7 @@ from homeassistant.helpers import entity_registry import tests.async_mock +from tests.async_mock import patch from tests.common import MockConfigEntry, flush_store, mock_registry YAML__OPEN_PATH = "homeassistant.util.yaml.loader.open" @@ -241,12 +241,20 @@ async def test_loading_extra_values(hass, hass_storage): "unique_id": "disabled-hass", "disabled_by": "hass", }, + { + "entity_id": "test.invalid__entity", + "platform": "super_platform", + "unique_id": "invalid-hass", + "disabled_by": "hass", + }, ] }, } registry = await entity_registry.async_get_registry(hass) + assert len(registry.entities) == 4 + entry_with_name = registry.async_get_or_create( "test", "super_platform", "with-name" ) diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index f6e375acf0b336..654cf8483dbc13 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -1,7 +1,6 @@ """Test event helpers.""" # pylint: disable=protected-access from datetime import datetime, timedelta -from unittest.mock import patch from astral import Astral import pytest @@ -27,6 +26,7 @@ from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util +from tests.async_mock import patch from tests.common import async_fire_time_changed DEFAULT_TIME_ZONE = dt_util.DEFAULT_TIME_ZONE diff --git a/tests/helpers/test_instance_id.py b/tests/helpers/test_instance_id.py new file mode 100644 index 00000000000000..36e87b31a4368c --- /dev/null +++ b/tests/helpers/test_instance_id.py @@ -0,0 +1,26 @@ +"""Tests for instance ID helper.""" +from tests.async_mock import patch + + +async def test_get_id_empty(hass, hass_storage): + """Get unique ID.""" + uuid = await hass.helpers.instance_id.async_get() + assert uuid is not None + # Assert it's stored + assert hass_storage["core.uuid"]["data"]["uuid"] == uuid + + +async def test_get_id_migrate(hass, hass_storage): + """Migrate existing file.""" + with patch( + "homeassistant.util.json.load_json", return_value={"uuid": "1234"} + ), patch("os.path.isfile", return_value=True), patch("os.remove") as mock_remove: + uuid = await hass.helpers.instance_id.async_get() + + assert uuid == "1234" + + # Assert it's stored + assert hass_storage["core.uuid"]["data"]["uuid"] == uuid + + # assert old deleted + assert len(mock_remove.mock_calls) == 1 diff --git a/tests/helpers/test_integration_platform.py b/tests/helpers/test_integration_platform.py index d6c844c0d914eb..6f0e56d34c869a 100644 --- a/tests/helpers/test_integration_platform.py +++ b/tests/helpers/test_integration_platform.py @@ -1,8 +1,7 @@ """Test integration platform helpers.""" -from unittest.mock import Mock - from homeassistant.setup import ATTR_COMPONENT, EVENT_COMPONENT_LOADED +from tests.async_mock import Mock from tests.common import mock_platform diff --git a/tests/helpers/test_network.py b/tests/helpers/test_network.py index d4c5366b879482..0fef40a82f41c3 100644 --- a/tests/helpers/test_network.py +++ b/tests/helpers/test_network.py @@ -1,9 +1,9 @@ """Test network helper.""" -from unittest.mock import Mock, patch - from homeassistant.components import cloud from homeassistant.helpers import network +from tests.async_mock import Mock, patch + async def test_get_external_url(hass): """Test get_external_url.""" diff --git a/tests/helpers/test_state.py b/tests/helpers/test_state.py index d8202b88b46045..b19669d13f4c42 100644 --- a/tests/helpers/test_state.py +++ b/tests/helpers/test_state.py @@ -1,7 +1,6 @@ """Test state helpers.""" import asyncio from datetime import timedelta -from unittest.mock import patch import pytest @@ -22,6 +21,7 @@ from homeassistant.helpers import state from homeassistant.util import dt as dt_util +from tests.async_mock import patch from tests.common import async_mock_service diff --git a/tests/helpers/test_sun.py b/tests/helpers/test_sun.py index b8ecd1ed86aa96..a877c7cdb006a9 100644 --- a/tests/helpers/test_sun.py +++ b/tests/helpers/test_sun.py @@ -1,12 +1,13 @@ """The tests for the Sun helpers.""" # pylint: disable=protected-access from datetime import datetime, timedelta -from unittest.mock import patch from homeassistant.const import SUN_EVENT_SUNRISE, SUN_EVENT_SUNSET import homeassistant.helpers.sun as sun import homeassistant.util.dt as dt_util +from tests.async_mock import patch + def test_next_events(hass): """Test retrieving next sun events.""" diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 6b3e0774bd85b0..a698c7af6e7e34 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -2,7 +2,6 @@ from datetime import datetime import math import random -from unittest.mock import patch import pytest import pytz @@ -21,6 +20,8 @@ import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem +from tests.async_mock import patch + def _set_up_units(hass): """Set up the tests.""" diff --git a/tests/mock/zwave.py b/tests/mock/zwave.py index 9089f159761292..1049108f9de62a 100644 --- a/tests/mock/zwave.py +++ b/tests/mock/zwave.py @@ -1,8 +1,8 @@ """Mock helpers for Z-Wave component.""" -from unittest.mock import MagicMock - from pydispatch import dispatcher +from tests.async_mock import MagicMock + def value_changed(value): """Fire a value changed.""" diff --git a/tests/scripts/test_auth.py b/tests/scripts/test_auth.py index 3ab194508793e2..c9ada99dc29c9a 100644 --- a/tests/scripts/test_auth.py +++ b/tests/scripts/test_auth.py @@ -1,11 +1,10 @@ """Test the auth script to manage local users.""" -from unittest.mock import Mock, patch - import pytest from homeassistant.auth.providers import homeassistant as hass_auth from homeassistant.scripts import auth as script_auth +from tests.async_mock import Mock, patch from tests.common import register_auth_provider diff --git a/tests/scripts/test_check_config.py b/tests/scripts/test_check_config.py index 737c3b56ecf38d..d28b5f6953001f 100644 --- a/tests/scripts/test_check_config.py +++ b/tests/scripts/test_check_config.py @@ -1,10 +1,10 @@ """Test check_config script.""" import logging -from unittest.mock import patch from homeassistant.config import YAML_CONFIG_FILE import homeassistant.scripts.check_config as check_config +from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files _LOGGER = logging.getLogger(__name__) diff --git a/tests/scripts/test_init.py b/tests/scripts/test_init.py index 8feef2d33844e2..2c14bfdcf0a4a2 100644 --- a/tests/scripts/test_init.py +++ b/tests/scripts/test_init.py @@ -1,8 +1,8 @@ """Test script init.""" -from unittest.mock import patch - import homeassistant.scripts as scripts +from tests.async_mock import patch + @patch("homeassistant.scripts.get_default_config_dir", return_value="/default") def test_config_per_platform(mock_def): diff --git a/tests/test_data_entry_flow.py b/tests/test_data_entry_flow.py index ad63dae6ee69c9..64b8587fe7c9a1 100644 --- a/tests/test_data_entry_flow.py +++ b/tests/test_data_entry_flow.py @@ -58,7 +58,14 @@ async def async_step_init(self, user_input=None): assert form["errors"]["base"] == "1" form = await manager.async_configure(form["flow_id"]) assert form["errors"]["base"] == "2" - assert len(manager.async_progress()) == 1 + assert manager.async_progress() == [ + { + "flow_id": form["flow_id"], + "handler": "test", + "step_id": "init", + "context": {}, + } + ] assert len(manager.mock_created_entries) == 0 diff --git a/tests/test_main.py b/tests/test_main.py index 5ec6460301f1c4..40c34b77b50fd9 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -1,9 +1,9 @@ """Test methods in __main__.""" -from unittest.mock import PropertyMock, patch - from homeassistant import __main__ as main from homeassistant.const import REQUIRED_PYTHON_VER +from tests.async_mock import PropertyMock, patch + @patch("sys.exit") def test_validate_python(mock_exit): diff --git a/tests/util/test_async.py b/tests/util/test_async.py index 33280895ba2f3f..b60b4097c52041 100644 --- a/tests/util/test_async.py +++ b/tests/util/test_async.py @@ -1,12 +1,13 @@ """Tests for async util methods from Python source.""" import asyncio from unittest import TestCase -from unittest.mock import MagicMock, Mock, patch import pytest from homeassistant.util import async_ as hasync +from tests.async_mock import MagicMock, Mock, patch + @patch("asyncio.coroutines.iscoroutine") @patch("concurrent.futures.Future") diff --git a/tests/util/test_init.py b/tests/util/test_init.py index 2ffca07082bc21..ef5ecd898d773e 100644 --- a/tests/util/test_init.py +++ b/tests/util/test_init.py @@ -1,12 +1,13 @@ """Test Home Assistant util methods.""" from datetime import datetime, timedelta -from unittest.mock import MagicMock, patch import pytest from homeassistant import util import homeassistant.util.dt as dt_util +from tests.async_mock import MagicMock, patch + def test_sanitize_filename(): """Test sanitize_filename.""" diff --git a/tests/util/test_json.py b/tests/util/test_json.py index 258f266ff78c54..c2b6a428515529 100644 --- a/tests/util/test_json.py +++ b/tests/util/test_json.py @@ -7,7 +7,6 @@ import sys from tempfile import mkdtemp import unittest -from unittest.mock import Mock import pytest @@ -20,6 +19,8 @@ save_json, ) +from tests.async_mock import Mock + # Test data that can be saved as JSON TEST_JSON_A = {"a": 1, "B": "two"} TEST_JSON_B = {"a": "one", "B": 2} diff --git a/tests/util/test_yaml.py b/tests/util/test_yaml.py index 140859ccb7352a..4d6f4ce3ac9168 100644 --- a/tests/util/test_yaml.py +++ b/tests/util/test_yaml.py @@ -3,7 +3,6 @@ import logging import os import unittest -from unittest.mock import patch import pytest @@ -12,6 +11,7 @@ import homeassistant.util.yaml as yaml from homeassistant.util.yaml import loader as yaml_loader +from tests.async_mock import patch from tests.common import get_test_config_dir, patch_yaml_files