diff --git a/.coveragerc b/.coveragerc index 3439bc1f148..c3a56fa27c0 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,8 +8,10 @@ omit = homeassistant/scripts/*.py # omit pieces of code that rely on external devices being present - homeassistant/components/acer_projector/switch.py + homeassistant/components/acer_projector/* + homeassistant/components/actiontec/const.py homeassistant/components/actiontec/device_tracker.py + homeassistant/components/actiontec/model.py homeassistant/components/acmeda/__init__.py homeassistant/components/acmeda/base.py homeassistant/components/acmeda/const.py @@ -23,9 +25,8 @@ omit = homeassistant/components/adguard/sensor.py homeassistant/components/adguard/switch.py homeassistant/components/ads/* - homeassistant/components/aemet/abstract_aemet_sensor.py homeassistant/components/aemet/weather_update_coordinator.py - homeassistant/components/aftership/sensor.py + homeassistant/components/aftership/* homeassistant/components/agent_dvr/__init__.py homeassistant/components/agent_dvr/alarm_control_panel.py homeassistant/components/agent_dvr/camera.py @@ -36,14 +37,14 @@ omit = homeassistant/components/airvisual/__init__.py homeassistant/components/airvisual/air_quality.py homeassistant/components/airvisual/sensor.py - homeassistant/components/aladdin_connect/cover.py + homeassistant/components/aladdin_connect/* homeassistant/components/alarmdecoder/__init__.py homeassistant/components/alarmdecoder/alarm_control_panel.py homeassistant/components/alarmdecoder/binary_sensor.py homeassistant/components/alarmdecoder/const.py homeassistant/components/alarmdecoder/sensor.py homeassistant/components/alpha_vantage/sensor.py - homeassistant/components/amazon_polly/tts.py + homeassistant/components/amazon_polly/* homeassistant/components/ambiclimate/climate.py homeassistant/components/ambient_station/* homeassistant/components/amcrest/* @@ -113,6 +114,10 @@ omit = homeassistant/components/bmw_connected_drive/lock.py homeassistant/components/bmw_connected_drive/notify.py homeassistant/components/bmw_connected_drive/sensor.py + homeassistant/components/bosch_shc/__init__.py + homeassistant/components/bosch_shc/const.py + homeassistant/components/bosch_shc/binary_sensor.py + homeassistant/components/bosch_shc/entity.py homeassistant/components/braviatv/__init__.py homeassistant/components/braviatv/const.py homeassistant/components/braviatv/media_player.py @@ -304,7 +309,7 @@ omit = homeassistant/components/firmata/pin.py homeassistant/components/firmata/sensor.py homeassistant/components/firmata/switch.py - homeassistant/components/fitbit/sensor.py + homeassistant/components/fitbit/* homeassistant/components/fixer/sensor.py homeassistant/components/fleetgo/device_tracker.py homeassistant/components/flexit/climate.py @@ -330,9 +335,12 @@ omit = homeassistant/components/freebox/sensor.py homeassistant/components/freebox/switch.py homeassistant/components/fritz/__init__.py + homeassistant/components/fritz/binary_sensor.py homeassistant/components/fritz/common.py homeassistant/components/fritz/const.py homeassistant/components/fritz/device_tracker.py + homeassistant/components/fritz/sensor.py + homeassistant/components/fritz/services.py homeassistant/components/fritzbox_callmonitor/__init__.py homeassistant/components/fritzbox_callmonitor/const.py homeassistant/components/fritzbox_callmonitor/base.py @@ -342,6 +350,9 @@ omit = homeassistant/components/frontier_silicon/media_player.py homeassistant/components/futurenow/light.py homeassistant/components/garadget/cover.py + homeassistant/components/garages_amsterdam/__init__.py + homeassistant/components/garages_amsterdam/binary_sensor.py + homeassistant/components/garages_amsterdam/sensor.py homeassistant/components/garmin_connect/__init__.py homeassistant/components/garmin_connect/const.py homeassistant/components/garmin_connect/sensor.py @@ -358,6 +369,7 @@ omit = homeassistant/components/goalfeed/* homeassistant/components/goalzero/__init__.py homeassistant/components/goalzero/binary_sensor.py + homeassistant/components/goalzero/switch.py homeassistant/components/google/* homeassistant/components/google_cloud/tts.py homeassistant/components/google_maps/device_tracker.py @@ -371,6 +383,7 @@ omit = homeassistant/components/greenwave/light.py homeassistant/components/group/notify.py homeassistant/components/growatt_server/sensor.py + homeassistant/components/growatt_server/__init__.py homeassistant/components/gstreamer/media_player.py homeassistant/components/gtfs/sensor.py homeassistant/components/guardian/__init__.py @@ -545,7 +558,6 @@ omit = homeassistant/components/life360/* homeassistant/components/lifx/* homeassistant/components/lifx_cloud/scene.py - homeassistant/components/lifx_legacy/light.py homeassistant/components/lightwave/* homeassistant/components/limitlessled/light.py homeassistant/components/linksys_smart/device_tracker.py @@ -599,6 +611,9 @@ omit = homeassistant/components/meteo_france/sensor.py homeassistant/components/meteo_france/weather.py homeassistant/components/meteoalarm/* + homeassistant/components/meteoclimatic/__init__.py + homeassistant/components/meteoclimatic/const.py + homeassistant/components/meteoclimatic/weather.py homeassistant/components/metoffice/sensor.py homeassistant/components/metoffice/weather.py homeassistant/components/microsoft/tts.py @@ -618,9 +633,6 @@ omit = homeassistant/components/mjpeg/camera.py homeassistant/components/mochad/* homeassistant/components/modbus/climate.py - homeassistant/components/modbus/cover.py - homeassistant/components/modbus/modbus.py - homeassistant/components/modbus/switch.py homeassistant/components/modem_callerid/sensor.py homeassistant/components/motion_blinds/__init__.py homeassistant/components/motion_blinds/const.py @@ -656,7 +668,7 @@ omit = homeassistant/components/mystrom/binary_sensor.py homeassistant/components/mystrom/light.py homeassistant/components/mystrom/switch.py - homeassistant/components/n26/* + homeassistant/components/myq/__init__.py homeassistant/components/nad/media_player.py homeassistant/components/nanoleaf/light.py homeassistant/components/neato/__init__.py @@ -703,6 +715,7 @@ omit = homeassistant/components/omnilogic/__init__.py homeassistant/components/omnilogic/common.py homeassistant/components/omnilogic/sensor.py + homeassistant/components/omnilogic/switch.py homeassistant/components/ondilo_ico/__init__.py homeassistant/components/ondilo_ico/api.py homeassistant/components/ondilo_ico/const.py @@ -862,6 +875,7 @@ omit = homeassistant/components/russound_rnet/media_player.py homeassistant/components/sabnzbd/* homeassistant/components/saj/sensor.py + homeassistant/components/samsungtv/bridge.py homeassistant/components/satel_integra/* homeassistant/components/schluter/* homeassistant/components/scrape/sensor.py @@ -905,6 +919,11 @@ omit = homeassistant/components/skybeacon/sensor.py homeassistant/components/skybell/* homeassistant/components/slack/notify.py + homeassistant/components/sia/__init__.py + homeassistant/components/sia/alarm_control_panel.py + homeassistant/components/sia/const.py + homeassistant/components/sia/hub.py + homeassistant/components/sia/utils.py homeassistant/components/sinch/* homeassistant/components/slide/* homeassistant/components/sma/__init__.py @@ -931,7 +950,12 @@ omit = homeassistant/components/soma/__init__.py homeassistant/components/soma/cover.py homeassistant/components/soma/sensor.py - homeassistant/components/somfy/* + homeassistant/components/somfy/__init__.py + homeassistant/components/somfy/api.py + homeassistant/components/somfy/climate.py + homeassistant/components/somfy/cover.py + homeassistant/components/somfy/sensor.py + homeassistant/components/somfy/switch.py homeassistant/components/somfy_mylink/__init__.py homeassistant/components/somfy_mylink/cover.py homeassistant/components/sonos/* @@ -940,7 +964,6 @@ omit = homeassistant/components/speedtestdotnet/* homeassistant/components/spider/* homeassistant/components/splunk/* - homeassistant/components/spotcrime/sensor.py homeassistant/components/spotify/__init__.py homeassistant/components/spotify/media_player.py homeassistant/components/spotify/system_health.py @@ -964,6 +987,10 @@ omit = homeassistant/components/switchbot/switch.py homeassistant/components/switcher_kis/switch.py homeassistant/components/switchmate/switch.py + homeassistant/components/syncthing/__init__.py + homeassistant/components/syncthing/sensor.py + homeassistant/components/syncthru/__init__.py + homeassistant/components/syncthru/binary_sensor.py homeassistant/components/syncthru/sensor.py homeassistant/components/synology_chat/notify.py homeassistant/components/synology_dsm/__init__.py @@ -973,6 +1000,10 @@ omit = homeassistant/components/synology_dsm/switch.py homeassistant/components/synology_srm/device_tracker.py homeassistant/components/syslog/notify.py + homeassistant/components/system_bridge/__init__.py + homeassistant/components/system_bridge/const.py + homeassistant/components/system_bridge/binary_sensor.py + homeassistant/components/system_bridge/sensor.py homeassistant/components/systemmonitor/sensor.py homeassistant/components/tado/* homeassistant/components/tado/device_tracker.py @@ -1033,7 +1064,6 @@ omit = homeassistant/components/toon/switch.py homeassistant/components/torque/sensor.py homeassistant/components/totalconnect/__init__.py - homeassistant/components/totalconnect/alarm_control_panel.py homeassistant/components/totalconnect/binary_sensor.py homeassistant/components/totalconnect/const.py homeassistant/components/touchline/climate.py @@ -1206,7 +1236,6 @@ omit = homeassistant/components/supla/* homeassistant/components/zwave/util.py homeassistant/components/zwave_js/discovery.py - homeassistant/components/zwave_js/light.py homeassistant/components/zwave_js/sensor.py [report] diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 05726ab79e5..7c169580cb2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -36,19 +36,6 @@ - [ ] Breaking change (fix/feature causing existing functionality to break) - [ ] Code quality improvements to existing code or addition of tests -## Example entry for `configuration.yaml`: - - -```yaml -# Example configuration.yaml - -``` - ## Additional information ssdp_confirm(None) --> ssdp_confirm({}) --> create_entry() @@ -185,7 +184,7 @@ class UpnpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): ) # Handle devices changing their UDN, only allow a single - existing_entries = self.hass.config_entries.async_entries(DOMAIN) + existing_entries = self._async_current_entries() for config_entry in existing_entries: entry_hostname = config_entry.data.get(CONFIG_ENTRY_HOSTNAME) if entry_hostname == discovery[DISCOVERY_HOSTNAME]: diff --git a/homeassistant/components/upnp/manifest.json b/homeassistant/components/upnp/manifest.json index e397d97f468..b130e721e35 100644 --- a/homeassistant/components/upnp/manifest.json +++ b/homeassistant/components/upnp/manifest.json @@ -1,9 +1,9 @@ { "domain": "upnp", - "name": "UPnP", + "name": "UPnP/IGD", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/upnp", - "requirements": ["async-upnp-client==0.16.2"], + "requirements": ["async-upnp-client==0.18.0"], "codeowners": ["@StevenLooman"], "ssdp": [ { diff --git a/homeassistant/components/upnp/sensor.py b/homeassistant/components/upnp/sensor.py index 3ffcb8d7426..54744490a86 100644 --- a/homeassistant/components/upnp/sensor.py +++ b/homeassistant/components/upnp/sensor.py @@ -2,13 +2,15 @@ from __future__ import annotations from datetime import timedelta -from typing import Any, Callable, Mapping +from typing import Any, Mapping from homeassistant.components.sensor import SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import DATA_BYTES, DATA_RATE_KIBIBYTES_PER_SECOND from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, @@ -82,7 +84,9 @@ async def async_setup_platform( async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the UPnP/IGD sensors.""" udn = config_entry.data[CONFIG_ENTRY_UDN] @@ -162,7 +166,7 @@ class UpnpSensor(CoordinatorEntity, SensorEntity): return self._sensor_type["unit"] @property - def device_info(self) -> Mapping[str, Any]: + def device_info(self) -> DeviceInfo: """Get device info.""" return { "connections": {(dr.CONNECTION_UPNP, self._device.udn)}, diff --git a/homeassistant/components/upnp/strings.json b/homeassistant/components/upnp/strings.json index 97e91c490e3..b0bc476ae61 100644 --- a/homeassistant/components/upnp/strings.json +++ b/homeassistant/components/upnp/strings.json @@ -1,16 +1,13 @@ { "config": { - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { - "init": { - }, "ssdp_confirm": { "description": "Do you want to set up this UPnP/IGD device?" }, "user": { "data": { - "usn": "Device", - "scan_interval": "Update interval (seconds, minimal 30)" + "unique_id": "Device" } } }, @@ -19,5 +16,14 @@ "no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]", "incomplete_discovery": "Incomplete discovery" } - } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval (seconds, minimal 30)" + } + } + } +} } diff --git a/homeassistant/components/upnp/translations/ca.json b/homeassistant/components/upnp/translations/ca.json index 5a3ed99a616..0fc68f8a9b7 100644 --- a/homeassistant/components/upnp/translations/ca.json +++ b/homeassistant/components/upnp/translations/ca.json @@ -9,7 +9,7 @@ "one": "un", "other": "altre" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vols configurar aquest dispositiu UPnP/IGD?" @@ -17,9 +17,19 @@ "user": { "data": { "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)", + "unique_id": "Dispositiu", "usn": "Dispositiu" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Interval d'actualitzaci\u00f3 (en segons, m\u00ednim 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/en.json b/homeassistant/components/upnp/translations/en.json index 1476c87e51f..c62ebfd2a87 100644 --- a/homeassistant/components/upnp/translations/en.json +++ b/homeassistant/components/upnp/translations/en.json @@ -5,7 +5,7 @@ "incomplete_discovery": "Incomplete discovery", "no_devices_found": "No devices found on the network" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Do you want to set up this UPnP/IGD device?" @@ -13,9 +13,19 @@ "user": { "data": { "scan_interval": "Update interval (seconds, minimal 30)", + "unique_id": "Device", "usn": "Device" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update interval (seconds, minimal 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/et.json b/homeassistant/components/upnp/translations/et.json index d5ee1897f98..2ac4884c9ea 100644 --- a/homeassistant/components/upnp/translations/et.json +++ b/homeassistant/components/upnp/translations/et.json @@ -9,7 +9,7 @@ "one": "\u00fcks", "other": "Teine" }, - "flow_title": "UPnP / IGD: {name}", + "flow_title": "{name}", "step": { "init": { "one": "\u00dcks", @@ -21,9 +21,19 @@ "user": { "data": { "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)", + "unique_id": "Seade", "usn": "Seade" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "P\u00e4ringute intervall (sekundites, v\u00e4hemalt 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/fr.json b/homeassistant/components/upnp/translations/fr.json index 7ef9ae20c82..fe1f1366d39 100644 --- a/homeassistant/components/upnp/translations/fr.json +++ b/homeassistant/components/upnp/translations/fr.json @@ -21,6 +21,7 @@ "user": { "data": { "scan_interval": "Intervalle de mise \u00e0 jour (secondes, minimum 30)", + "unique_id": "Appareil", "usn": "Appareil" } } diff --git a/homeassistant/components/upnp/translations/it.json b/homeassistant/components/upnp/translations/it.json index ca4e376432c..67a3a385dbc 100644 --- a/homeassistant/components/upnp/translations/it.json +++ b/homeassistant/components/upnp/translations/it.json @@ -9,7 +9,7 @@ "one": "Vuoto", "other": "Vuoto" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "Vuoi configurare questo dispositivo UPnP/IGD?" @@ -17,9 +17,19 @@ "user": { "data": { "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)", + "unique_id": "Dispositivo", "usn": "Dispositivo" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Intervallo di aggiornamento (secondi, minimo 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/nl.json b/homeassistant/components/upnp/translations/nl.json index 331d5850fc4..b4d690ca58c 100644 --- a/homeassistant/components/upnp/translations/nl.json +++ b/homeassistant/components/upnp/translations/nl.json @@ -9,7 +9,7 @@ "one": "Een", "other": "Ander" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "init": { "one": "Leeg", @@ -21,9 +21,19 @@ "user": { "data": { "scan_interval": "Update-interval (seconden, minimaal 30)", + "unique_id": "Apparaat", "usn": "Apparaat" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Update-interval (seconden, minimaal 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/no.json b/homeassistant/components/upnp/translations/no.json index 606fb8b6853..c92144bf40d 100644 --- a/homeassistant/components/upnp/translations/no.json +++ b/homeassistant/components/upnp/translations/no.json @@ -13,7 +13,7 @@ "two": "to", "zero": "ingen" }, - "flow_title": "", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u00d8nsker du \u00e5 sette opp denne UPnP/IGD-enheten?" @@ -21,9 +21,19 @@ "user": { "data": { "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)", + "unique_id": "Enhet", "usn": "Enhet" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "Oppdateringsintervall (sekunder, minimum 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/pl.json b/homeassistant/components/upnp/translations/pl.json index 4f519323fd0..3f66ec7c6f9 100644 --- a/homeassistant/components/upnp/translations/pl.json +++ b/homeassistant/components/upnp/translations/pl.json @@ -11,7 +11,7 @@ "one": "jeden", "other": "inne" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "init": { "few": "kilka", diff --git a/homeassistant/components/upnp/translations/ru.json b/homeassistant/components/upnp/translations/ru.json index 518cd182339..20c652fa1e0 100644 --- a/homeassistant/components/upnp/translations/ru.json +++ b/homeassistant/components/upnp/translations/ru.json @@ -11,7 +11,7 @@ "one": "\u043e\u0434\u0438\u043d", "other": "\u0434\u0440\u0443\u0433\u0438\u0435" }, - "flow_title": "UPnP/IGD: {name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e UPnP / IGD?" @@ -19,9 +19,19 @@ "user": { "data": { "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)", + "unique_id": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e", "usn": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u0418\u043d\u0442\u0435\u0440\u0432\u0430\u043b \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u044f (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445, \u043c\u0438\u043d\u0438\u043c\u0443\u043c 30)" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/upnp/translations/zh-Hant.json b/homeassistant/components/upnp/translations/zh-Hant.json index ceb8dda3263..80c92662d22 100644 --- a/homeassistant/components/upnp/translations/zh-Hant.json +++ b/homeassistant/components/upnp/translations/zh-Hant.json @@ -5,7 +5,7 @@ "incomplete_discovery": "\u672a\u5b8c\u6210\u63a2\u7d22", "no_devices_found": "\u7db2\u8def\u4e0a\u627e\u4e0d\u5230\u88dd\u7f6e" }, - "flow_title": "UPnP/IGD\uff1a{name}", + "flow_title": "{name}", "step": { "ssdp_confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a UPnP/IGD \u88dd\u7f6e\uff1f" @@ -13,9 +13,19 @@ "user": { "data": { "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09", + "unique_id": "\u88dd\u7f6e", "usn": "\u88dd\u7f6e" } } } + }, + "options": { + "step": { + "init": { + "data": { + "scan_interval": "\u66f4\u65b0\u9593\u9694\uff08\u79d2\u3001\u6700\u5c11 30 \u79d2\uff09" + } + } + } } } \ No newline at end of file diff --git a/homeassistant/components/usgs_earthquakes_feed/manifest.json b/homeassistant/components/usgs_earthquakes_feed/manifest.json index ef6fa7a982f..d38a5c056b8 100644 --- a/homeassistant/components/usgs_earthquakes_feed/manifest.json +++ b/homeassistant/components/usgs_earthquakes_feed/manifest.json @@ -2,7 +2,7 @@ "domain": "usgs_earthquakes_feed", "name": "U.S. Geological Survey Earthquake Hazards (USGS)", "documentation": "https://www.home-assistant.io/integrations/usgs_earthquakes_feed", - "requirements": ["geojson_client==0.4"], + "requirements": ["geojson_client==0.6"], "codeowners": ["@exxamalte"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index d28819a38cb..509c0562f97 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -5,10 +5,17 @@ import logging import voluptuous as vol -from homeassistant.components.sensor import SensorEntity +from homeassistant.components.sensor import ( + ATTR_LAST_RESET, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONF_NAME, + DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR, + ENERGY_WATT_HOUR, EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE, STATE_UNKNOWN, @@ -51,9 +58,13 @@ ATTR_SOURCE_ID = "source" ATTR_STATUS = "status" ATTR_PERIOD = "meter_period" ATTR_LAST_PERIOD = "last_period" -ATTR_LAST_RESET = "last_reset" ATTR_TARIFF = "tariff" +DEVICE_CLASS_MAP = { + ENERGY_WATT_HOUR: DEVICE_CLASS_ENERGY, + ENERGY_KILO_WATT_HOUR: DEVICE_CLASS_ENERGY, +} + ICON = "mdi:counter" PRECISION = 3 @@ -94,7 +105,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(meters) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_CALIBRATE_METER, @@ -120,7 +131,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._sensor_source_id = source_entity self._state = 0 self._last_period = 0 - self._last_reset = dt_util.now() + self._last_reset = dt_util.utcnow() self._collecting = None if name: self._name = name @@ -226,7 +237,7 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): if self._tariff_entity != entity_id: return _LOGGER.debug("Reset utility meter <%s>", self.entity_id) - self._last_reset = dt_util.now() + self._last_reset = dt_util.utcnow() self._last_period = str(self._state) self._state = 0 self.async_write_ha_state() @@ -273,8 +284,8 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): self._state = Decimal(state.state) self._unit_of_measurement = state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) self._last_period = state.attributes.get(ATTR_LAST_PERIOD) - self._last_reset = dt_util.parse_datetime( - state.attributes.get(ATTR_LAST_RESET) + self._last_reset = dt_util.as_utc( + dt_util.parse_datetime(state.attributes.get(ATTR_LAST_RESET)) ) if state.attributes.get(ATTR_STATUS) == COLLECTING: # Fake cancellation function to init the meter in similar state @@ -314,6 +325,16 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): """Return the state of the sensor.""" return self._state + @property + def device_class(self): + """Return the device class of the sensor.""" + return DEVICE_CLASS_MAP.get(self.unit_of_measurement) + + @property + def state_class(self): + """Return the device class of the sensor.""" + return STATE_CLASS_MEASUREMENT + @property def unit_of_measurement(self): """Return the unit the value is expressed in.""" @@ -331,7 +352,6 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): ATTR_SOURCE_ID: self._sensor_source_id, ATTR_STATUS: PAUSED if self._collecting is None else COLLECTING, ATTR_LAST_PERIOD: self._last_period, - ATTR_LAST_RESET: self._last_reset, } if self._period is not None: state_attr[ATTR_PERIOD] = self._period @@ -343,3 +363,8 @@ class UtilityMeterSensor(RestoreEntity, SensorEntity): def icon(self): """Return the icon to use in the frontend, if any.""" return ICON + + @property + def last_reset(self): + """Return the time when the sensor was last reset.""" + return self._last_reset diff --git a/homeassistant/components/utility_meter/services.yaml b/homeassistant/components/utility_meter/services.yaml index d33229f4e56..c3f95d22175 100644 --- a/homeassistant/components/utility_meter/services.yaml +++ b/homeassistant/components/utility_meter/services.yaml @@ -1,35 +1,45 @@ # Describes the format for available switch services reset: - description: Resets the counter of an utility meter. - fields: - entity_id: - description: Name(s) of the utility meter to reset - example: "utility_meter.energy" + name: Reset + description: Resets the counter of a utility meter. + target: + entity: + domain: utility_meter next_tariff: + name: Next Tariff description: Changes the tariff to the next one. - fields: - entity_id: - description: Name(s) of entities to reset - example: "utility_meter.energy" + target: + entity: + domain: utility_meter select_tariff: - description: selects the current tariff of an utility meter. + name: Select Tariff + description: Selects the current tariff of a utility meter. + target: + entity: + domain: utility_meter fields: - entity_id: - description: Name of the entity to set the tariff for - example: "utility_meter.energy" tariff: + name: Tariff description: Name of the tariff to switch to example: "offpeak" + required: true + selector: + text: calibrate: - description: calibrates an utility meter. + name: Calibrate + description: Calibrates a utility meter sensor. + target: + entity: + domain: sensor fields: - entity_id: - description: Name of the entity to calibrate - example: "utility_meter.energy" value: + name: Value description: Value to which set the meter example: "100" + required: true + selector: + text: diff --git a/homeassistant/components/vacuum/services.yaml b/homeassistant/components/vacuum/services.yaml index e0064bc475b..26c8d745b27 100644 --- a/homeassistant/components/vacuum/services.yaml +++ b/homeassistant/components/vacuum/services.yaml @@ -4,51 +4,71 @@ turn_on: name: Turn on description: Start a new cleaning task. target: + entity: + domain: vacuum turn_off: name: Turn off description: Stop the current cleaning task and return to home. target: + entity: + domain: vacuum stop: name: Stop description: Stop the current cleaning task. target: + entity: + domain: vacuum locate: name: Locate description: Locate the vacuum cleaner robot. target: + entity: + domain: vacuum start_pause: name: Start/Pause description: Start, pause, or resume the cleaning task. target: + entity: + domain: vacuum start: name: Start description: Start or resume the cleaning task. target: + entity: + domain: vacuum pause: name: Pause description: Pause the cleaning task. target: + entity: + domain: vacuum return_to_base: name: Return to base description: Tell the vacuum cleaner to return to its dock. target: + entity: + domain: vacuum clean_spot: name: Clean spot description: Tell the vacuum cleaner to do a spot clean-up. target: + entity: + domain: vacuum send_command: name: Send command description: Send a raw command to the vacuum cleaner. target: + entity: + domain: vacuum fields: command: name: Command @@ -68,6 +88,8 @@ set_fan_speed: name: Set fan speed description: Set the fan speed of the vacuum cleaner. target: + entity: + domain: vacuum fields: fan_speed: name: Fan speed diff --git a/homeassistant/components/vallox/services.yaml b/homeassistant/components/vallox/services.yaml index 65757b70364..98d7abac249 100644 --- a/homeassistant/components/vallox/services.yaml +++ b/homeassistant/components/vallox/services.yaml @@ -1,27 +1,57 @@ set_profile: + name: Set profile description: Set the ventilation profile. fields: profile: - description: "Set to any of: Home, Away, Boost, Fireplace" - example: Away + name: Profile + description: "Set profile." + required: true + selector: + select: + options: + - 'Away' + - 'Boost' + - 'Fireplace' + - 'Home' set_profile_fan_speed_home: + name: Set profile fan speed hom description: Set the fan speed of the Home profile. fields: fan_speed: - description: Fan speed in %. Integer, between 0 and 100. - example: 50 + name: Fan speed + description: Fan speed. + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' set_profile_fan_speed_away: + name: Set profile fan speed away description: Set the fan speed of the Away profile. fields: fan_speed: - description: Fan speed in %. Integer, between 0 and 100. - example: 25 + name: Fan speed + description: Fan speed. + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' set_profile_fan_speed_boost: + name: Set profile fan speed boost description: Set the fan speed of the Boost profile. fields: fan_speed: - description: Fan speed in %. Integer, between 0 and 100. - example: 65 + name: Fan speed + description: Fan speed. + required: true + selector: + number: + min: 0 + max: 100 + unit_of_measurement: '%' diff --git a/homeassistant/components/velbus/config_flow.py b/homeassistant/components/velbus/config_flow.py index 7d392ab37c5..a10d59bad4a 100644 --- a/homeassistant/components/velbus/config_flow.py +++ b/homeassistant/components/velbus/config_flow.py @@ -22,7 +22,6 @@ class VelbusConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self) -> None: """Initialize the velbus config flow.""" diff --git a/homeassistant/components/velbus/services.yaml b/homeassistant/components/velbus/services.yaml index 490c746fa74..9fed172fad4 100644 --- a/homeassistant/components/velbus/services.yaml +++ b/homeassistant/components/velbus/services.yaml @@ -1,18 +1,29 @@ sync_clock: + name: Sync clock description: Sync the velbus modules clock to the Home Assistant clock, this is the same as the 'sync clock' from VelbusLink set_memo_text: + name: Set memo text description: > Set the memo text to the display of modules like VMBGPO, VMBGPOD Be sure the page(s) of the module is configured to display the memo text. fields: address: + name: Address description: > The module address in decimal format. The decimal addresses are displayed in front of the modules listed at the integration page. - example: "11" + required: true + selector: + number: + min: 0 + max: 255 memo_text: + name: Memo text description: > The actual text to be displayed. Text is limited to 64 characters. example: "Do not forget trash" + default: '' + selector: + text: diff --git a/homeassistant/components/velux/services.yaml b/homeassistant/components/velux/services.yaml index 2460db0bbb0..46aee795890 100644 --- a/homeassistant/components/velux/services.yaml +++ b/homeassistant/components/velux/services.yaml @@ -1,4 +1,5 @@ # Velux Integration services reboot_gateway: + name: Reboot gateway description: Reboots the KLF200 Gateway. diff --git a/homeassistant/components/venstar/climate.py b/homeassistant/components/venstar/climate.py index b4d8264a3ab..72e9ecc3de4 100644 --- a/homeassistant/components/venstar/climate.py +++ b/homeassistant/components/venstar/climate.py @@ -124,7 +124,7 @@ class VenstarThermostat(ClimateEntity): if self._client.mode == self._client.MODE_AUTO: features |= SUPPORT_TARGET_TEMPERATURE_RANGE - if self._humidifier and hasattr(self._client, "hum_active"): + if self._humidifier and self._client.hum_setpoint is not None: features |= SUPPORT_TARGET_HUMIDITY return features diff --git a/homeassistant/components/venstar/manifest.json b/homeassistant/components/venstar/manifest.json index 0baa1e56cfa..444a3fabf9a 100644 --- a/homeassistant/components/venstar/manifest.json +++ b/homeassistant/components/venstar/manifest.json @@ -2,7 +2,7 @@ "domain": "venstar", "name": "Venstar", "documentation": "https://www.home-assistant.io/integrations/venstar", - "requirements": ["venstarcolortouch==0.13"], + "requirements": ["venstarcolortouch==0.14"], "codeowners": [], "iot_class": "local_polling" } diff --git a/homeassistant/components/vera/__init__.py b/homeassistant/components/vera/__init__.py index 9feba3cd08d..096c6a8aa15 100644 --- a/homeassistant/components/vera/__init__.py +++ b/homeassistant/components/vera/__init__.py @@ -159,6 +159,9 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, stop_subscription) ) + config_entry.async_on_unload( + config_entry.add_update_listener(_async_update_listener) + ) return True @@ -176,6 +179,11 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> return True +async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry): + """Handle options update.""" + await hass.config_entries.async_reload(entry.entry_id) + + def map_vera_device(vera_device: veraApi.VeraDevice, remap: list[int]) -> str: """Map vera classes to Home Assistant types.""" @@ -212,7 +220,9 @@ DeviceType = TypeVar("DeviceType", bound=veraApi.VeraDevice) class VeraDevice(Generic[DeviceType], Entity): """Representation of a Vera device entity.""" - def __init__(self, vera_device: DeviceType, controller_data: ControllerData): + def __init__( + self, vera_device: DeviceType, controller_data: ControllerData + ) -> None: """Initialize the device.""" self.vera_device = vera_device self.controller = controller_data.controller diff --git a/homeassistant/components/vera/binary_sensor.py b/homeassistant/components/vera/binary_sensor.py index 816234bb602..f41ce5cfd08 100644 --- a/homeassistant/components/vera/binary_sensor.py +++ b/homeassistant/components/vera/binary_sensor.py @@ -1,8 +1,6 @@ """Support for Vera binary sensors.""" from __future__ import annotations -from typing import Callable - import pyvera as veraApi from homeassistant.components.binary_sensor import ( @@ -12,7 +10,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VeraDevice from .common import ControllerData, get_controller_data @@ -21,7 +19,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -39,7 +37,7 @@ class VeraBinarySensor(VeraDevice[veraApi.VeraBinarySensor], BinarySensorEntity) def __init__( self, vera_device: veraApi.VeraBinarySensor, controller_data: ControllerData - ): + ) -> None: """Initialize the binary_sensor.""" self._state = False VeraDevice.__init__(self, vera_device, controller_data) diff --git a/homeassistant/components/vera/climate.py b/homeassistant/components/vera/climate.py index 5027becb71f..cde36dcc623 100644 --- a/homeassistant/components/vera/climate.py +++ b/homeassistant/components/vera/climate.py @@ -1,7 +1,7 @@ """Support for Vera thermostats.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -23,7 +23,7 @@ from homeassistant.components.climate.const import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_TEMPERATURE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from . import VeraDevice @@ -38,7 +38,7 @@ SUPPORT_HVAC = [HVAC_MODE_COOL, HVAC_MODE_HEAT, HVAC_MODE_HEAT_COOL, HVAC_MODE_O async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -56,7 +56,7 @@ class VeraThermostat(VeraDevice[veraApi.VeraThermostat], ClimateEntity): def __init__( self, vera_device: veraApi.VeraThermostat, controller_data: ControllerData - ): + ) -> None: """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) diff --git a/homeassistant/components/vera/config_flow.py b/homeassistant/components/vera/config_flow.py index a5450cd4a65..ae4fdf8cefa 100644 --- a/homeassistant/components/vera/config_flow.py +++ b/homeassistant/components/vera/config_flow.py @@ -67,7 +67,7 @@ def options_data(user_input: dict) -> dict: class OptionsFlowHandler(config_entries.OptionsFlow): """Options for the component.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Init object.""" self.config_entry = config_entry diff --git a/homeassistant/components/vera/cover.py b/homeassistant/components/vera/cover.py index cf3dd4a3d13..62a04d4e6f3 100644 --- a/homeassistant/components/vera/cover.py +++ b/homeassistant/components/vera/cover.py @@ -1,7 +1,7 @@ """Support for Vera cover - curtains, rollershutters etc.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -13,7 +13,7 @@ from homeassistant.components.cover import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VeraDevice from .common import ControllerData, get_controller_data @@ -22,7 +22,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -40,7 +40,7 @@ class VeraCover(VeraDevice[veraApi.VeraCurtain], CoverEntity): def __init__( self, vera_device: veraApi.VeraCurtain, controller_data: ControllerData - ): + ) -> None: """Initialize the Vera device.""" VeraDevice.__init__(self, vera_device, controller_data) self.entity_id = ENTITY_ID_FORMAT.format(self.vera_id) diff --git a/homeassistant/components/vera/light.py b/homeassistant/components/vera/light.py index 7fcb726efcc..7f67f065c91 100644 --- a/homeassistant/components/vera/light.py +++ b/homeassistant/components/vera/light.py @@ -1,7 +1,7 @@ """Support for Vera lights.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -16,7 +16,7 @@ from homeassistant.components.light import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from . import VeraDevice @@ -26,7 +26,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -44,7 +44,7 @@ class VeraLight(VeraDevice[veraApi.VeraDimmer], LightEntity): def __init__( self, vera_device: veraApi.VeraDimmer, controller_data: ControllerData - ): + ) -> None: """Initialize the light.""" self._state = False self._color = None diff --git a/homeassistant/components/vera/lock.py b/homeassistant/components/vera/lock.py index eada4b20550..b99728d0fcf 100644 --- a/homeassistant/components/vera/lock.py +++ b/homeassistant/components/vera/lock.py @@ -1,7 +1,7 @@ """Support for Vera locks.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -13,7 +13,7 @@ from homeassistant.components.lock import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from . import VeraDevice from .common import ControllerData, get_controller_data @@ -25,7 +25,7 @@ ATTR_LOW_BATTERY = "low_battery" async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -41,7 +41,9 @@ async def async_setup_entry( class VeraLock(VeraDevice[veraApi.VeraLock], LockEntity): """Representation of a Vera lock.""" - def __init__(self, vera_device: veraApi.VeraLock, controller_data: ControllerData): + def __init__( + self, vera_device: veraApi.VeraLock, controller_data: ControllerData + ) -> None: """Initialize the Vera device.""" self._state = None VeraDevice.__init__(self, vera_device, controller_data) diff --git a/homeassistant/components/vera/scene.py b/homeassistant/components/vera/scene.py index c6eb983a8f7..c1381f488dd 100644 --- a/homeassistant/components/vera/scene.py +++ b/homeassistant/components/vera/scene.py @@ -1,14 +1,14 @@ """Support for Vera scenes.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi from homeassistant.components.scene import Scene from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import slugify from .common import ControllerData, get_controller_data @@ -18,7 +18,7 @@ from .const import VERA_ID_FORMAT async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -30,7 +30,9 @@ async def async_setup_entry( class VeraScene(Scene): """Representation of a Vera scene entity.""" - def __init__(self, vera_scene: veraApi.VeraScene, controller_data: ControllerData): + def __init__( + self, vera_scene: veraApi.VeraScene, controller_data: ControllerData + ) -> None: """Initialize the scene.""" self.vera_scene = vera_scene self.controller = controller_data.controller diff --git a/homeassistant/components/vera/sensor.py b/homeassistant/components/vera/sensor.py index 516801b57c6..878f6ff376d 100644 --- a/homeassistant/components/vera/sensor.py +++ b/homeassistant/components/vera/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Callable, cast +from typing import cast import pyvera as veraApi @@ -14,7 +14,7 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import LIGHT_LUX, PERCENTAGE, TEMP_CELSIUS, TEMP_FAHRENHEIT from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from . import VeraDevice @@ -26,7 +26,7 @@ SCAN_INTERVAL = timedelta(seconds=5) async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -44,7 +44,7 @@ class VeraSensor(VeraDevice[veraApi.VeraSensor], SensorEntity): def __init__( self, vera_device: veraApi.VeraSensor, controller_data: ControllerData - ): + ) -> None: """Initialize the sensor.""" self.current_value = None self._temperature_units = None diff --git a/homeassistant/components/vera/switch.py b/homeassistant/components/vera/switch.py index c779a3c8cfc..304441037ec 100644 --- a/homeassistant/components/vera/switch.py +++ b/homeassistant/components/vera/switch.py @@ -1,7 +1,7 @@ """Support for Vera switches.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any import pyvera as veraApi @@ -12,7 +12,7 @@ from homeassistant.components.switch import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util import convert from . import VeraDevice @@ -22,7 +22,7 @@ from .common import ControllerData, get_controller_data async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" controller_data = get_controller_data(hass, entry) @@ -40,7 +40,7 @@ class VeraSwitch(VeraDevice[veraApi.VeraSwitch], SwitchEntity): def __init__( self, vera_device: veraApi.VeraSwitch, controller_data: ControllerData - ): + ) -> None: """Initialize the Vera device.""" self._state = False VeraDevice.__init__(self, vera_device, controller_data) diff --git a/homeassistant/components/vera/translations/ru.json b/homeassistant/components/vera/translations/ru.json index 4f050ef3880..857ffaa5bf5 100644 --- a/homeassistant/components/vera/translations/ru.json +++ b/homeassistant/components/vera/translations/ru.json @@ -22,7 +22,7 @@ "exclude": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u0438\u0441\u043a\u043b\u044e\u0447\u0435\u043d\u0438\u044f \u0438\u0437 Home Assistant.", "lights": "\u0418\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0442\u043e\u0440\u044b \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432 Vera \u0434\u043b\u044f \u043f\u0435\u0440\u0435\u043d\u0430\u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f \u0438\u0437 \u0432\u044b\u043a\u043b\u044e\u0447\u0430\u0442\u0435\u043b\u044f \u0432 \u043e\u0441\u0432\u0435\u0449\u0435\u043d\u0438\u0435 \u0432 Home Assistant." }, - "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u043b\u044e\u0431\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0440\u043e\u0431\u0435\u043b.", + "description": "\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439 \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0438\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u0440\u0430\u043c\u0435\u0442\u0440\u0430\u0445: https://www.home-assistant.io/integrations/vera/.\n\u0414\u043b\u044f \u0432\u043d\u0435\u0441\u0435\u043d\u0438\u044f \u043b\u044e\u0431\u044b\u0445 \u0438\u0437\u043c\u0435\u043d\u0435\u043d\u0438\u0439 \u043f\u043e\u0442\u0440\u0435\u0431\u0443\u0435\u0442\u0441\u044f \u043f\u0435\u0440\u0435\u0437\u0430\u043f\u0443\u0441\u043a \u0441\u0435\u0440\u0432\u0435\u0440\u0430 Home Assistant. \u0427\u0442\u043e\u0431\u044b \u043e\u0447\u0438\u0441\u0442\u0438\u0442\u044c \u0437\u043d\u0430\u0447\u0435\u043d\u0438\u044f, \u043f\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0440\u043e\u0431\u0435\u043b.", "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438 \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u043b\u0435\u0440\u0430 Vera" } } diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index f61208309fc..8abb3e59a9f 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -3,9 +3,6 @@ from __future__ import annotations from contextlib import suppress import os -from typing import Any - -import voluptuous as vol from homeassistant.components.alarm_control_panel import ( DOMAIN as ALARM_CONTROL_PANEL_DOMAIN, @@ -15,27 +12,14 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry -from homeassistant.const import ( - CONF_EMAIL, - CONF_PASSWORD, - CONF_USERNAME, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR -from .const import ( - CONF_CODE_DIGITS, - CONF_DEFAULT_LOCK_CODE, - CONF_GIID, - CONF_LOCK_CODE_DIGITS, - CONF_LOCK_DEFAULT_CODE, - DEFAULT_LOCK_CODE_DIGITS, - DOMAIN, -) +from .const import DOMAIN from .coordinator import VerisureDataUpdateCoordinator PLATFORMS = [ @@ -47,80 +31,11 @@ PLATFORMS = [ SWITCH_DOMAIN, ] -CONFIG_SCHEMA = vol.Schema( - vol.All( - cv.deprecated(DOMAIN), - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_USERNAME): cv.string, - vol.Optional(CONF_CODE_DIGITS): cv.positive_int, - vol.Optional(CONF_GIID): cv.string, - vol.Optional(CONF_DEFAULT_LOCK_CODE): cv.string, - }, - extra=vol.ALLOW_EXTRA, - ) - }, - ), - extra=vol.ALLOW_EXTRA, -) - - -async def async_setup(hass: HomeAssistant, config: dict[str, Any]) -> bool: - """Set up the Verisure integration.""" - if DOMAIN in config: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_IMPORT}, - data={ - CONF_EMAIL: config[DOMAIN][CONF_USERNAME], - CONF_PASSWORD: config[DOMAIN][CONF_PASSWORD], - CONF_GIID: config[DOMAIN].get(CONF_GIID), - CONF_LOCK_CODE_DIGITS: config[DOMAIN].get(CONF_CODE_DIGITS), - CONF_LOCK_DEFAULT_CODE: config[DOMAIN].get(CONF_LOCK_DEFAULT_CODE), - }, - ) - ) - - return True +CONFIG_SCHEMA = cv.deprecated(DOMAIN) async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Set up Verisure from a config entry.""" - # Migrate old YAML settings (hidden in the config entry), - # to config entry options. Can be removed after YAML support is gone. - if CONF_LOCK_CODE_DIGITS in entry.data or CONF_DEFAULT_LOCK_CODE in entry.data: - options = entry.options.copy() - - if ( - CONF_LOCK_CODE_DIGITS in entry.data - and CONF_LOCK_CODE_DIGITS not in entry.options - and entry.data[CONF_LOCK_CODE_DIGITS] != DEFAULT_LOCK_CODE_DIGITS - ): - options.update( - { - CONF_LOCK_CODE_DIGITS: entry.data[CONF_LOCK_CODE_DIGITS], - } - ) - - if ( - CONF_DEFAULT_LOCK_CODE in entry.data - and CONF_DEFAULT_LOCK_CODE not in entry.options - ): - options.update( - { - CONF_DEFAULT_LOCK_CODE: entry.data[CONF_DEFAULT_LOCK_CODE], - } - ) - - data = entry.data.copy() - data.pop(CONF_LOCK_CODE_DIGITS, None) - data.pop(CONF_DEFAULT_LOCK_CODE, None) - hass.config_entries.async_update_entry(entry, data=data, options=options) - - # Continue as normal... coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): diff --git a/homeassistant/components/verisure/alarm_control_panel.py b/homeassistant/components/verisure/alarm_control_panel.py index b84affe9e8d..4def470ac5e 100644 --- a/homeassistant/components/verisure/alarm_control_panel.py +++ b/homeassistant/components/verisure/alarm_control_panel.py @@ -2,8 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable -from typing import Any, Callable from homeassistant.components.alarm_control_panel import ( FORMAT_NUMBER, @@ -15,7 +13,8 @@ from homeassistant.components.alarm_control_panel.const import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ALARM_STATE_TO_HA, CONF_GIID, DOMAIN, LOGGER @@ -25,7 +24,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure alarm control panel from a config entry.""" async_add_entities([VerisureAlarm(coordinator=hass.data[DOMAIN][entry.entry_id])]) @@ -36,21 +35,11 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): coordinator: VerisureDataUpdateCoordinator + _attr_name = "Verisure Alarm" _changed_by: str | None = None - _state: str | None = None @property - def name(self) -> str: - """Return the name of the entity.""" - return "Verisure Alarm" - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.coordinator.entry.data[CONF_GIID] - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" return { "name": "Verisure Alarm", @@ -59,16 +48,16 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): "identifiers": {(DOMAIN, self.coordinator.entry.data[CONF_GIID])}, } - @property - def state(self) -> str | None: - """Return the state of the entity.""" - return self._state - @property def supported_features(self) -> int: """Return the list of supported features.""" return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY + @property + def unique_id(self) -> str: + """Return the unique ID for this entity.""" + return self.coordinator.entry.data[CONF_GIID] + @property def code_format(self) -> str: """Return one or more digits/characters.""" @@ -110,7 +99,7 @@ class VerisureAlarm(CoordinatorEntity, AlarmControlPanelEntity): @callback def _handle_coordinator_update(self) -> None: """Handle updated data from the coordinator.""" - self._state = ALARM_STATE_TO_HA.get( + self._attr_state = ALARM_STATE_TO_HA.get( self.coordinator.data["alarm"]["statusType"] ) self._changed_by = self.coordinator.data["alarm"].get("name") diff --git a/homeassistant/components/verisure/binary_sensor.py b/homeassistant/components/verisure/binary_sensor.py index 758636bee98..4d9b1e84770 100644 --- a/homeassistant/components/verisure/binary_sensor.py +++ b/homeassistant/components/verisure/binary_sensor.py @@ -1,9 +1,6 @@ """Support for Verisure binary sensors.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_CONNECTIVITY, DEVICE_CLASS_OPENING, @@ -11,7 +8,8 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN @@ -21,7 +19,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure binary sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -41,25 +39,19 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): coordinator: VerisureDataUpdateCoordinator + _attr_device_class = DEVICE_CLASS_OPENING + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the Verisure door window sensor.""" super().__init__(coordinator) + self._attr_name = coordinator.data["door_window"][serial_number]["area"] + self._attr_unique_id = f"{serial_number}_door_window" self.serial_number = serial_number @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["door_window"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_door_window" - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["door_window"][self.serial_number]["area"] return { @@ -71,11 +63,6 @@ class VerisureDoorWindowSensor(CoordinatorEntity, BinarySensorEntity): "via_device": (DOMAIN, self.coordinator.entry.data[CONF_GIID]), } - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_OPENING - @property def is_on(self) -> bool: """Return the state of the sensor.""" @@ -97,10 +84,8 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): coordinator: VerisureDataUpdateCoordinator - @property - def name(self) -> str: - """Return the name of this entity.""" - return "Verisure Ethernet status" + _attr_name = "Verisure Ethernet status" + _attr_device_class = DEVICE_CLASS_CONNECTIVITY @property def unique_id(self) -> str: @@ -108,7 +93,7 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): return f"{self.coordinator.entry.data[CONF_GIID]}_ethernet" @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" return { "name": "Verisure Alarm", @@ -126,8 +111,3 @@ class VerisureEthernetStatus(CoordinatorEntity, BinarySensorEntity): def available(self) -> bool: """Return True if entity is available.""" return super().available and self.coordinator.data["ethernet"] is not None - - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_CONNECTIVITY diff --git a/homeassistant/components/verisure/camera.py b/homeassistant/components/verisure/camera.py index a4442d2ae4b..a137f61d98f 100644 --- a/homeassistant/components/verisure/camera.py +++ b/homeassistant/components/verisure/camera.py @@ -1,10 +1,8 @@ """Support for Verisure cameras.""" from __future__ import annotations -from collections.abc import Iterable import errno import os -from typing import Any, Callable from verisure import Error as VerisureError @@ -12,8 +10,11 @@ from homeassistant.components.camera import Camera from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_platform import current_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN, LOGGER, SERVICE_CAPTURE_SMARTCAM @@ -23,12 +24,12 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - platform = current_platform.get() + platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_CAPTURE_SMARTCAM, {}, @@ -52,28 +53,21 @@ class VerisureSmartcam(CoordinatorEntity, Camera): coordinator: VerisureDataUpdateCoordinator, serial_number: str, directory_path: str, - ): + ) -> None: """Initialize Verisure File Camera component.""" super().__init__(coordinator) Camera.__init__(self) + self._attr_name = coordinator.data["cameras"][serial_number]["area"] + self._attr_unique_id = serial_number + self.serial_number = serial_number self._directory_path = directory_path self._image = None self._image_id = None @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["cameras"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.serial_number - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["cameras"][self.serial_number]["area"] return { diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 2c472212bda..6c2822896e6 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -11,12 +11,7 @@ from verisure import ( ) import voluptuous as vol -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_POLL, - ConfigEntry, - ConfigFlow, - OptionsFlow, -) +from homeassistant.config_entries import ConfigEntry, ConfigFlow, OptionsFlow from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import callback from homeassistant.data_entry_flow import FlowResult @@ -35,21 +30,12 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow for Verisure.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL email: str entry: ConfigEntry installations: dict[str, str] password: str - # These can be removed after YAML import has been removed. - giid: str | None = None - settings: dict[str, int | str] - - def __init__(self): - """Initialize.""" - self.settings = {} - @staticmethod @callback def async_get_options_flow(config_entry: ConfigEntry) -> VerisureOptionsFlowHandler: @@ -101,8 +87,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): """Select Verisure installation to add.""" if len(self.installations) == 1: user_input = {CONF_GIID: list(self.installations)[0]} - elif self.giid and self.giid in self.installations: - user_input = {CONF_GIID: self.giid} if user_input is None: return self.async_show_form( @@ -121,7 +105,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): CONF_EMAIL: self.email, CONF_PASSWORD: self.password, CONF_GIID: user_input[CONF_GIID], - **self.settings, }, ) @@ -174,26 +157,6 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_import(self, user_input: dict[str, Any]) -> FlowResult: - """Import Verisure YAML configuration.""" - if user_input[CONF_GIID]: - self.giid = user_input[CONF_GIID] - await self.async_set_unique_id(self.giid) - self._abort_if_unique_id_configured() - else: - # The old YAML configuration could handle 1 single Verisure instance. - # Therefore, if we don't know the GIID, we can use the discovery - # without a unique ID logic, to prevent re-import/discovery. - await self._async_handle_discovery_without_unique_id() - - # Settings, later to be converted to config entry options - if user_input[CONF_LOCK_CODE_DIGITS]: - self.settings[CONF_LOCK_CODE_DIGITS] = user_input[CONF_LOCK_CODE_DIGITS] - if user_input[CONF_LOCK_DEFAULT_CODE]: - self.settings[CONF_LOCK_DEFAULT_CODE] = user_input[CONF_LOCK_DEFAULT_CODE] - - return await self.async_step_user(user_input) - class VerisureOptionsFlowHandler(OptionsFlow): """Handle Verisure options.""" diff --git a/homeassistant/components/verisure/const.py b/homeassistant/components/verisure/const.py index 030c5a58075..e8720baa1d5 100644 --- a/homeassistant/components/verisure/const.py +++ b/homeassistant/components/verisure/const.py @@ -44,7 +44,3 @@ ALARM_STATE_TO_HA = { "ARMED_AWAY": STATE_ALARM_ARMED_AWAY, "PENDING": STATE_ALARM_PENDING, } - -# Legacy; to remove after YAML removal -CONF_CODE_DIGITS = "code_digits" -CONF_DEFAULT_LOCK_CODE = "default_lock_code" diff --git a/homeassistant/components/verisure/lock.py b/homeassistant/components/verisure/lock.py index bcd5ac214ee..e645bf3f8c1 100644 --- a/homeassistant/components/verisure/lock.py +++ b/homeassistant/components/verisure/lock.py @@ -2,8 +2,6 @@ from __future__ import annotations import asyncio -from collections.abc import Iterable -from typing import Any, Callable from verisure import Error as VerisureError @@ -11,8 +9,11 @@ from homeassistant.components.lock import LockEntity from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CODE, STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity -from homeassistant.helpers.entity_platform import current_platform +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import ( + AddEntitiesCallback, + async_get_current_platform, +) from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import ( @@ -31,12 +32,12 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure alarm control panel from a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - platform = current_platform.get() + platform = async_get_current_platform() platform.async_register_entity_service( SERVICE_DISABLE_AUTOLOCK, {}, @@ -64,6 +65,10 @@ class VerisureDoorlock(CoordinatorEntity, LockEntity): ) -> None: """Initialize the Verisure lock.""" super().__init__(coordinator) + + self._attr_name = coordinator.data["locks"][serial_number]["area"] + self._attr_unique_id = serial_number + self.serial_number = serial_number self._state = None self._digits = coordinator.entry.options.get( @@ -71,17 +76,7 @@ class VerisureDoorlock(CoordinatorEntity, LockEntity): ) @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["locks"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.serial_number - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["locks"][self.serial_number]["area"] return { diff --git a/homeassistant/components/verisure/sensor.py b/homeassistant/components/verisure/sensor.py index 72b061bd628..d39c235e9d5 100644 --- a/homeassistant/components/verisure/sensor.py +++ b/homeassistant/components/verisure/sensor.py @@ -1,9 +1,6 @@ """Support for Verisure sensors.""" from __future__ import annotations -from collections.abc import Iterable -from typing import Any, Callable - from homeassistant.components.sensor import ( DEVICE_CLASS_HUMIDITY, DEVICE_CLASS_TEMPERATURE, @@ -12,7 +9,8 @@ from homeassistant.components.sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import PERCENTAGE, TEMP_CELSIUS from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DEVICE_TYPE_NAME, DOMAIN @@ -22,7 +20,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure sensors based on a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -52,11 +50,15 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): coordinator: VerisureDataUpdateCoordinator + _attr_device_class = DEVICE_CLASS_TEMPERATURE + _attr_unit_of_measurement = TEMP_CELSIUS + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{serial_number}_temperature" self.serial_number = serial_number @property @@ -66,17 +68,7 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): return f"{name} Temperature" @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_temperature" - - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_TEMPERATURE - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" device_type = self.coordinator.data["climate"][self.serial_number].get( "deviceType" @@ -105,22 +97,21 @@ class VerisureThermometer(CoordinatorEntity, SensorEntity): and "temperature" in self.coordinator.data["climate"][self.serial_number] ) - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return TEMP_CELSIUS - class VerisureHygrometer(CoordinatorEntity, SensorEntity): """Representation of a Verisure hygrometer.""" coordinator: VerisureDataUpdateCoordinator + _attr_device_class = DEVICE_CLASS_HUMIDITY + _attr_unit_of_measurement = PERCENTAGE + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{serial_number}_humidity" self.serial_number = serial_number @property @@ -130,17 +121,7 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): return f"{name} Humidity" @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_humidity" - - @property - def device_class(self) -> str: - """Return the class of this entity.""" - return DEVICE_CLASS_HUMIDITY - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" device_type = self.coordinator.data["climate"][self.serial_number].get( "deviceType" @@ -169,22 +150,20 @@ class VerisureHygrometer(CoordinatorEntity, SensorEntity): and "humidity" in self.coordinator.data["climate"][self.serial_number] ) - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return PERCENTAGE - class VerisureMouseDetection(CoordinatorEntity, SensorEntity): """Representation of a Verisure mouse detector.""" coordinator: VerisureDataUpdateCoordinator + _attr_unit_of_measurement = "Mice" + def __init__( self, coordinator: VerisureDataUpdateCoordinator, serial_number: str ) -> None: """Initialize the sensor.""" super().__init__(coordinator) + self._attr_unique_id = f"{serial_number}_mice" self.serial_number = serial_number @property @@ -194,12 +173,7 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): return f"{name} Mouse" @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return f"{self.serial_number}_mice" - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["mice"][self.serial_number]["area"] return { @@ -224,8 +198,3 @@ class VerisureMouseDetection(CoordinatorEntity, SensorEntity): and self.serial_number in self.coordinator.data["mice"] and "detections" in self.coordinator.data["mice"][self.serial_number] ) - - @property - def unit_of_measurement(self) -> str: - """Return the unit of measurement of this entity.""" - return "Mice" diff --git a/homeassistant/components/verisure/switch.py b/homeassistant/components/verisure/switch.py index 1284ed5fde4..f428f70cda6 100644 --- a/homeassistant/components/verisure/switch.py +++ b/homeassistant/components/verisure/switch.py @@ -1,14 +1,13 @@ """Support for Verisure Smartplugs.""" from __future__ import annotations -from collections.abc import Iterable from time import monotonic -from typing import Any, Callable from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import CoordinatorEntity from .const import CONF_GIID, DOMAIN @@ -18,7 +17,7 @@ from .coordinator import VerisureDataUpdateCoordinator async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[Iterable[Entity]], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Verisure alarm control panel from a config entry.""" coordinator: VerisureDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] @@ -38,22 +37,16 @@ class VerisureSmartplug(CoordinatorEntity, SwitchEntity): ) -> None: """Initialize the Verisure device.""" super().__init__(coordinator) + + self._attr_name = coordinator.data["smart_plugs"][serial_number]["area"] + self._attr_unique_id = serial_number + self.serial_number = serial_number self._change_timestamp = 0 self._state = False @property - def name(self) -> str: - """Return the name of this entity.""" - return self.coordinator.data["smart_plugs"][self.serial_number]["area"] - - @property - def unique_id(self) -> str: - """Return the unique ID for this entity.""" - return self.serial_number - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this entity.""" area = self.coordinator.data["smart_plugs"][self.serial_number]["area"] return { diff --git a/homeassistant/components/version/sensor.py b/homeassistant/components/version/sensor.py index d438f391334..04165ec9db1 100644 --- a/homeassistant/components/version/sensor.py +++ b/homeassistant/components/version/sensor.py @@ -111,7 +111,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class VersionData: """Get the latest data and update the states.""" - def __init__(self, api: HaVersion): + def __init__(self, api: HaVersion) -> None: """Initialize the data object.""" self.api = api @@ -131,7 +131,7 @@ class VersionData: class VersionSensor(SensorEntity): """Representation of a Home Assistant version sensor.""" - def __init__(self, data: VersionData, name: str): + def __init__(self, data: VersionData, name: str) -> None: """Initialize the Version sensor.""" self.data = data self._name = name diff --git a/homeassistant/components/vesync/__init__.py b/homeassistant/components/vesync/__init__.py index 6ae978eb4b8..3a17af55c93 100644 --- a/homeassistant/components/vesync/__init__.py +++ b/homeassistant/components/vesync/__init__.py @@ -10,7 +10,6 @@ from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from .common import async_process_devices -from .config_flow import configured_instances from .const import ( DOMAIN, SERVICE_UPDATE_DEVS, @@ -27,14 +26,17 @@ PLATFORMS = ["switch", "fan", "light"] _LOGGER = logging.getLogger(__name__) CONFIG_SCHEMA = vol.Schema( - { - DOMAIN: vol.Schema( - { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - } - ) - }, + vol.All( + cv.deprecated(DOMAIN), + { + DOMAIN: vol.Schema( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_PASSWORD): cv.string, + } + ) + }, + ), extra=vol.ALLOW_EXTRA, ) @@ -46,7 +48,7 @@ async def async_setup(hass, config): if conf is None: return True - if not configured_instances(hass): + if not hass.config_entries.async_entries(DOMAIN): hass.async_create_task( hass.config_entries.flow.async_init( DOMAIN, diff --git a/homeassistant/components/vesync/common.py b/homeassistant/components/vesync/common.py index fcab5bb5a63..d51da7a375b 100644 --- a/homeassistant/components/vesync/common.py +++ b/homeassistant/components/vesync/common.py @@ -21,6 +21,10 @@ async def async_process_devices(hass, manager): devices[VS_FANS].extend(manager.fans) _LOGGER.info("%d VeSync fans found", len(manager.fans)) + if manager.bulbs: + devices[VS_LIGHTS].extend(manager.bulbs) + _LOGGER.info("%d VeSync lights found", len(manager.bulbs)) + if manager.outlets: devices[VS_SWITCHES].extend(manager.outlets) _LOGGER.info("%d VeSync outlets found", len(manager.outlets)) diff --git a/homeassistant/components/vesync/config_flow.py b/homeassistant/components/vesync/config_flow.py index f930619d00b..f91848c5238 100644 --- a/homeassistant/components/vesync/config_flow.py +++ b/homeassistant/components/vesync/config_flow.py @@ -11,18 +11,10 @@ from homeassistant.core import callback from .const import DOMAIN -@callback -def configured_instances(hass): - """Return already configured instances.""" - return hass.config_entries.async_entries(DOMAIN) - - -@config_entries.HANDLERS.register(DOMAIN) -class VeSyncFlowHandler(config_entries.ConfigFlow): +class VeSyncFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Instantiate config flow.""" @@ -47,7 +39,7 @@ class VeSyncFlowHandler(config_entries.ConfigFlow): async def async_step_user(self, user_input=None): """Handle a flow start.""" - if configured_instances(self.hass): + if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") if not user_input: diff --git a/homeassistant/components/vesync/fan.py b/homeassistant/components/vesync/fan.py index d01d3d4dc5d..6641f43d17b 100644 --- a/homeassistant/components/vesync/fan.py +++ b/homeassistant/components/vesync/fan.py @@ -18,12 +18,16 @@ _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { "LV-PUR131S": "fan", + "Core200S": "fan", } FAN_MODE_AUTO = "auto" FAN_MODE_SLEEP = "sleep" -PRESET_MODES = [FAN_MODE_AUTO, FAN_MODE_SLEEP] +PRESET_MODES = { + "LV-PUR131S": [FAN_MODE_AUTO, FAN_MODE_SLEEP], + "Core200S": [FAN_MODE_SLEEP], +} SPEED_RANGE = (1, 3) # off is not included @@ -86,7 +90,7 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def preset_modes(self): """Get the list of available preset modes.""" - return PRESET_MODES + return PRESET_MODES[self.device.device_type] @property def preset_mode(self): @@ -103,13 +107,30 @@ class VeSyncFanHA(VeSyncDevice, FanEntity): @property def extra_state_attributes(self): """Return the state attributes of the fan.""" - return { - "mode": self.smartfan.mode, - "active_time": self.smartfan.active_time, - "filter_life": self.smartfan.filter_life, - "air_quality": self.smartfan.air_quality, - "screen_status": self.smartfan.screen_status, - } + attr = {} + + if hasattr(self.smartfan, "active_time"): + attr["active_time"] = self.smartfan.active_time + + if hasattr(self.smartfan, "screen_status"): + attr["screen_status"] = self.smartfan.screen_status + + if hasattr(self.smartfan, "child_lock"): + attr["child_lock"] = self.smartfan.child_lock + + if hasattr(self.smartfan, "night_light"): + attr["night_light"] = self.smartfan.night_light + + if hasattr(self.smartfan, "air_quality"): + attr["air_quality"] = self.smartfan.air_quality + + if hasattr(self.smartfan, "mode"): + attr["mode"] = self.smartfan.mode + + if hasattr(self.smartfan, "filter_life"): + attr["filter_life"] = self.smartfan.filter_life + + return attr def set_percentage(self, percentage): """Set the speed of the device.""" diff --git a/homeassistant/components/vesync/light.py b/homeassistant/components/vesync/light.py index b98c87e5a7f..b747c10ee4e 100644 --- a/homeassistant/components/vesync/light.py +++ b/homeassistant/components/vesync/light.py @@ -1,9 +1,11 @@ -"""Support for VeSync dimmers.""" +"""Support for VeSync bulbs and wall dimmers.""" import logging from homeassistant.components.light import ( ATTR_BRIGHTNESS, - SUPPORT_BRIGHTNESS, + ATTR_COLOR_TEMP, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, LightEntity, ) from homeassistant.core import callback @@ -15,8 +17,10 @@ from .const import DOMAIN, VS_DISCOVERY, VS_DISPATCHERS, VS_LIGHTS _LOGGER = logging.getLogger(__name__) DEV_TYPE_TO_HA = { - "ESD16": "light", - "ESWD16": "light", + "ESD16": "walldimmer", + "ESWD16": "walldimmer", + "ESL100": "bulb-dimmable", + "ESL100CW": "bulb-tunable-white", } @@ -40,8 +44,10 @@ def _async_setup_entities(devices, async_add_entities): """Check if device is online and add entity.""" entities = [] for dev in devices: - if DEV_TYPE_TO_HA.get(dev.device_type) == "light": - entities.append(VeSyncDimmerHA(dev)) + if DEV_TYPE_TO_HA.get(dev.device_type) in ("walldimmer", "bulb-dimmable"): + entities.append(VeSyncDimmableLightHA(dev)) + elif DEV_TYPE_TO_HA.get(dev.device_type) in ("bulb-tunable-white"): + entities.append(VeSyncTunableWhiteLightHA(dev)) else: _LOGGER.debug( "%s - Unknown device type - %s", dev.device_name, dev.device_type @@ -51,34 +57,133 @@ def _async_setup_entities(devices, async_add_entities): async_add_entities(entities, update_before_add=True) -class VeSyncDimmerHA(VeSyncDevice, LightEntity): - """Representation of a VeSync dimmer.""" - - def __init__(self, dimmer): - """Initialize the VeSync dimmer device.""" - super().__init__(dimmer) - self.dimmer = dimmer - - def turn_on(self, **kwargs): - """Turn the device on.""" - if ATTR_BRIGHTNESS in kwargs: - # get brightness from HA data - brightness = int(kwargs[ATTR_BRIGHTNESS]) - # convert to percent that vesync api expects - brightness = round((brightness / 255) * 100) - # clamp to 1-100 - brightness = max(1, min(brightness, 100)) - self.dimmer.set_brightness(brightness) - # Avoid turning device back on if this is just a brightness adjustment - if not self.is_on: - self.device.turn_on() - - @property - def supported_features(self): - """Get supported features for this entity.""" - return SUPPORT_BRIGHTNESS +class VeSyncBaseLight(VeSyncDevice, LightEntity): + """Base class for VeSync Light Devices Representations.""" @property def brightness(self): - """Get dimmer brightness.""" - return round((int(self.dimmer.brightness) / 100) * 255) + """Get light brightness.""" + # get value from pyvesync library api, + result = self.device.brightness + try: + # check for validity of brightness value received + brightness_value = int(result) + except ValueError: + # deal if any unexpected/non numeric value + _LOGGER.debug( + "VeSync - received unexpected 'brightness' value from pyvesync api: %s", + result, + ) + return 0 + # convert percent brightness to ha expected range + return round((max(1, brightness_value) / 100) * 255) + + def turn_on(self, **kwargs): + """Turn the device on.""" + attribute_adjustment_only = False + # set white temperature + if self.color_mode in (COLOR_MODE_COLOR_TEMP) and ATTR_COLOR_TEMP in kwargs: + # get white temperature from HA data + color_temp = int(kwargs[ATTR_COLOR_TEMP]) + # ensure value between min-max supported Mireds + color_temp = max(self.min_mireds, min(color_temp, self.max_mireds)) + # convert Mireds to Percent value that api expects + color_temp = round( + ((color_temp - self.min_mireds) / (self.max_mireds - self.min_mireds)) + * 100 + ) + # flip cold/warm to what pyvesync api expects + color_temp = 100 - color_temp + # ensure value between 0-100 + color_temp = max(0, min(color_temp, 100)) + # call pyvesync library api method to set color_temp + self.device.set_color_temp(color_temp) + # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly + attribute_adjustment_only = True + # set brightness level + if ( + self.color_mode in (COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP) + and ATTR_BRIGHTNESS in kwargs + ): + # get brightness from HA data + brightness = int(kwargs[ATTR_BRIGHTNESS]) + # ensure value between 1-255 + brightness = max(1, min(brightness, 255)) + # convert to percent that vesync api expects + brightness = round((brightness / 255) * 100) + # ensure value between 1-100 + brightness = max(1, min(brightness, 100)) + # call pyvesync library api method to set brightness + self.device.set_brightness(brightness) + # flag attribute_adjustment_only, so it doesn't turn_on the device redundantly + attribute_adjustment_only = True + # check flag if should skip sending the turn_on command + if attribute_adjustment_only: + return + # send turn_on command to pyvesync api + self.device.turn_on() + + +class VeSyncDimmableLightHA(VeSyncBaseLight, LightEntity): + """Representation of a VeSync dimmable light device.""" + + @property + def color_mode(self): + """Set color mode for this entity.""" + return COLOR_MODE_BRIGHTNESS + + @property + def supported_color_modes(self): + """Flag supported color_modes (in an array format).""" + return [COLOR_MODE_BRIGHTNESS] + + +class VeSyncTunableWhiteLightHA(VeSyncBaseLight, LightEntity): + """Representation of a VeSync Tunable White Light device.""" + + @property + def color_temp(self): + """Get device white temperature.""" + # get value from pyvesync library api, + result = self.device.color_temp_pct + try: + # check for validity of brightness value received + color_temp_value = int(result) + except ValueError: + # deal if any unexpected/non numeric value + _LOGGER.debug( + "VeSync - received unexpected 'color_temp_pct' value from pyvesync api: %s", + result, + ) + return 0 + # flip cold/warm + color_temp_value = 100 - color_temp_value + # ensure value between 0-100 + color_temp_value = max(0, min(color_temp_value, 100)) + # convert percent value to Mireds + color_temp_value = round( + self.min_mireds + + ((self.max_mireds - self.min_mireds) / 100 * color_temp_value) + ) + # ensure value between minimum and maximum Mireds + return max(self.min_mireds, min(color_temp_value, self.max_mireds)) + + @property + def min_mireds(self): + """Set device coldest white temperature.""" + return 154 # 154 Mireds ( 1,000,000 divided by 6500 Kelvin = 154 Mireds) + + @property + def max_mireds(self): + """Set device warmest white temperature.""" + return 370 # 370 Mireds ( 1,000,000 divided by 2700 Kelvin = 370 Mireds) + + @property + def color_mode(self): + """Set color mode for this entity.""" + return COLOR_MODE_COLOR_TEMP + + @property + def supported_color_modes(self): + """Flag supported color_modes (in an array format).""" + return [COLOR_MODE_COLOR_TEMP] diff --git a/homeassistant/components/vesync/manifest.json b/homeassistant/components/vesync/manifest.json index f09a58e4696..70c46d0f02e 100644 --- a/homeassistant/components/vesync/manifest.json +++ b/homeassistant/components/vesync/manifest.json @@ -3,7 +3,7 @@ "name": "VeSync", "documentation": "https://www.home-assistant.io/integrations/vesync", "codeowners": ["@markperdue", "@webdjoe", "@thegardenmonkey"], - "requirements": ["pyvesync==1.3.1"], + "requirements": ["pyvesync==1.4.0"], "config_flow": true, "iot_class": "cloud_polling" } diff --git a/homeassistant/components/vesync/services.yaml b/homeassistant/components/vesync/services.yaml index dec19740aef..da264ea3b5d 100644 --- a/homeassistant/components/vesync/services.yaml +++ b/homeassistant/components/vesync/services.yaml @@ -1,2 +1,3 @@ update_devices: + name: Update devices description: Add new VeSync devices to Home Assistant diff --git a/homeassistant/components/vicare/climate.py b/homeassistant/components/vicare/climate.py index c819c6593a1..cfbfa1ddec6 100644 --- a/homeassistant/components/vicare/climate.py +++ b/homeassistant/components/vicare/climate.py @@ -104,7 +104,7 @@ async def async_setup_platform( ] ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_VICARE_MODE, diff --git a/homeassistant/components/vicare/services.yaml b/homeassistant/components/vicare/services.yaml index 2efaf530a9c..94146c4250e 100644 --- a/homeassistant/components/vicare/services.yaml +++ b/homeassistant/components/vicare/services.yaml @@ -1,9 +1,22 @@ set_vicare_mode: + name: Set vicare mode description: Set a ViCare mode. + target: + entity: + integration: vicare + domain: climate fields: - entity_id: - description: Name(s) of vicare climate entities. - example: "climate.vicare_heating" vicare_mode: - description: ViCare mode. One of "dhw", "dhwAndHeating", "heating", "dhwAndHeatingCooling", "forcedReduced", "forcedNormal" or "standby" - example: "dhw" + name: Vicare Mode + description: ViCare mode. + required: true + selector: + select: + options: + - 'dhw' + - 'dhwAndHeating' + - 'dhwAndHeatingCooling' + - 'forcedNormal' + - 'forcedReduced' + - 'heating' + - 'standby' diff --git a/homeassistant/components/vilfo/config_flow.py b/homeassistant/components/vilfo/config_flow.py index 569ce7992fa..9483542f19b 100644 --- a/homeassistant/components/vilfo/config_flow.py +++ b/homeassistant/components/vilfo/config_flow.py @@ -106,7 +106,6 @@ class DomainConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Vilfo Router.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): """Handle the initial step.""" diff --git a/homeassistant/components/vizio/__init__.py b/homeassistant/components/vizio/__init__.py index bec6b803023..7c1ed7e8fa7 100644 --- a/homeassistant/components/vizio/__init__.py +++ b/homeassistant/components/vizio/__init__.py @@ -10,12 +10,12 @@ from pyvizio.util import gen_apps_list_from_url import voluptuous as vol from homeassistant.components.media_player import DEVICE_CLASS_TV -from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.typing import ConfigType -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import CONF_APPS, CONF_DEVICE_CLASS, DOMAIN, VIZIO_SCHEMA @@ -80,7 +80,7 @@ async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> ) # Exclude this config entry because its not unloaded yet if not any( - entry.state == ENTRY_STATE_LOADED + entry.state is ConfigEntryState.LOADED and entry.entry_id != config_entry.entry_id and entry.data[CONF_DEVICE_CLASS] == DEVICE_CLASS_TV for entry in hass.config_entries.async_entries(DOMAIN) @@ -106,10 +106,30 @@ class VizioAppsDataUpdateCoordinator(DataUpdateCoordinator): update_method=self._async_update_data, ) self.data = APPS + self.fail_count = 0 + self.fail_threshold = 10 async def _async_update_data(self) -> list[dict[str, Any]]: """Update data via library.""" data = await gen_apps_list_from_url(session=async_get_clientsession(self.hass)) if not data: - raise UpdateFailed + # For every failure, increase the fail count until we reach the threshold. + # We then log a warning, increase the threshold, and reset the fail count. + # This is here to prevent silent failures but to reduce repeat logs. + if self.fail_count == self.fail_threshold: + _LOGGER.warning( + ( + "Unable to retrieve the apps list from the external server " + "for the last %s days" + ), + self.fail_threshold, + ) + self.fail_count = 0 + self.fail_threshold += 10 + else: + self.fail_count += 1 + return self.data + # Reset the fail count and threshold when the data is successfully retrieved + self.fail_count = 0 + self.fail_threshold = 10 return sorted(data, key=lambda app: app["name"]) diff --git a/homeassistant/components/vizio/config_flow.py b/homeassistant/components/vizio/config_flow.py index 55504a753f1..c1aba99d84b 100644 --- a/homeassistant/components/vizio/config_flow.py +++ b/homeassistant/components/vizio/config_flow.py @@ -175,7 +175,6 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Vizio config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback @@ -279,7 +278,7 @@ class VizioConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a config entry from configuration.yaml.""" # Check if new config entry matches any existing config entries - for entry in self.hass.config_entries.async_entries(DOMAIN): + for entry in self._async_current_entries(): # If source is ignore bypass host check and continue through loop if entry.source == SOURCE_IGNORE: continue diff --git a/homeassistant/components/vizio/media_player.py b/homeassistant/components/vizio/media_player.py index 57d770b26ae..f5071ce146a 100644 --- a/homeassistant/components/vizio/media_player.py +++ b/homeassistant/components/vizio/media_player.py @@ -3,7 +3,7 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any, Callable +from typing import Any from pyvizio import VizioAsync from pyvizio.api.apps import find_app_name @@ -33,7 +33,8 @@ from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import ( @@ -65,7 +66,7 @@ PARALLEL_UPDATES = 0 async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Vizio media player entry.""" host = config_entry.data[CONF_HOST] @@ -120,7 +121,7 @@ async def async_setup_entry( entity = VizioDevice(config_entry, device, name, device_class, apps_coordinator) async_add_entities([entity], update_before_add=True) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_UPDATE_SETTING, UPDATE_SETTING_SCHEMA, "async_update_setting" ) @@ -423,7 +424,7 @@ class VizioDevice(MediaPlayerEntity): return self._config_entry.unique_id @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device registry information.""" return { "identifiers": {(DOMAIN, self._config_entry.unique_id)}, diff --git a/homeassistant/components/vizio/translations/de.json b/homeassistant/components/vizio/translations/de.json index 913317c88d7..28cb0d2c0b2 100644 --- a/homeassistant/components/vizio/translations/de.json +++ b/homeassistant/components/vizio/translations/de.json @@ -7,7 +7,8 @@ }, "error": { "cannot_connect": "Verbindung fehlgeschlagen", - "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst." + "complete_pairing_failed": "Das Pairing konnte nicht abgeschlossen werden. Vergewissere dich, dass der eingegebene PIN korrekt ist und dass der Fernseher noch mit Strom versorgt wird und mit dem Netzwerk verbunden ist, bevor du es erneut versuchst.", + "existing_config_entry_found": "Ein bestehender VIZIO SmartCast-Ger\u00e4t Config-Eintrag mit der gleichen Seriennummer wurde bereits konfiguriert. Sie m\u00fcssen den vorhandenen Eintrag l\u00f6schen, um diesen zu konfigurieren." }, "step": { "pair_tv": { @@ -18,11 +19,11 @@ "title": "Schlie\u00dfen Sie den Pairing-Prozess ab" }, "pairing_complete": { - "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.", + "description": "Dein VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.", "title": "Kopplung abgeschlossen" }, "pairing_complete_import": { - "description": "Dein Richten Sie das VIZIO SmartCast-Ger\u00e4t ein ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.", + "description": "Dein VIZIO SmartCast-Ger\u00e4t ist jetzt mit Home Assistant verbunden.\n\nDein Zugangstoken ist '**{access_token}**'.", "title": "Kopplung abgeschlossen" }, "user": { @@ -33,7 +34,7 @@ "name": "Name" }, "description": "Ein Zugangstoken wird nur f\u00fcr Fernsehger\u00e4te ben\u00f6tigt. Wenn du ein Fernsehger\u00e4t konfigurierst und noch kein Zugangstoken hast, lass es leer, um einen Pairing-Vorgang durchzuf\u00fchren.", - "title": "Richten Sie das VIZIO SmartCast-Ger\u00e4t ein" + "title": "VIZIO SmartCast-Ger\u00e4t" } } }, @@ -46,7 +47,7 @@ "volume_step": "Lautst\u00e4rken-Schrittgr\u00f6\u00dfe" }, "description": "Wenn Sie \u00fcber ein Smart-TV-Ger\u00e4t verf\u00fcgen, k\u00f6nnen Sie Ihre Quellliste optional filtern, indem Sie ausw\u00e4hlen, welche Apps in Ihre Quellliste aufgenommen oder ausgeschlossen werden sollen.", - "title": "Aktualisiere die Richten Sie das VIZIO SmartCast-Ger\u00e4t ein-Optionen" + "title": "Aktualisiere die VIZIO SmartCast-Ger\u00e4t-Optionen" } } } diff --git a/homeassistant/components/vizio/translations/nl.json b/homeassistant/components/vizio/translations/nl.json index 48a7a7d353d..f17e0470799 100644 --- a/homeassistant/components/vizio/translations/nl.json +++ b/homeassistant/components/vizio/translations/nl.json @@ -8,7 +8,7 @@ "error": { "cannot_connect": "Verbinding mislukt", "complete_pairing_failed": "Kan het koppelen niet voltooien. Zorg ervoor dat de door u opgegeven pincode correct is en dat de tv nog steeds van stroom wordt voorzien en is verbonden met het netwerk voordat u opnieuw verzendt.", - "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande invoer verwijderen om deze te kunnen configureren." + "existing_config_entry_found": "Een bestaande VIZIO SmartCast-apparaat config entry met hetzelfde serienummer is reeds geconfigureerd. U moet de bestaande entry verwijderen om deze te kunnen configureren." }, "step": { "pair_tv": { diff --git a/homeassistant/components/vlc_telnet/manifest.json b/homeassistant/components/vlc_telnet/manifest.json index 1aa41fb9bb9..d03e9163961 100644 --- a/homeassistant/components/vlc_telnet/manifest.json +++ b/homeassistant/components/vlc_telnet/manifest.json @@ -1,7 +1,7 @@ { "domain": "vlc_telnet", "name": "VLC media player Telnet", - "documentation": "https://www.home-assistant.io/integrations/vlc-telnet", + "documentation": "https://www.home-assistant.io/integrations/vlc_telnet", "requirements": ["python-telnet-vlc==2.0.1"], "codeowners": ["@rodripf", "@dmcc"], "iot_class": "local_polling" diff --git a/homeassistant/components/volumio/config_flow.py b/homeassistant/components/volumio/config_flow.py index 80ec2f05d91..45c424b356e 100644 --- a/homeassistant/components/volumio/config_flow.py +++ b/homeassistant/components/volumio/config_flow.py @@ -36,7 +36,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Volumio.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize flow.""" diff --git a/homeassistant/components/volvooncall/binary_sensor.py b/homeassistant/components/volvooncall/binary_sensor.py index f08b4509bd1..7d2a9660703 100644 --- a/homeassistant/components/volvooncall/binary_sensor.py +++ b/homeassistant/components/volvooncall/binary_sensor.py @@ -16,7 +16,9 @@ class VolvoSensor(VolvoEntity, BinarySensorEntity): @property def is_on(self): - """Return True if the binary sensor is on.""" + """Return True if the binary sensor is on, but invert for the 'Door lock'.""" + if self.instrument.attr == "is_locked": + return not self.instrument.is_on return self.instrument.is_on @property diff --git a/homeassistant/components/wake_on_lan/services.yaml b/homeassistant/components/wake_on_lan/services.yaml index 7540451d061..ea374a88b8f 100644 --- a/homeassistant/components/wake_on_lan/services.yaml +++ b/homeassistant/components/wake_on_lan/services.yaml @@ -18,7 +18,7 @@ send_magic_packet: broadcast_port: name: Broadcast port description: Port where to send the magic packet. - example: 9 + default: 9 selector: number: min: 1 diff --git a/homeassistant/components/wallbox/__init__.py b/homeassistant/components/wallbox/__init__.py new file mode 100644 index 00000000000..97b2ea12f35 --- /dev/null +++ b/homeassistant/components/wallbox/__init__.py @@ -0,0 +1,147 @@ +"""The Wallbox integration.""" +import asyncio +from datetime import timedelta +import logging + +import requests +from wallbox import Wallbox + +from homeassistant import exceptions +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator + +from .const import CONF_CONNECTIONS, CONF_ROUND, CONF_SENSOR_TYPES, CONF_STATION, DOMAIN + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS = ["sensor"] +UPDATE_INTERVAL = 30 + + +class WallboxHub: + """Wallbox Hub class.""" + + def __init__(self, station, username, password, hass): + """Initialize.""" + self._station = station + self._username = username + self._password = password + self._wallbox = Wallbox(self._username, self._password) + self._hass = hass + self._coordinator = DataUpdateCoordinator( + hass, + _LOGGER, + # Name of the data. For logging purposes. + name="wallbox", + update_method=self.async_get_data, + # Polling interval. Will only be polled if there are subscribers. + update_interval=timedelta(seconds=UPDATE_INTERVAL), + ) + + def _authenticate(self): + """Authenticate using Wallbox API.""" + try: + self._wallbox.authenticate() + return True + except requests.exceptions.HTTPError as wallbox_connection_error: + if wallbox_connection_error.response.status_code == 403: + raise InvalidAuth from wallbox_connection_error + raise ConnectionError from wallbox_connection_error + + def _get_data(self): + """Get new sensor data for Wallbox component.""" + try: + self._authenticate() + data = self._wallbox.getChargerStatus(self._station) + + filtered_data = {k: data[k] for k in CONF_SENSOR_TYPES if k in data} + + for key, value in filtered_data.items(): + sensor_round = CONF_SENSOR_TYPES[key][CONF_ROUND] + if sensor_round: + try: + filtered_data[key] = round(value, sensor_round) + except TypeError: + _LOGGER.debug("Cannot format %s", key) + + return filtered_data + except requests.exceptions.HTTPError as wallbox_connection_error: + raise ConnectionError from wallbox_connection_error + + async def async_coordinator_first_refresh(self): + """Refresh coordinator for the first time.""" + await self._coordinator.async_config_entry_first_refresh() + + async def async_authenticate(self) -> bool: + """Authenticate using Wallbox API.""" + return await self._hass.async_add_executor_job(self._authenticate) + + async def async_get_data(self) -> bool: + """Get new sensor data for Wallbox component.""" + data = await self._hass.async_add_executor_job(self._get_data) + return data + + @property + def coordinator(self): + """Return the coordinator.""" + return self._coordinator + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): + """Set up Wallbox from a config entry.""" + wallbox = WallboxHub( + entry.data[CONF_STATION], + entry.data[CONF_USERNAME], + entry.data[CONF_PASSWORD], + hass, + ) + + await wallbox.async_authenticate() + + await wallbox.async_coordinator_first_refresh() + + hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}}) + hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = wallbox + + for platform in PLATFORMS: + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, platform) + ) + + 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]["connections"].pop(entry.entry_id) + + return unload_ok + + +class CannotConnect(exceptions.HomeAssistantError): + """Error to indicate we cannot connect.""" + + def __init__(self, msg=""): + """Create a log record.""" + super().__init__() + _LOGGER.error("Cannot connect to Wallbox API. %s", msg) + + +class InvalidAuth(exceptions.HomeAssistantError): + """Error to indicate there is invalid auth.""" + + def __init__(self, msg=""): + """Create a log record.""" + super().__init__() + _LOGGER.error("Cannot authenticate with Wallbox API. %s", msg) diff --git a/homeassistant/components/wallbox/config_flow.py b/homeassistant/components/wallbox/config_flow.py new file mode 100644 index 00000000000..69b01d96c40 --- /dev/null +++ b/homeassistant/components/wallbox/config_flow.py @@ -0,0 +1,58 @@ +"""Config flow for Wallbox integration.""" +import voluptuous as vol + +from homeassistant import config_entries, core +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from . import CannotConnect, InvalidAuth, WallboxHub +from .const import CONF_STATION, DOMAIN + +COMPONENT_DOMAIN = DOMAIN + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_STATION): str, + vol.Required(CONF_USERNAME): str, + vol.Required(CONF_PASSWORD): str, + } +) + + +async def validate_input(hass: core.HomeAssistant, data): + """Validate the user input allows to connect. + + Data has the keys from STEP_USER_DATA_SCHEMA with values provided by the user. + """ + hub = WallboxHub(data["station"], data["username"], data["password"], hass) + + await hub.async_get_data() + + # Return info that you want to store in the config entry. + return {"title": "Wallbox Portal"} + + +class ConfigFlow(config_entries.ConfigFlow, domain=COMPONENT_DOMAIN): + """Handle a config flow for Wallbox.""" + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + if user_input is None: + return self.async_show_form( + step_id="user", + data_schema=STEP_USER_DATA_SCHEMA, + ) + + errors = {} + + try: + info = await validate_input(self.hass, user_input) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + else: + return self.async_create_entry(title=info["title"], data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/wallbox/const.py b/homeassistant/components/wallbox/const.py new file mode 100644 index 00000000000..41996107ce0 --- /dev/null +++ b/homeassistant/components/wallbox/const.py @@ -0,0 +1,99 @@ +"""Constants for the Wallbox integration.""" +from homeassistant.const import ( + CONF_ICON, + CONF_NAME, + CONF_UNIT_OF_MEASUREMENT, + ELECTRICAL_CURRENT_AMPERE, + ENERGY_KILO_WATT_HOUR, + LENGTH_KILOMETERS, + PERCENTAGE, + POWER_KILO_WATT, + STATE_UNAVAILABLE, +) + +DOMAIN = "wallbox" + +CONF_STATION = "station" + +CONF_CONNECTIONS = "connections" +CONF_ROUND = "round" + +CONF_SENSOR_TYPES = { + "charging_power": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Charging Power", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: POWER_KILO_WATT, + STATE_UNAVAILABLE: False, + }, + "max_available_power": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Max Available Power", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: ELECTRICAL_CURRENT_AMPERE, + STATE_UNAVAILABLE: False, + }, + "charging_speed": { + CONF_ICON: "mdi:speedometer", + CONF_NAME: "Charging Speed", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "added_range": { + CONF_ICON: "mdi:map-marker-distance", + CONF_NAME: "Added Range", + CONF_ROUND: 0, + CONF_UNIT_OF_MEASUREMENT: LENGTH_KILOMETERS, + STATE_UNAVAILABLE: False, + }, + "added_energy": { + CONF_ICON: "mdi:battery-positive", + CONF_NAME: "Added Energy", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR, + STATE_UNAVAILABLE: False, + }, + "charging_time": { + CONF_ICON: "mdi:timer", + CONF_NAME: "Charging Time", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "cost": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Cost", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "state_of_charge": { + CONF_ICON: "mdi:battery-charging-80", + CONF_NAME: "State of Charge", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: PERCENTAGE, + STATE_UNAVAILABLE: False, + }, + "current_mode": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Current Mode", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "depot_price": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Depot Price", + CONF_ROUND: 2, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, + "status_description": { + CONF_ICON: "mdi:ev-station", + CONF_NAME: "Status Description", + CONF_ROUND: None, + CONF_UNIT_OF_MEASUREMENT: None, + STATE_UNAVAILABLE: False, + }, +} diff --git a/homeassistant/components/wallbox/manifest.json b/homeassistant/components/wallbox/manifest.json new file mode 100644 index 00000000000..aeadf541345 --- /dev/null +++ b/homeassistant/components/wallbox/manifest.json @@ -0,0 +1,13 @@ +{ + "domain": "wallbox", + "name": "Wallbox", + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/wallbox", + "requirements": ["wallbox==0.4.4"], + "ssdp": [], + "zeroconf": [], + "homekit": {}, + "dependencies": [], + "codeowners": ["@hesselonline"], + "iot_class": "cloud_polling" +} diff --git a/homeassistant/components/wallbox/sensor.py b/homeassistant/components/wallbox/sensor.py new file mode 100644 index 00000000000..6d3ef952cbe --- /dev/null +++ b/homeassistant/components/wallbox/sensor.py @@ -0,0 +1,61 @@ +"""Home Assistant component for accessing the Wallbox Portal API. The sensor component creates multiple sensors regarding wallbox performance.""" + +from homeassistant.helpers.entity import Entity +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import ( + CONF_CONNECTIONS, + CONF_ICON, + CONF_NAME, + CONF_SENSOR_TYPES, + CONF_UNIT_OF_MEASUREMENT, + DOMAIN, +) + +CONF_STATION = "station" +UPDATE_INTERVAL = 30 + + +async def async_setup_entry(hass, config, async_add_entities): + """Create wallbox sensor entities in HASS.""" + wallbox = hass.data[DOMAIN][CONF_CONNECTIONS][config.entry_id] + + coordinator = wallbox.coordinator + + async_add_entities( + WallboxSensor(coordinator, idx, ent, config) + for idx, ent in enumerate(coordinator.data) + ) + + +class WallboxSensor(CoordinatorEntity, Entity): + """Representation of the Wallbox portal.""" + + def __init__(self, coordinator, idx, ent, config): + """Initialize a Wallbox sensor.""" + super().__init__(coordinator) + self._properties = CONF_SENSOR_TYPES[ent] + self._name = f"{config.title} {self._properties[CONF_NAME]}" + self._icon = self._properties[CONF_ICON] + self._unit = self._properties[CONF_UNIT_OF_MEASUREMENT] + self._ent = ent + + @property + def name(self): + """Return the name of the sensor.""" + return self._name + + @property + def state(self): + """Return the state of the sensor.""" + return self.coordinator.data[self._ent] + + @property + def unit_of_measurement(self): + """Return the unit of the sensor.""" + return self._unit + + @property + def icon(self): + """Return the icon of the sensor.""" + return self._icon diff --git a/homeassistant/components/wallbox/strings.json b/homeassistant/components/wallbox/strings.json new file mode 100644 index 00000000000..63fc5d89e85 --- /dev/null +++ b/homeassistant/components/wallbox/strings.json @@ -0,0 +1,22 @@ +{ + "title": "Wallbox", + "config": { + "step": { + "user": { + "data": { + "station": "Station Serial Number", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/en.json b/homeassistant/components/wallbox/translations/en.json new file mode 100644 index 00000000000..52dcf8530d4 --- /dev/null +++ b/homeassistant/components/wallbox/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Device is already configured" + }, + "error": { + "cannot_connect": "Failed to connect", + "invalid_auth": "Invalid authentication", + "unknown": "Unexpected error" + }, + "step": { + "user": { + "data": { + "password": "Password", + "station": "Station Serial Number", + "username": "Username" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/et.json b/homeassistant/components/wallbox/translations/et.json new file mode 100644 index 00000000000..12e24fd83ba --- /dev/null +++ b/homeassistant/components/wallbox/translations/et.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Seade on juba h\u00e4\u00e4lestatud" + }, + "error": { + "cannot_connect": "\u00dchendamine nurjus", + "invalid_auth": "Tuvastamine nurjus", + "unknown": "Tundmatu t\u00f5rge" + }, + "step": { + "user": { + "data": { + "password": "Salas\u00f5na", + "station": "Seadme seerianumber", + "username": "Kasutajanimi" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/nl.json b/homeassistant/components/wallbox/translations/nl.json new file mode 100644 index 00000000000..6ba03e7ee99 --- /dev/null +++ b/homeassistant/components/wallbox/translations/nl.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Apparaat is al geconfigureerd" + }, + "error": { + "cannot_connect": "Kan geen verbinding maken", + "invalid_auth": "Ongeldige authenticatie", + "unknown": "Onverwachte fout" + }, + "step": { + "user": { + "data": { + "password": "Wachtwoord", + "station": "Station Serienummer", + "username": "Gebruikersnaam" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/no.json b/homeassistant/components/wallbox/translations/no.json new file mode 100644 index 00000000000..42368703121 --- /dev/null +++ b/homeassistant/components/wallbox/translations/no.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "Enheten er allerede konfigurert" + }, + "error": { + "cannot_connect": "Tilkobling mislyktes", + "invalid_auth": "Ugyldig godkjenning", + "unknown": "Uventet feil" + }, + "step": { + "user": { + "data": { + "password": "Passord", + "station": "Serienummer for stasjon", + "username": "Brukernavn" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/ru.json b/homeassistant/components/wallbox/translations/ru.json new file mode 100644 index 00000000000..b6b33a6eb48 --- /dev/null +++ b/homeassistant/components/wallbox/translations/ru.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant." + }, + "error": { + "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f.", + "invalid_auth": "\u041e\u0448\u0438\u0431\u043a\u0430 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438.", + "unknown": "\u041d\u0435\u043f\u0440\u0435\u0434\u0432\u0438\u0434\u0435\u043d\u043d\u0430\u044f \u043e\u0448\u0438\u0431\u043a\u0430." + }, + "step": { + "user": { + "data": { + "password": "\u041f\u0430\u0440\u043e\u043b\u044c", + "station": "\u0421\u0435\u0440\u0438\u0439\u043d\u044b\u0439 \u043d\u043e\u043c\u0435\u0440 \u0441\u0442\u0430\u043d\u0446\u0438\u0438", + "username": "\u0418\u043c\u044f \u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u0435\u043b\u044f" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/wallbox/translations/zh-Hant.json b/homeassistant/components/wallbox/translations/zh-Hant.json new file mode 100644 index 00000000000..78a752f9a0d --- /dev/null +++ b/homeassistant/components/wallbox/translations/zh-Hant.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" + }, + "error": { + "cannot_connect": "\u9023\u7dda\u5931\u6557", + "invalid_auth": "\u9a57\u8b49\u78bc\u7121\u6548", + "unknown": "\u672a\u9810\u671f\u932f\u8aa4" + }, + "step": { + "user": { + "data": { + "password": "\u5bc6\u78bc", + "station": "\u5de5\u4f5c\u7ad9\u5e8f\u865f", + "username": "\u4f7f\u7528\u8005\u540d\u7a31" + } + } + } + }, + "title": "Wallbox" +} \ No newline at end of file diff --git a/homeassistant/components/water_heater/services.yaml b/homeassistant/components/water_heater/services.yaml index 8aee796b9cb..a3b372f219e 100644 --- a/homeassistant/components/water_heater/services.yaml +++ b/homeassistant/components/water_heater/services.yaml @@ -1,31 +1,54 @@ # Describes the format for available water_heater services set_away_mode: + name: Set away mode description: Turn away mode on/off for water_heater device. + target: + entity: + domain: water_heater fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.water_heater" away_mode: + name: Away mode description: New value of away mode. - example: true + required: true + selector: + boolean: set_temperature: + name: Set temperature description: Set target temperature of water_heater device. + target: + entity: + domain: water_heater fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.water_heater" temperature: + name: Temperature description: New target temperature for water heater. - example: 25 - -set_operation_mode: - description: Set operation mode for water_heater device. - fields: - entity_id: - description: Name(s) of entities to change. - example: "water_heater.water_heater" + required: true + selector: + number: + min: 0 + max: 100 + step: 0.5 + unit_of_measurement: "°" operation_mode: + name: Operation mode description: New value of operation mode. example: eco + selector: + text: + +set_operation_mode: + name: Set operation mode + description: Set operation mode for water_heater device. + target: + entity: + domain: water_heater + fields: + operation_mode: + name: Operation mode + description: New value of operation mode. + required: true + example: eco + selector: + text: diff --git a/homeassistant/components/water_heater/translations/ru.json b/homeassistant/components/water_heater/translations/ru.json index b3491f82e5e..1220a4ec70f 100644 --- a/homeassistant/components/water_heater/translations/ru.json +++ b/homeassistant/components/water_heater/translations/ru.json @@ -12,7 +12,7 @@ "gas": "\u0413\u0430\u0437", "heat_pump": "\u0422\u0435\u043f\u043b\u043e\u0432\u043e\u0439 \u043d\u0430\u0441\u043e\u0441", "high_demand": "\u0411\u043e\u043b\u044c\u0448\u0430\u044f \u043d\u0430\u0433\u0440\u0443\u0437\u043a\u0430", - "off": "\u0412\u044b\u043a\u043b", + "off": "\u0412\u044b\u043a\u043b\u044e\u0447\u0435\u043d\u043e", "performance": "\u041f\u0440\u043e\u0438\u0437\u0432\u043e\u0434\u0438\u0442\u0435\u043b\u044c\u043d\u043e\u0441\u0442\u044c" } } diff --git a/homeassistant/components/watson_tts/manifest.json b/homeassistant/components/watson_tts/manifest.json index e833ac02638..679ea1ef5c3 100644 --- a/homeassistant/components/watson_tts/manifest.json +++ b/homeassistant/components/watson_tts/manifest.json @@ -2,7 +2,7 @@ "domain": "watson_tts", "name": "IBM Watson TTS", "documentation": "https://www.home-assistant.io/integrations/watson_tts", - "requirements": ["ibm-watson==4.0.1"], + "requirements": ["ibm-watson==5.1.0"], "codeowners": ["@rutkai"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/watson_tts/tts.py b/homeassistant/components/watson_tts/tts.py index ad989ec39fc..cdcbbc6ed2a 100644 --- a/homeassistant/components/watson_tts/tts.py +++ b/homeassistant/components/watson_tts/tts.py @@ -1,4 +1,6 @@ """Support for IBM Watson TTS integration.""" +import logging + from ibm_cloud_sdk_core.authenticators import IAMAuthenticator from ibm_watson import TextToSpeechV1 import voluptuous as vol @@ -6,10 +8,12 @@ import voluptuous as vol from homeassistant.components.tts import PLATFORM_SCHEMA, Provider import homeassistant.helpers.config_validation as cv +_LOGGER = logging.getLogger(__name__) + CONF_URL = "watson_url" CONF_APIKEY = "watson_apikey" -DEFAULT_URL = "https://stream.watsonplatform.net/text-to-speech/api" +DEFAULT_URL = "https://api.us-south.text-to-speech.watson.cloud.ibm.com" CONF_VOICE = "voice" CONF_OUTPUT_FORMAT = "output_format" @@ -18,22 +22,32 @@ CONF_TEXT_TYPE = "text" # List from https://tinyurl.com/watson-tts-docs SUPPORTED_VOICES = [ "ar-AR_OmarVoice", + "ar-MS_OmarVoice", + "de-DE_BirgitV2Voice", "de-DE_BirgitV3Voice", "de-DE_BirgitVoice", + "de-DE_DieterV2Voice", "de-DE_DieterV3Voice", "de-DE_DieterVoice", "de-DE_ErikaV3Voice", + "en-AU_CraigVoice", + "en-AU_MadisonVoice", "en-GB_KateV3Voice", "en-GB_KateVoice", "en-GB_CharlotteV3Voice", "en-GB_JamesV3Voice", + "en-GB_KateV3Voice", + "en-GB_KateVoice", + "en-US_AllisonV2Voice", "en-US_AllisonV3Voice", "en-US_AllisonVoice", "en-US_EmilyV3Voice", "en-US_HenryV3Voice", "en-US_KevinV3Voice", + "en-US_LisaV2Voice", "en-US_LisaV3Voice", "en-US_LisaVoice", + "en-US_MichaelV2Voice", "en-US_MichaelV3Voice", "en-US_MichaelVoice", "en-US_OliviaV3Voice", @@ -45,12 +59,17 @@ SUPPORTED_VOICES = [ "es-LA_SofiaVoice", "es-US_SofiaV3Voice", "es-US_SofiaVoice", + "fr-CA_LouiseV3Voice", + "fr-FR_NicolasV3Voice", "fr-FR_ReneeV3Voice", "fr-FR_ReneeVoice", + "it-IT_FrancescaV2Voice", "it-IT_FrancescaV3Voice", "it-IT_FrancescaVoice", "ja-JP_EmiV3Voice", "ja-JP_EmiVoice", + "ko-KR_HyunjunVoice", + "ko-KR_SiWooVoice", "ko-KR_YoungmiVoice", "ko-KR_YunaVoice", "nl-NL_EmmaVoice", @@ -62,6 +81,25 @@ SUPPORTED_VOICES = [ "zh-CN_ZhangJingVoice", ] +DEPRECATED_VOICES = [ + "ar-AR_OmarVoice", + "de-DE_BirgitVoice", + "de-DE_DieterVoice", + "en-GB_KateVoice", + "en-GB_KateV3Voice", + "en-US_AllisonVoice", + "en-US_LisaVoice", + "en-US_MichaelVoice", + "es-ES_EnriqueVoice", + "es-ES_LauraVoice", + "es-LA_SofiaVoice", + "es-US_SofiaVoice", + "fr-FR_ReneeVoice", + "it-IT_FrancescaVoice", + "ja-JP_EmiVoice", + "pt-BR_IsabelaVoice", +] + SUPPORTED_OUTPUT_FORMATS = [ "audio/flac", "audio/mp3", @@ -82,7 +120,7 @@ CONTENT_TYPE_EXTENSIONS = { "audio/wav": "wav", } -DEFAULT_VOICE = "en-US_AllisonVoice" +DEFAULT_VOICE = "en-US_AllisonV3Voice" DEFAULT_OUTPUT_FORMAT = "audio/mp3" PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( @@ -109,6 +147,12 @@ def get_engine(hass, config, discovery_info=None): output_format = config[CONF_OUTPUT_FORMAT] service.set_default_headers({"x-watson-learning-opt-out": "true"}) + if default_voice in DEPRECATED_VOICES: + _LOGGER.warning( + "Watson TTS voice %s is deprecated, it may be removed in the future", + default_voice, + ) + return WatsonTTSProvider(service, supported_languages, default_voice, output_format) diff --git a/homeassistant/components/waze_travel_time/__init__.py b/homeassistant/components/waze_travel_time/__init__.py index 5800cfe94ab..57382689f61 100644 --- a/homeassistant/components/waze_travel_time/__init__.py +++ b/homeassistant/components/waze_travel_time/__init__.py @@ -1,13 +1,28 @@ """The waze_travel_time component.""" +import logging from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.helpers.entity_registry import ( + async_entries_for_config_entry, + async_get, +) PLATFORMS = ["sensor"] +_LOGGER = logging.getLogger(__name__) async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: """Load the saved entities.""" + if config_entry.unique_id is not None: + hass.config_entries.async_update_entry(config_entry, unique_id=None) + + ent_reg = async_get(hass) + for entity in async_entries_for_config_entry(ent_reg, config_entry.entry_id): + ent_reg.async_update_entity( + entity.entity_id, new_unique_id=config_entry.entry_id + ) + hass.config_entries.async_setup_platforms(config_entry, PLATFORMS) return True diff --git a/homeassistant/components/waze_travel_time/config_flow.py b/homeassistant/components/waze_travel_time/config_flow.py index 10262e12804..54097ad37bd 100644 --- a/homeassistant/components/waze_travel_time/config_flow.py +++ b/homeassistant/components/waze_travel_time/config_flow.py @@ -1,13 +1,15 @@ """Config flow for Waze Travel Time integration.""" +from __future__ import annotations + import logging +from typing import Any import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_NAME, CONF_REGION -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv -from homeassistant.util import slugify from .const import ( CONF_AVOID_FERRIES, @@ -20,7 +22,12 @@ from .const import ( CONF_REALTIME, CONF_UNITS, CONF_VEHICLE_TYPE, + DEFAULT_AVOID_FERRIES, + DEFAULT_AVOID_SUBSCRIPTION_ROADS, + DEFAULT_AVOID_TOLL_ROADS, DEFAULT_NAME, + DEFAULT_REALTIME, + DEFAULT_VEHICLE_TYPE, DOMAIN, REGIONS, UNITS, @@ -31,6 +38,50 @@ from .helpers import is_valid_config_entry _LOGGER = logging.getLogger(__name__) +def is_dupe_import( + hass: HomeAssistant, entry: config_entries.ConfigEntry, user_input: dict[str, Any] +) -> bool: + """Return whether imported config already exists.""" + entry_data = {**entry.data, **entry.options} + defaults = { + CONF_REALTIME: DEFAULT_REALTIME, + CONF_VEHICLE_TYPE: DEFAULT_VEHICLE_TYPE, + CONF_UNITS: hass.config.units.name, + CONF_AVOID_FERRIES: DEFAULT_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS: DEFAULT_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS: DEFAULT_AVOID_TOLL_ROADS, + } + + for key in ( + CONF_ORIGIN, + CONF_DESTINATION, + CONF_REGION, + CONF_INCL_FILTER, + CONF_EXCL_FILTER, + CONF_REALTIME, + CONF_VEHICLE_TYPE, + CONF_UNITS, + CONF_AVOID_FERRIES, + CONF_AVOID_SUBSCRIPTION_ROADS, + CONF_AVOID_TOLL_ROADS, + ): + # If the key is present the check is simple + if key in user_input and user_input[key] != entry_data[key]: + return False + + # If the key is not present, then we have to check if the key has a default and + # if the default is in the options. If it doesn't have a default, we have to check + # if the key is in the options + if key not in user_input: + if key in defaults and defaults[key] != entry_data[key]: + return False + + if key not in defaults and key in entry_data: + return False + + return True + + class WazeOptionsFlow(config_entries.OptionsFlow): """Handle an options flow for Waze Travel Time.""" @@ -93,7 +144,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Waze Travel Time.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @staticmethod @callback @@ -109,12 +159,14 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): user_input = user_input or {} if user_input: - await self.async_set_unique_id( - slugify( - f"{DOMAIN}_{user_input[CONF_ORIGIN]}_{user_input[CONF_DESTINATION]}" - ) - ) - self._abort_if_unique_id_configured() + # We need to prevent duplicate imports + if self.source == config_entries.SOURCE_IMPORT and any( + is_dupe_import(self.hass, entry, user_input) + for entry in self.hass.config_entries.async_entries(DOMAIN) + if entry.source == config_entries.SOURCE_IMPORT + ): + return self.async_abort(reason="already_configured") + if ( self.source == config_entries.SOURCE_IMPORT or await self.hass.async_add_executor_job( diff --git a/homeassistant/components/waze_travel_time/sensor.py b/homeassistant/components/waze_travel_time/sensor.py index 2796838ee50..4fb56700f59 100644 --- a/homeassistant/components/waze_travel_time/sensor.py +++ b/homeassistant/components/waze_travel_time/sensor.py @@ -4,7 +4,6 @@ from __future__ import annotations from datetime import timedelta import logging import re -from typing import Any, Callable from WazeRouteCalculator import WazeRouteCalculator, WRCError import voluptuous as vol @@ -23,6 +22,8 @@ from homeassistant.const import ( ) from homeassistant.core import Config, CoreState, HomeAssistant import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import DeviceInfo +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import ( ATTR_DESTINATION, @@ -111,7 +112,7 @@ async def async_setup_platform( async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[SensorEntity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up a Waze travel time sensor entry.""" defaults = { @@ -157,7 +158,7 @@ async def async_setup_entry( config_entry, ) - sensor = WazeTravelTime(config_entry.unique_id, name, origin, destination, data) + sensor = WazeTravelTime(config_entry.entry_id, name, origin, destination, data) async_add_entities([sensor], False) @@ -263,7 +264,7 @@ class WazeTravelTime(SensorEntity): self._waze_data.update() @property - def device_info(self) -> dict[str, Any] | None: + def device_info(self) -> DeviceInfo: """Return device specific attributes.""" return { "name": "Waze", diff --git a/homeassistant/components/waze_travel_time/translations/cs.json b/homeassistant/components/waze_travel_time/translations/cs.json index 3f6b731b9bf..35932c10fd4 100644 --- a/homeassistant/components/waze_travel_time/translations/cs.json +++ b/homeassistant/components/waze_travel_time/translations/cs.json @@ -5,6 +5,13 @@ }, "error": { "cannot_connect": "Nepoda\u0159ilo se p\u0159ipojit" + }, + "step": { + "user": { + "data": { + "name": "Jm\u00e9no" + } + } } } } \ No newline at end of file diff --git a/homeassistant/components/waze_travel_time/translations/de.json b/homeassistant/components/waze_travel_time/translations/de.json index f5586b3d80d..ac2a911d233 100644 --- a/homeassistant/components/waze_travel_time/translations/de.json +++ b/homeassistant/components/waze_travel_time/translations/de.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Zielort", + "name": "Name", "origin": "Startort", "region": "Region" } diff --git a/homeassistant/components/waze_travel_time/translations/es.json b/homeassistant/components/waze_travel_time/translations/es.json index 2ae07164fe7..62325233cab 100644 --- a/homeassistant/components/waze_travel_time/translations/es.json +++ b/homeassistant/components/waze_travel_time/translations/es.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destino", + "name": "Nombre", "origin": "Origen", "region": "Regi\u00f3n" }, diff --git a/homeassistant/components/waze_travel_time/translations/fr.json b/homeassistant/components/waze_travel_time/translations/fr.json index 8b977e76d08..e0039ef4b14 100644 --- a/homeassistant/components/waze_travel_time/translations/fr.json +++ b/homeassistant/components/waze_travel_time/translations/fr.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destination", + "name": "Nom", "origin": "Point de d\u00e9part", "region": "R\u00e9gion" }, diff --git a/homeassistant/components/waze_travel_time/translations/it.json b/homeassistant/components/waze_travel_time/translations/it.json index ce109b3751c..bfbe94c2a23 100644 --- a/homeassistant/components/waze_travel_time/translations/it.json +++ b/homeassistant/components/waze_travel_time/translations/it.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destinazione", + "name": "Nome", "origin": "Origine", "region": "Area geografica" }, diff --git a/homeassistant/components/waze_travel_time/translations/nl.json b/homeassistant/components/waze_travel_time/translations/nl.json index 4d7bee4ad4d..223d69625d4 100644 --- a/homeassistant/components/waze_travel_time/translations/nl.json +++ b/homeassistant/components/waze_travel_time/translations/nl.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Bestemming", + "name": "Naam", "origin": "Vertrekpunt", "region": "Regio" }, diff --git a/homeassistant/components/waze_travel_time/translations/no.json b/homeassistant/components/waze_travel_time/translations/no.json index 7ae2bf8d418..c9baef06743 100644 --- a/homeassistant/components/waze_travel_time/translations/no.json +++ b/homeassistant/components/waze_travel_time/translations/no.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "Destinasjon", + "name": "Navn", "origin": "Opprinnelse", "region": "Region" }, diff --git a/homeassistant/components/waze_travel_time/translations/zh-Hant.json b/homeassistant/components/waze_travel_time/translations/zh-Hant.json index a0f71b51ae2..5c30067de6c 100644 --- a/homeassistant/components/waze_travel_time/translations/zh-Hant.json +++ b/homeassistant/components/waze_travel_time/translations/zh-Hant.json @@ -10,6 +10,7 @@ "user": { "data": { "destination": "\u76ee\u7684\u5730", + "name": "\u540d\u7a31", "origin": "\u51fa\u767c\u5730", "region": "\u5340\u57df" }, diff --git a/homeassistant/components/weather/translations/de.json b/homeassistant/components/weather/translations/de.json index 123cae340ab..280922ba5bd 100644 --- a/homeassistant/components/weather/translations/de.json +++ b/homeassistant/components/weather/translations/de.json @@ -9,7 +9,7 @@ "lightning": "Gewitter", "lightning-rainy": "Gewitter, regnerisch", "partlycloudy": "Teilweise bew\u00f6lkt", - "pouring": "Str\u00f6mend", + "pouring": "Platzregen", "rainy": "Regnerisch", "snowy": "Verschneit", "snowy-rainy": "Verschneit, regnerisch", diff --git a/homeassistant/components/weather/translations/it.json b/homeassistant/components/weather/translations/it.json index 171b29673cd..fd3f506a6d0 100644 --- a/homeassistant/components/weather/translations/it.json +++ b/homeassistant/components/weather/translations/it.json @@ -9,7 +9,7 @@ "lightning": "Temporale", "lightning-rainy": "Temporale, piovoso", "partlycloudy": "Parzialmente nuvoloso", - "pouring": "Piogge intense", + "pouring": "Rovescio", "rainy": "Piovoso", "snowy": "Nevoso", "snowy-rainy": "Nevoso, piovoso", diff --git a/homeassistant/components/webostv/services.yaml b/homeassistant/components/webostv/services.yaml index 430916f7c71..f9d56cd1921 100644 --- a/homeassistant/components/webostv/services.yaml +++ b/homeassistant/components/webostv/services.yaml @@ -1,42 +1,76 @@ # Describes the format for available webostv services button: + name: Button description: "Send a button press command." fields: entity_id: + name: Entity description: Name(s) of the webostv entities where to run the API method. - example: "media_player.living_room_tv" + required: true + selector: + entity: + integration: webostv + domain: media_player button: + name: Button description: >- Name of the button to press. Known possible values are LEFT, RIGHT, DOWN, UP, HOME, MENU, BACK, ENTER, DASH, INFO, ASTERISK, CC, EXIT, MUTE, RED, GREEN, BLUE, VOLUMEUP, VOLUMEDOWN, CHANNELUP, CHANNELDOWN, PLAY, PAUSE, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 + required: true example: "LEFT" + selector: + text: command: + name: Command description: "Send a command." fields: entity_id: + name: Entity description: Name(s) of the webostv entities where to run the API method. - example: "media_player.living_room_tv" + required: true + selector: + entity: + integration: webostv + domain: media_player command: + name: Command description: >- Endpoint of the command. Known valid endpoints are listed in https://github.com/TheRealLink/pylgtv/blob/master/pylgtv/endpoints.py + required: true example: "system.launcher/open" + selector: + text: payload: + name: Payload description: >- An optional payload to provide to the endpoint in the format of key value pair(s). example: >- target: https://www.google.com + advanced: true + selector: + object: select_sound_output: + name: Select Sound Output description: "Send the TV the command to change sound output." fields: entity_id: + name: Entity description: Name(s) of the webostv entities to change sound output on. - example: "media_player.living_room_tv" + required: true + selector: + entity: + integration: webostv + domain: media_player sound_output: + name: Sound Output description: Name of the sound output to switch to. + required: true example: "external_speaker" + selector: + text: diff --git a/homeassistant/components/websocket_api/__init__.py b/homeassistant/components/websocket_api/__init__.py index e7b10e18889..52158d3f1ad 100644 --- a/homeassistant/components/websocket_api/__init__.py +++ b/homeassistant/components/websocket_api/__init__.py @@ -1,11 +1,12 @@ """WebSocket based API for Home Assistant.""" from __future__ import annotations -from typing import cast +from typing import Final, cast import voluptuous as vol from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.typing import ConfigType from homeassistant.loader import bind_hass from . import commands, connection, const, decorators, http, messages # noqa: F401 @@ -34,11 +35,9 @@ from .messages import ( # noqa: F401 result_message, ) -# mypy: allow-untyped-calls, allow-untyped-defs +DOMAIN: Final = const.DOMAIN -DOMAIN = const.DOMAIN - -DEPENDENCIES = ("http",) +DEPENDENCIES: Final[tuple[str]] = ("http",) @bind_hass @@ -53,8 +52,8 @@ def async_register_command( # pylint: disable=protected-access if handler is None: handler = cast(const.WebSocketCommandHandler, command_or_handler) - command = handler._ws_command # type: ignore - schema = handler._ws_schema # type: ignore + command = handler._ws_command # type: ignore[attr-defined] + schema = handler._ws_schema # type: ignore[attr-defined] else: command = command_or_handler handlers = hass.data.get(DOMAIN) @@ -63,8 +62,8 @@ def async_register_command( handlers[command] = (handler, schema) -async def async_setup(hass, config): +async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: """Initialize the websocket API.""" - hass.http.register_view(http.WebsocketAPIView) + hass.http.register_view(http.WebsocketAPIView()) commands.async_register_commands(hass, async_register_command) return True diff --git a/homeassistant/components/websocket_api/auth.py b/homeassistant/components/websocket_api/auth.py index 3c795902900..130ffe82840 100644 --- a/homeassistant/components/websocket_api/auth.py +++ b/homeassistant/components/websocket_api/auth.py @@ -1,22 +1,31 @@ """Handle the auth of a connection.""" +from __future__ import annotations + +from collections.abc import Callable +from typing import TYPE_CHECKING, Any, Final + +from aiohttp.web import Request import voluptuous as vol from voluptuous.humanize import humanize_error from homeassistant.auth.models import RefreshToken, User from homeassistant.components.http.ban import process_success_login, process_wrong_login from homeassistant.const import __version__ +from homeassistant.core import HomeAssistant from .connection import ActiveConnection from .error import Disconnect -# mypy: allow-untyped-calls, allow-untyped-defs +if TYPE_CHECKING: + from .http import WebSocketAdapter -TYPE_AUTH = "auth" -TYPE_AUTH_INVALID = "auth_invalid" -TYPE_AUTH_OK = "auth_ok" -TYPE_AUTH_REQUIRED = "auth_required" -AUTH_MESSAGE_SCHEMA = vol.Schema( +TYPE_AUTH: Final = "auth" +TYPE_AUTH_INVALID: Final = "auth_invalid" +TYPE_AUTH_OK: Final = "auth_ok" +TYPE_AUTH_REQUIRED: Final = "auth_required" + +AUTH_MESSAGE_SCHEMA: Final = vol.Schema( { vol.Required("type"): TYPE_AUTH, vol.Exclusive("api_password", "auth"): str, @@ -25,17 +34,17 @@ AUTH_MESSAGE_SCHEMA = vol.Schema( ) -def auth_ok_message(): +def auth_ok_message() -> dict[str, str]: """Return an auth_ok message.""" return {"type": TYPE_AUTH_OK, "ha_version": __version__} -def auth_required_message(): +def auth_required_message() -> dict[str, str]: """Return an auth_required message.""" return {"type": TYPE_AUTH_REQUIRED, "ha_version": __version__} -def auth_invalid_message(message): +def auth_invalid_message(message: str) -> dict[str, str]: """Return an auth_invalid message.""" return {"type": TYPE_AUTH_INVALID, "message": message} @@ -43,16 +52,20 @@ def auth_invalid_message(message): class AuthPhase: """Connection that requires client to authenticate first.""" - def __init__(self, logger, hass, send_message, request): + def __init__( + self, + logger: WebSocketAdapter, + hass: HomeAssistant, + send_message: Callable[[str | dict[str, Any]], None], + request: Request, + ) -> None: """Initialize the authentiated connection.""" self._hass = hass self._send_message = send_message self._logger = logger self._request = request - self._authenticated = False - self._connection = None - async def async_handle(self, msg): + async def async_handle(self, msg: dict[str, str]) -> ActiveConnection: """Handle authentication.""" try: msg = AUTH_MESSAGE_SCHEMA(msg) diff --git a/homeassistant/components/websocket_api/commands.py b/homeassistant/components/websocket_api/commands.py index af2c914bfbd..179fbcd1a30 100644 --- a/homeassistant/components/websocket_api/commands.py +++ b/homeassistant/components/websocket_api/commands.py @@ -1,6 +1,10 @@ """Commands part of Websocket API.""" +from __future__ import annotations + import asyncio +from collections.abc import Callable import json +from typing import Any import voluptuous as vol @@ -8,7 +12,7 @@ from homeassistant.auth.permissions.const import CAT_ENTITIES, POLICY_READ from homeassistant.bootstrap import SIGNAL_BOOTSTRAP_INTEGRATONS from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.const import EVENT_STATE_CHANGED, EVENT_TIME_CHANGED, MATCH_ALL -from homeassistant.core import callback +from homeassistant.core import Context, Event, HomeAssistant, callback from homeassistant.exceptions import ( HomeAssistantError, ServiceNotFound, @@ -17,19 +21,25 @@ from homeassistant.exceptions import ( ) from homeassistant.helpers import config_validation as cv, entity, template from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.event import TrackTemplate, async_track_template_result +from homeassistant.helpers.event import ( + TrackTemplate, + TrackTemplateResult, + async_track_template_result, +) from homeassistant.helpers.json import ExtendedJSONEncoder from homeassistant.helpers.service import async_get_all_descriptions from homeassistant.loader import IntegrationNotFound, async_get_integration from homeassistant.setup import DATA_SETUP_TIME, async_get_loaded_integrations from . import const, decorators, messages - -# mypy: allow-untyped-calls, allow-untyped-defs +from .connection import ActiveConnection @callback -def async_register_commands(hass, async_reg): +def async_register_commands( + hass: HomeAssistant, + async_reg: Callable[[HomeAssistant, const.WebSocketCommandHandler], None], +) -> None: """Register commands.""" async_reg(hass, handle_call_service) async_reg(hass, handle_entity_source) @@ -49,7 +59,7 @@ def async_register_commands(hass, async_reg): async_reg(hass, handle_unsubscribe_events) -def pong_message(iden): +def pong_message(iden: int) -> dict[str, Any]: """Return a pong message.""" return {"id": iden, "type": "pong"} @@ -61,7 +71,9 @@ def pong_message(iden): vol.Optional("event_type", default=MATCH_ALL): str, } ) -def handle_subscribe_events(hass, connection, msg): +def handle_subscribe_events( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle subscribe events command.""" # Circular dep # pylint: disable=import-outside-toplevel @@ -75,7 +87,7 @@ def handle_subscribe_events(hass, connection, msg): if event_type == EVENT_STATE_CHANGED: @callback - def forward_events(event): + def forward_events(event: Event) -> None: """Forward state changed events to websocket.""" if not connection.user.permissions.check_entity( event.data["entity_id"], POLICY_READ @@ -87,7 +99,7 @@ def handle_subscribe_events(hass, connection, msg): else: @callback - def forward_events(event): + def forward_events(event: Event) -> None: """Forward events to websocket.""" if event.event_type == EVENT_TIME_CHANGED: return @@ -107,11 +119,13 @@ def handle_subscribe_events(hass, connection, msg): vol.Required("type"): "subscribe_bootstrap_integrations", } ) -def handle_subscribe_bootstrap_integrations(hass, connection, msg): +def handle_subscribe_bootstrap_integrations( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle subscribe bootstrap integrations command.""" @callback - def forward_bootstrap_integrations(message): + def forward_bootstrap_integrations(message: dict[str, Any]) -> None: """Forward bootstrap integrations to websocket.""" connection.send_message(messages.event_message(msg["id"], message)) @@ -129,7 +143,9 @@ def handle_subscribe_bootstrap_integrations(hass, connection, msg): vol.Required("subscription"): cv.positive_int, } ) -def handle_unsubscribe_events(hass, connection, msg): +def handle_unsubscribe_events( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle unsubscribe events command.""" subscription = msg["subscription"] @@ -154,7 +170,9 @@ def handle_unsubscribe_events(hass, connection, msg): } ) @decorators.async_response -async def handle_call_service(hass, connection, msg): +async def handle_call_service( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle call service command.""" blocking = True # We do not support templates. @@ -206,7 +224,9 @@ async def handle_call_service(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "get_states"}) -def handle_get_states(hass, connection, msg): +def handle_get_states( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle get states command.""" if connection.user.permissions.access_all_entities("read"): states = hass.states.async_all() @@ -223,7 +243,9 @@ def handle_get_states(hass, connection, msg): @decorators.websocket_command({vol.Required("type"): "get_services"}) @decorators.async_response -async def handle_get_services(hass, connection, msg): +async def handle_get_services( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle get services command.""" descriptions = await async_get_all_descriptions(hass) connection.send_message(messages.result_message(msg["id"], descriptions)) @@ -231,14 +253,18 @@ async def handle_get_services(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "get_config"}) -def handle_get_config(hass, connection, msg): +def handle_get_config( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle get config command.""" connection.send_message(messages.result_message(msg["id"], hass.config.as_dict())) @decorators.websocket_command({vol.Required("type"): "manifest/list"}) @decorators.async_response -async def handle_manifest_list(hass, connection, msg): +async def handle_manifest_list( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle integrations command.""" loaded_integrations = async_get_loaded_integrations(hass) integrations = await asyncio.gather( @@ -253,7 +279,9 @@ async def handle_manifest_list(hass, connection, msg): {vol.Required("type"): "manifest/get", vol.Required("integration"): str} ) @decorators.async_response -async def handle_manifest_get(hass, connection, msg): +async def handle_manifest_get( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle integrations command.""" try: integration = await async_get_integration(hass, msg["integration"]) @@ -264,7 +292,9 @@ async def handle_manifest_get(hass, connection, msg): @decorators.websocket_command({vol.Required("type"): "integration/setup_info"}) @decorators.async_response -async def handle_integration_setup_info(hass, connection, msg): +async def handle_integration_setup_info( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle integrations command.""" connection.send_result( msg["id"], @@ -277,7 +307,9 @@ async def handle_integration_setup_info(hass, connection, msg): @callback @decorators.websocket_command({vol.Required("type"): "ping"}) -def handle_ping(hass, connection, msg): +def handle_ping( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle ping command.""" connection.send_message(pong_message(msg["id"])) @@ -293,10 +325,12 @@ def handle_ping(hass, connection, msg): } ) @decorators.async_response -async def handle_render_template(hass, connection, msg): +async def handle_render_template( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle render_template command.""" template_str = msg["template"] - template_obj = template.Template(template_str, hass) + template_obj = template.Template(template_str, hass) # type: ignore[no-untyped-call] variables = msg.get("variables") timeout = msg.get("timeout") info = None @@ -319,7 +353,7 @@ async def handle_render_template(hass, connection, msg): return @callback - def _template_listener(event, updates): + def _template_listener(event: Event, updates: list[TrackTemplateResult]) -> None: nonlocal info track_template_result = updates.pop() result = track_template_result.result @@ -329,7 +363,7 @@ async def handle_render_template(hass, connection, msg): connection.send_message( messages.event_message( - msg["id"], {"result": result, "listeners": info.listeners} # type: ignore + msg["id"], {"result": result, "listeners": info.listeners} # type: ignore[attr-defined] ) ) @@ -356,7 +390,9 @@ async def handle_render_template(hass, connection, msg): @decorators.websocket_command( {vol.Required("type"): "entity/source", vol.Optional("entity_id"): [cv.entity_id]} ) -def handle_entity_source(hass, connection, msg): +def handle_entity_source( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle entity source command.""" raw_sources = entity.entity_sources(hass) entity_perm = connection.user.permissions.check_entity @@ -395,7 +431,6 @@ def handle_entity_source(hass, connection, msg): connection.send_result(msg["id"], sources) -@callback @decorators.websocket_command( { vol.Required("type"): "subscribe_trigger", @@ -405,7 +440,9 @@ def handle_entity_source(hass, connection, msg): ) @decorators.require_admin @decorators.async_response -async def handle_subscribe_trigger(hass, connection, msg): +async def handle_subscribe_trigger( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle subscribe trigger command.""" # Circular dep # pylint: disable=import-outside-toplevel @@ -414,7 +451,9 @@ async def handle_subscribe_trigger(hass, connection, msg): trigger_config = await trigger.async_validate_trigger_config(hass, msg["trigger"]) @callback - def forward_triggers(variables, context=None): + def forward_triggers( + variables: dict[str, Any], context: Context | None = None + ) -> None: """Forward events to websocket.""" message = messages.event_message( msg["id"], {"variables": variables, "context": context} @@ -450,7 +489,9 @@ async def handle_subscribe_trigger(hass, connection, msg): ) @decorators.require_admin @decorators.async_response -async def handle_test_condition(hass, connection, msg): +async def handle_test_condition( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle test condition command.""" # Circular dep # pylint: disable=import-outside-toplevel @@ -471,7 +512,9 @@ async def handle_test_condition(hass, connection, msg): ) @decorators.require_admin @decorators.async_response -async def handle_execute_script(hass, connection, msg): +async def handle_execute_script( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] +) -> None: """Handle execute script command.""" # Circular dep # pylint: disable=import-outside-toplevel diff --git a/homeassistant/components/websocket_api/connection.py b/homeassistant/components/websocket_api/connection.py index 4e0ba257d59..62c21ef5894 100644 --- a/homeassistant/components/websocket_api/connection.py +++ b/homeassistant/components/websocket_api/connection.py @@ -3,48 +3,50 @@ from __future__ import annotations import asyncio from collections.abc import Hashable -from typing import Any, Callable +from typing import TYPE_CHECKING, Any, Callable import voluptuous as vol -from homeassistant.core import Context, callback +from homeassistant.auth.models import RefreshToken, User +from homeassistant.core import Context, HomeAssistant, callback from homeassistant.exceptions import HomeAssistantError, Unauthorized from . import const, messages -# mypy: allow-untyped-calls, allow-untyped-defs +if TYPE_CHECKING: + from .http import WebSocketAdapter class ActiveConnection: """Handle an active websocket client connection.""" - def __init__(self, logger, hass, send_message, user, refresh_token): + def __init__( + self, + logger: WebSocketAdapter, + hass: HomeAssistant, + send_message: Callable[[str | dict[str, Any]], None], + user: User, + refresh_token: RefreshToken, + ) -> None: """Initialize an active connection.""" self.logger = logger self.hass = hass self.send_message = send_message self.user = user - if refresh_token: - self.refresh_token_id = refresh_token.id - else: - self.refresh_token_id = None - + self.refresh_token_id = refresh_token.id self.subscriptions: dict[Hashable, Callable[[], Any]] = {} self.last_id = 0 - def context(self, msg): + def context(self, msg: dict[str, Any]) -> Context: """Return a context.""" - user = self.user - if user is None: - return Context() - return Context(user_id=user.id) + return Context(user_id=self.user.id) @callback def send_result(self, msg_id: int, result: Any | None = None) -> None: """Send a result message.""" self.send_message(messages.result_message(msg_id, result)) - async def send_big_result(self, msg_id, result): + async def send_big_result(self, msg_id: int, result: Any) -> None: """Send a result message that would be expensive to JSON serialize.""" content = await self.hass.async_add_executor_job( const.JSON_DUMP, messages.result_message(msg_id, result) @@ -57,7 +59,7 @@ class ActiveConnection: self.send_message(messages.error_message(msg_id, code, message)) @callback - def async_handle(self, msg): + def async_handle(self, msg: dict[str, Any]) -> None: """Handle a single incoming message.""" handlers = self.hass.data[const.DOMAIN] @@ -102,13 +104,13 @@ class ActiveConnection: self.last_id = cur_id @callback - def async_close(self): + def async_close(self) -> None: """Close down connection.""" for unsub in self.subscriptions.values(): unsub() @callback - def async_handle_exception(self, msg, err): + def async_handle_exception(self, msg: dict[str, Any], err: Exception) -> None: """Handle an exception while processing a handler.""" log_handler = self.logger.error diff --git a/homeassistant/components/websocket_api/const.py b/homeassistant/components/websocket_api/const.py index 7c3f18f856c..69716b97076 100644 --- a/homeassistant/components/websocket_api/const.py +++ b/homeassistant/components/websocket_api/const.py @@ -1,9 +1,11 @@ """Websocket constants.""" +from __future__ import annotations + import asyncio from concurrent import futures from functools import partial import json -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Awaitable, Callable, Dict, Final from homeassistant.core import HomeAssistant from homeassistant.helpers.json import JSONEncoder @@ -12,37 +14,42 @@ if TYPE_CHECKING: from .connection import ActiveConnection -WebSocketCommandHandler = Callable[[HomeAssistant, "ActiveConnection", dict], None] +WebSocketCommandHandler = Callable[ + [HomeAssistant, "ActiveConnection", Dict[str, Any]], None +] +AsyncWebSocketCommandHandler = Callable[ + [HomeAssistant, "ActiveConnection", Dict[str, Any]], Awaitable[None] +] -DOMAIN = "websocket_api" -URL = "/api/websocket" -PENDING_MSG_PEAK = 512 -PENDING_MSG_PEAK_TIME = 5 -MAX_PENDING_MSG = 2048 +DOMAIN: Final = "websocket_api" +URL: Final = "/api/websocket" +PENDING_MSG_PEAK: Final = 512 +PENDING_MSG_PEAK_TIME: Final = 5 +MAX_PENDING_MSG: Final = 2048 -ERR_ID_REUSE = "id_reuse" -ERR_INVALID_FORMAT = "invalid_format" -ERR_NOT_FOUND = "not_found" -ERR_NOT_SUPPORTED = "not_supported" -ERR_HOME_ASSISTANT_ERROR = "home_assistant_error" -ERR_UNKNOWN_COMMAND = "unknown_command" -ERR_UNKNOWN_ERROR = "unknown_error" -ERR_UNAUTHORIZED = "unauthorized" -ERR_TIMEOUT = "timeout" -ERR_TEMPLATE_ERROR = "template_error" +ERR_ID_REUSE: Final = "id_reuse" +ERR_INVALID_FORMAT: Final = "invalid_format" +ERR_NOT_FOUND: Final = "not_found" +ERR_NOT_SUPPORTED: Final = "not_supported" +ERR_HOME_ASSISTANT_ERROR: Final = "home_assistant_error" +ERR_UNKNOWN_COMMAND: Final = "unknown_command" +ERR_UNKNOWN_ERROR: Final = "unknown_error" +ERR_UNAUTHORIZED: Final = "unauthorized" +ERR_TIMEOUT: Final = "timeout" +ERR_TEMPLATE_ERROR: Final = "template_error" -TYPE_RESULT = "result" +TYPE_RESULT: Final = "result" # Define the possible errors that occur when connections are cancelled. # Originally, this was just asyncio.CancelledError, but issue #9546 showed # that futures.CancelledErrors can also occur in some situations. -CANCELLATION_ERRORS = (asyncio.CancelledError, futures.CancelledError) +CANCELLATION_ERRORS: Final = (asyncio.CancelledError, futures.CancelledError) # Event types -SIGNAL_WEBSOCKET_CONNECTED = "websocket_connected" -SIGNAL_WEBSOCKET_DISCONNECTED = "websocket_disconnected" +SIGNAL_WEBSOCKET_CONNECTED: Final = "websocket_connected" +SIGNAL_WEBSOCKET_DISCONNECTED: Final = "websocket_disconnected" # Data used to store the current connection list -DATA_CONNECTIONS = f"{DOMAIN}.connections" +DATA_CONNECTIONS: Final = f"{DOMAIN}.connections" -JSON_DUMP = partial(json.dumps, cls=JSONEncoder, allow_nan=False) +JSON_DUMP: Final = partial(json.dumps, cls=JSONEncoder, allow_nan=False) diff --git a/homeassistant/components/websocket_api/decorators.py b/homeassistant/components/websocket_api/decorators.py index cbb0e8563c5..af762cf2d46 100644 --- a/homeassistant/components/websocket_api/decorators.py +++ b/homeassistant/components/websocket_api/decorators.py @@ -2,9 +2,10 @@ from __future__ import annotations import asyncio -from collections.abc import Awaitable from functools import wraps -from typing import Callable +from typing import Any, Callable + +import voluptuous as vol from homeassistant.core import HomeAssistant, callback from homeassistant.exceptions import Unauthorized @@ -12,10 +13,13 @@ from homeassistant.exceptions import Unauthorized from . import const, messages from .connection import ActiveConnection -# mypy: allow-untyped-calls, allow-untyped-defs - -async def _handle_async_response(func, hass, connection, msg): +async def _handle_async_response( + func: const.AsyncWebSocketCommandHandler, + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict[str, Any], +) -> None: """Create a response and handle exception.""" try: await func(hass, connection, msg) @@ -24,13 +28,15 @@ async def _handle_async_response(func, hass, connection, msg): def async_response( - func: Callable[[HomeAssistant, ActiveConnection, dict], Awaitable[None]] + func: const.AsyncWebSocketCommandHandler, ) -> const.WebSocketCommandHandler: """Decorate an async function to handle WebSocket API messages.""" @callback @wraps(func) - def schedule_handler(hass, connection, msg): + def schedule_handler( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: """Schedule the handler.""" # As the webserver is now started before the start # event we do not want to block for websocket responders @@ -43,7 +49,9 @@ def require_admin(func: const.WebSocketCommandHandler) -> const.WebSocketCommand """Websocket decorator to require user to be an admin.""" @wraps(func) - def with_admin(hass, connection, msg): + def with_admin( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: """Check admin and call function.""" user = connection.user @@ -56,34 +64,32 @@ def require_admin(func: const.WebSocketCommandHandler) -> const.WebSocketCommand def ws_require_user( - only_owner=False, - only_system_user=False, - allow_system_user=True, - only_active_user=True, - only_inactive_user=False, -): + only_owner: bool = False, + only_system_user: bool = False, + allow_system_user: bool = True, + only_active_user: bool = True, + only_inactive_user: bool = False, +) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Decorate function validating login user exist in current WS connection. Will write out error message if not authenticated. """ - def validator(func): + def validator(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate func.""" @wraps(func) - def check_current_user(hass, connection, msg): + def check_current_user( + hass: HomeAssistant, connection: ActiveConnection, msg: dict[str, Any] + ) -> None: """Check current user.""" - def output_error(message_id, message): + def output_error(message_id: str, message: str) -> None: """Output error message.""" connection.send_message( messages.error_message(msg["id"], message_id, message) ) - if connection.user is None: - output_error("no_user", "Not authenticated as a user") - return - if only_owner and not connection.user.is_owner: output_error("only_owner", "Only allowed as owner") return @@ -112,16 +118,16 @@ def ws_require_user( def websocket_command( - schema: dict, + schema: dict[vol.Marker, Any], ) -> Callable[[const.WebSocketCommandHandler], const.WebSocketCommandHandler]: """Tag a function as a websocket command.""" command = schema["type"] - def decorate(func): + def decorate(func: const.WebSocketCommandHandler) -> const.WebSocketCommandHandler: """Decorate ws command function.""" # pylint: disable=protected-access - func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) - func._ws_command = command + func._ws_schema = messages.BASE_COMMAND_MESSAGE_SCHEMA.extend(schema) # type: ignore[attr-defined] + func._ws_command = command # type: ignore[attr-defined] return func return decorate diff --git a/homeassistant/components/websocket_api/http.py b/homeassistant/components/websocket_api/http.py index a84db598fdc..a80ff111f0d 100644 --- a/homeassistant/components/websocket_api/http.py +++ b/homeassistant/components/websocket_api/http.py @@ -2,15 +2,18 @@ from __future__ import annotations import asyncio +from collections.abc import Callable from contextlib import suppress +import datetime as dt import logging +from typing import Any, Final from aiohttp import WSMsgType, web import async_timeout from homeassistant.components.http import HomeAssistantView from homeassistant.const import EVENT_HOMEASSISTANT_STOP -from homeassistant.core import callback +from homeassistant.core import Event, HomeAssistant, callback from homeassistant.helpers.event import async_call_later from .auth import AuthPhase, auth_required_message @@ -27,16 +30,15 @@ from .const import ( from .error import Disconnect from .messages import message_to_json -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs -_WS_LOGGER = logging.getLogger(f"{__name__}.connection") +_WS_LOGGER: Final = logging.getLogger(f"{__name__}.connection") class WebsocketAPIView(HomeAssistantView): """View to serve a websockets endpoint.""" - name = "websocketapi" - url = URL - requires_auth = False + name: str = "websocketapi" + url: str = URL + requires_auth: bool = False async def get(self, request: web.Request) -> web.WebSocketResponse: """Handle an incoming websocket connection.""" @@ -46,7 +48,7 @@ class WebsocketAPIView(HomeAssistantView): class WebSocketAdapter(logging.LoggerAdapter): """Add connection id to websocket messages.""" - def process(self, msg, kwargs): + def process(self, msg: str, kwargs: Any) -> tuple[str, Any]: """Add connid to websocket log messages.""" return f'[{self.extra["connid"]}] {msg}', kwargs @@ -54,20 +56,21 @@ class WebSocketAdapter(logging.LoggerAdapter): class WebSocketHandler: """Handle an active websocket client connection.""" - def __init__(self, hass, request): + def __init__(self, hass: HomeAssistant, request: web.Request) -> None: """Initialize an active connection.""" self.hass = hass self.request = request self.wsock: web.WebSocketResponse | None = None self._to_write: asyncio.Queue = asyncio.Queue(maxsize=MAX_PENDING_MSG) - self._handle_task = None - self._writer_task = None + self._handle_task: asyncio.Task | None = None + self._writer_task: asyncio.Task | None = None self._logger = WebSocketAdapter(_WS_LOGGER, {"connid": id(self)}) - self._peak_checker_unsub = None + self._peak_checker_unsub: Callable[[], None] | None = None - async def _writer(self): + async def _writer(self) -> None: """Write outgoing messages.""" # Exceptions if Socket disconnected or cancelled by connection handler + assert self.wsock is not None with suppress(RuntimeError, ConnectionResetError, *CANCELLATION_ERRORS): while not self.wsock.closed: message = await self._to_write.get() @@ -78,12 +81,12 @@ class WebSocketHandler: await self.wsock.send_str(message) # Clean up the peaker checker when we shut down the writer - if self._peak_checker_unsub: + if self._peak_checker_unsub is not None: self._peak_checker_unsub() self._peak_checker_unsub = None @callback - def _send_message(self, message): + def _send_message(self, message: str | dict[str, Any]) -> None: """Send a message to the client. Closes connection if the client is not reading the messages. @@ -114,7 +117,7 @@ class WebSocketHandler: ) @callback - def _check_write_peak(self, _): + def _check_write_peak(self, _utc_time: dt.datetime) -> None: """Check that we are no longer above the write peak.""" self._peak_checker_unsub = None @@ -129,10 +132,12 @@ class WebSocketHandler: self._cancel() @callback - def _cancel(self): + def _cancel(self) -> None: """Cancel the connection.""" - self._handle_task.cancel() - self._writer_task.cancel() + if self._handle_task is not None: + self._handle_task.cancel() + if self._writer_task is not None: + self._writer_task.cancel() async def async_handle(self) -> web.WebSocketResponse: """Handle a websocket response.""" @@ -143,7 +148,7 @@ class WebSocketHandler: self._handle_task = asyncio.current_task() @callback - def handle_hass_stop(event): + def handle_hass_stop(event: Event) -> None: """Cancel this connection.""" self._cancel() diff --git a/homeassistant/components/websocket_api/messages.py b/homeassistant/components/websocket_api/messages.py index 736a7ad59f0..8cdda3f8fa3 100644 --- a/homeassistant/components/websocket_api/messages.py +++ b/homeassistant/components/websocket_api/messages.py @@ -3,7 +3,7 @@ from __future__ import annotations from functools import lru_cache import logging -from typing import Any +from typing import Any, Final import voluptuous as vol @@ -17,28 +17,27 @@ from homeassistant.util.yaml.loader import JSON_TYPE from . import const -_LOGGER = logging.getLogger(__name__) -# mypy: allow-untyped-defs +_LOGGER: Final = logging.getLogger(__name__) # Minimal requirements of a message -MINIMAL_MESSAGE_SCHEMA = vol.Schema( +MINIMAL_MESSAGE_SCHEMA: Final = vol.Schema( {vol.Required("id"): cv.positive_int, vol.Required("type"): cv.string}, extra=vol.ALLOW_EXTRA, ) # Base schema to extend by message handlers -BASE_COMMAND_MESSAGE_SCHEMA = vol.Schema({vol.Required("id"): cv.positive_int}) +BASE_COMMAND_MESSAGE_SCHEMA: Final = vol.Schema({vol.Required("id"): cv.positive_int}) -IDEN_TEMPLATE = "__IDEN__" -IDEN_JSON_TEMPLATE = '"__IDEN__"' +IDEN_TEMPLATE: Final = "__IDEN__" +IDEN_JSON_TEMPLATE: Final = '"__IDEN__"' -def result_message(iden: int, result: Any = None) -> dict: +def result_message(iden: int, result: Any = None) -> dict[str, Any]: """Return a success result message.""" return {"id": iden, "type": const.TYPE_RESULT, "success": True, "result": result} -def error_message(iden: int, code: str, message: str) -> dict: +def error_message(iden: int | None, code: str, message: str) -> dict[str, Any]: """Return an error result message.""" return { "id": iden, @@ -48,7 +47,7 @@ def error_message(iden: int, code: str, message: str) -> dict: } -def event_message(iden: JSON_TYPE, event: Any) -> dict: +def event_message(iden: JSON_TYPE, event: Any) -> dict[str, Any]: """Return an event message.""" return {"id": iden, "type": "event", "event": event} @@ -75,7 +74,7 @@ def _cached_event_message(event: Event) -> str: return message_to_json(event_message(IDEN_TEMPLATE, event)) -def message_to_json(message: Any) -> str: +def message_to_json(message: dict[str, Any]) -> str: """Serialize a websocket message to json.""" try: return const.JSON_DUMP(message) diff --git a/homeassistant/components/websocket_api/permissions.py b/homeassistant/components/websocket_api/permissions.py index 010a18f972c..5dade8eeb2a 100644 --- a/homeassistant/components/websocket_api/permissions.py +++ b/homeassistant/components/websocket_api/permissions.py @@ -2,6 +2,10 @@ Separate file to avoid circular imports. """ +from __future__ import annotations + +from typing import Final + from homeassistant.components.frontend import EVENT_PANELS_UPDATED from homeassistant.components.lovelace.const import EVENT_LOVELACE_UPDATED from homeassistant.components.persistent_notification import ( @@ -22,7 +26,7 @@ from homeassistant.helpers.entity_registry import EVENT_ENTITY_REGISTRY_UPDATED # These are events that do not contain any sensitive data # Except for state_changed, which is handled accordingly. -SUBSCRIBE_ALLOWLIST = { +SUBSCRIBE_ALLOWLIST: Final[set[str]] = { EVENT_AREA_REGISTRY_UPDATED, EVENT_COMPONENT_LOADED, EVENT_CORE_CONFIG_UPDATE, diff --git a/homeassistant/components/websocket_api/sensor.py b/homeassistant/components/websocket_api/sensor.py index dfcdc57842e..60d42e97604 100644 --- a/homeassistant/components/websocket_api/sensor.py +++ b/homeassistant/components/websocket_api/sensor.py @@ -1,7 +1,12 @@ """Entity to track connections to websocket API.""" +from __future__ import annotations + +from typing import Any from homeassistant.components.sensor import SensorEntity -from homeassistant.core import callback +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import ConfigType from .const import ( DATA_CONNECTIONS, @@ -9,10 +14,13 @@ from .const import ( SIGNAL_WEBSOCKET_DISCONNECTED, ) -# mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs - -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_platform( + hass: HomeAssistant, + config: ConfigType, + async_add_entities: AddEntitiesCallback, + discovery_info: dict[str, Any] | None = None, +) -> None: """Set up the API streams platform.""" entity = APICount() @@ -22,11 +30,11 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= class APICount(SensorEntity): """Entity to represent how many people are connected to the stream API.""" - def __init__(self): + def __init__(self) -> None: """Initialize the API count.""" self.count = 0 - async def async_added_to_hass(self): + async def async_added_to_hass(self) -> None: """Added to hass.""" self.async_on_remove( self.hass.helpers.dispatcher.async_dispatcher_connect( @@ -40,21 +48,21 @@ class APICount(SensorEntity): ) @property - def name(self): + def name(self) -> str: """Return name of entity.""" return "Connected clients" @property - def state(self): + def state(self) -> int: """Return current API count.""" return self.count @property - def unit_of_measurement(self): + def unit_of_measurement(self) -> str: """Return the unit of measurement.""" return "clients" @callback - def _update_count(self): + def _update_count(self) -> None: self.count = self.hass.data.get(DATA_CONNECTIONS, 0) self.async_write_ha_state() diff --git a/homeassistant/components/wemo/__init__.py b/homeassistant/components/wemo/__init__.py index 6ae016954f2..cb3beb9b67a 100644 --- a/homeassistant/components/wemo/__init__.py +++ b/homeassistant/components/wemo/__init__.py @@ -1,5 +1,6 @@ """Support for WeMo device discovery.""" -import asyncio +from __future__ import annotations + import logging import pywemo @@ -16,9 +17,14 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.event import async_call_later +from homeassistant.util.async_ import gather_with_concurrency from .const import DOMAIN +# Max number of devices to initialize at once. This limit is in place to +# avoid tying up too many executor threads with WeMo device setup. +MAX_CONCURRENCY = 3 + # Mapping from Wemo model_name to domain. WEMO_MODEL_DISPATCH = { "Bridge": LIGHT_DOMAIN, @@ -99,9 +105,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): # Keep track of WeMo device subscriptions for push updates registry = hass.data[DOMAIN]["registry"] = pywemo.SubscriptionRegistry() await hass.async_add_executor_job(registry.start) - + static_conf = config.get(CONF_STATIC, []) wemo_dispatcher = WemoDispatcher(entry) - wemo_discovery = WemoDiscovery(hass, wemo_dispatcher) + wemo_discovery = WemoDiscovery(hass, wemo_dispatcher, static_conf) async def async_stop_wemo(event): """Shutdown Wemo subscriptions and subscription thread on exit.""" @@ -113,17 +119,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, async_stop_wemo) ) - static_conf = config.get(CONF_STATIC, []) - if static_conf: - _LOGGER.debug("Adding statically configured WeMo devices") - for device in await asyncio.gather( - *[ - hass.async_add_executor_job(validate_static_config, host, port) - for host, port in static_conf - ] - ): - if device: - wemo_dispatcher.async_add_unique_device(hass, device) + # Need to do this at least once in case statics are defined and discovery is disabled + await wemo_discovery.discover_statics() if config.get(CONF_DISCOVERY, DEFAULT_DISCOVERY): await wemo_discovery.async_discover_and_schedule() @@ -134,7 +131,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): class WemoDispatcher: """Dispatch WeMo devices to the correct platform.""" - def __init__(self, config_entry: ConfigEntry): + def __init__(self, config_entry: ConfigEntry) -> None: """Initialize the WemoDispatcher.""" self._config_entry = config_entry self._added_serial_numbers = set() @@ -183,12 +180,18 @@ class WemoDiscovery: ADDITIONAL_SECONDS_BETWEEN_SCANS = 10 MAX_SECONDS_BETWEEN_SCANS = 300 - def __init__(self, hass: HomeAssistant, wemo_dispatcher: WemoDispatcher) -> None: + def __init__( + self, + hass: HomeAssistant, + wemo_dispatcher: WemoDispatcher, + static_config: list[tuple[[str, str | None]]], + ) -> None: """Initialize the WemoDiscovery.""" self._hass = hass self._wemo_dispatcher = wemo_dispatcher self._stop = None self._scan_delay = 0 + self._static_config = static_config async def async_discover_and_schedule(self, *_) -> None: """Periodically scan the network looking for WeMo devices.""" @@ -198,6 +201,8 @@ class WemoDiscovery: pywemo.discover_devices ): self._wemo_dispatcher.async_add_unique_device(self._hass, device) + await self.discover_statics() + finally: # Run discovery more frequently after hass has just started. self._scan_delay = min( @@ -217,6 +222,22 @@ class WemoDiscovery: self._stop() self._stop = None + async def discover_statics(self): + """Initialize or Re-Initialize connections to statically configured devices.""" + if self._static_config: + _LOGGER.debug("Adding statically configured WeMo devices") + for device in await gather_with_concurrency( + MAX_CONCURRENCY, + *[ + self._hass.async_add_executor_job( + validate_static_config, host, port + ) + for host, port in self._static_config + ], + ): + if device: + self._wemo_dispatcher.async_add_unique_device(self._hass, device) + def validate_static_config(host, port): """Handle a static config.""" diff --git a/homeassistant/components/wemo/config_flow.py b/homeassistant/components/wemo/config_flow.py index 9ad7dda10ba..b778779ea3c 100644 --- a/homeassistant/components/wemo/config_flow.py +++ b/homeassistant/components/wemo/config_flow.py @@ -2,7 +2,6 @@ import pywemo -from homeassistant import config_entries from homeassistant.helpers import config_entry_flow from . import DOMAIN @@ -13,6 +12,4 @@ async def _async_has_devices(hass): return bool(await hass.async_add_executor_job(pywemo.discover_devices)) -config_entry_flow.register_discovery_flow( - DOMAIN, "Wemo", _async_has_devices, config_entries.CONN_CLASS_LOCAL_PUSH -) +config_entry_flow.register_discovery_flow(DOMAIN, "Wemo", _async_has_devices) diff --git a/homeassistant/components/wemo/entity.py b/homeassistant/components/wemo/entity.py index a315f9daf02..810ad74b953 100644 --- a/homeassistant/components/wemo/entity.py +++ b/homeassistant/components/wemo/entity.py @@ -5,13 +5,12 @@ import asyncio from collections.abc import Generator import contextlib import logging -from typing import Any import async_timeout from pywemo import WeMoDevice from pywemo.exceptions import ActionException -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN as WEMO_DOMAIN @@ -127,7 +126,7 @@ class WemoSubscriptionEntity(WemoEntity): return self.wemo.serialnumber @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "name": self.name, diff --git a/homeassistant/components/wemo/fan.py b/homeassistant/components/wemo/fan.py index a3da5edae76..6910a4c8536 100644 --- a/homeassistant/components/wemo/fan.py +++ b/homeassistant/components/wemo/fan.py @@ -81,7 +81,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ] ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() # This will call WemoHumidifier.set_humidity(target_humidity=VALUE) platform.async_register_entity_service( diff --git a/homeassistant/components/wemo/services.yaml b/homeassistant/components/wemo/services.yaml index c47d666f5c1..e86366b6a5c 100644 --- a/homeassistant/components/wemo/services.yaml +++ b/homeassistant/components/wemo/services.yaml @@ -1,16 +1,26 @@ set_humidity: + name: Set humidity description: Set the target humidity of WeMo humidifier devices. + target: + entity: + integration: wemo + domain: fan fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: "fan.wemo_humidifier" target_humidity: - description: Target humidity. This is a float value between 0 and 100, but will be mapped to the humidity levels that WeMo humidifiers support (45, 50, 55, 60, and 100/Max) by rounding the value down to the nearest supported value. - example: 56.5 + name: Target humidity + description: Target humidity. + required: true + selector: + number: + min: 0 + max: 100 + step: 5 + unit_of_measurement: '%' reset_filter_life: + name: Reset filter life description: Reset the WeMo Humidifier's filter life to 100%. - fields: - entity_id: - description: Names of the WeMo humidifier entities (1 or more entity_ids are required). - example: "fan.wemo_humidifier" + target: + entity: + integration: wemo + domain: fan diff --git a/homeassistant/components/wiffi/config_flow.py b/homeassistant/components/wiffi/config_flow.py index 768f66bf8de..f9456f25df2 100644 --- a/homeassistant/components/wiffi/config_flow.py +++ b/homeassistant/components/wiffi/config_flow.py @@ -18,7 +18,6 @@ class WiffiFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Wiffi server setup config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH @staticmethod @callback diff --git a/homeassistant/components/wilight/config_flow.py b/homeassistant/components/wilight/config_flow.py index 2706db07871..4bfc331a543 100644 --- a/homeassistant/components/wilight/config_flow.py +++ b/homeassistant/components/wilight/config_flow.py @@ -4,7 +4,7 @@ from urllib.parse import urlparse import pywilight from homeassistant.components import ssdp -from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, ConfigFlow +from homeassistant.config_entries import ConfigFlow from homeassistant.const import CONF_HOST from . import DOMAIN @@ -22,7 +22,6 @@ class WiLightFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a WiLight config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the WiLight flow.""" diff --git a/homeassistant/components/wilight/manifest.json b/homeassistant/components/wilight/manifest.json index 689a37f3c91..fec9fdb6c6a 100644 --- a/homeassistant/components/wilight/manifest.json +++ b/homeassistant/components/wilight/manifest.json @@ -3,7 +3,7 @@ "name": "WiLight", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/wilight", - "requirements": ["pywilight==0.0.68"], + "requirements": ["pywilight==0.0.70"], "ssdp": [ { "manufacturer": "All Automacao Ltda" diff --git a/homeassistant/components/wilight/strings.json b/homeassistant/components/wilight/strings.json index 710543a5a53..e267a8e5327 100644 --- a/homeassistant/components/wilight/strings.json +++ b/homeassistant/components/wilight/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "title": "WiLight", diff --git a/homeassistant/components/wilight/translations/ca.json b/homeassistant/components/wilight/translations/ca.json index 5920e54d258..0ace653e050 100644 --- a/homeassistant/components/wilight/translations/ca.json +++ b/homeassistant/components/wilight/translations/ca.json @@ -5,7 +5,7 @@ "not_supported_device": "Actualment aquest WiLight no \u00e9s compatible", "not_wilight_device": "Aquest dispositiu no \u00e9s WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Voleu configurar el WiLight {name}? \n\n Admet: {components}", diff --git a/homeassistant/components/wilight/translations/de.json b/homeassistant/components/wilight/translations/de.json index d56e782279a..59838d9ee86 100644 --- a/homeassistant/components/wilight/translations/de.json +++ b/homeassistant/components/wilight/translations/de.json @@ -6,6 +6,7 @@ "flow_title": "WiLight: {name}", "step": { "confirm": { + "description": "M\u00f6chten Sie WiLight {name} einrichten? \n\n Es unterst\u00fctzt: {components}", "title": "WiLight" } } diff --git a/homeassistant/components/wilight/translations/en.json b/homeassistant/components/wilight/translations/en.json index 14724af4ae7..3d3a83a0270 100644 --- a/homeassistant/components/wilight/translations/en.json +++ b/homeassistant/components/wilight/translations/en.json @@ -5,7 +5,7 @@ "not_supported_device": "This WiLight is currently not supported", "not_wilight_device": "This Device is not WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Do you want to set up WiLight {name}?\n\n It supports: {components}", diff --git a/homeassistant/components/wilight/translations/et.json b/homeassistant/components/wilight/translations/et.json index 9f3236ded62..1be23313837 100644 --- a/homeassistant/components/wilight/translations/et.json +++ b/homeassistant/components/wilight/translations/et.json @@ -5,7 +5,7 @@ "not_supported_device": "Seda WiLight'i sidumist ei toetata", "not_wilight_device": "See seade ei ole WiLight" }, - "flow_title": "", + "flow_title": "{name}", "step": { "confirm": { "description": "Kas soovid seadistada WiLight'i {name} ?\n\n See toetab: {components}", diff --git a/homeassistant/components/wilight/translations/it.json b/homeassistant/components/wilight/translations/it.json index eb9cdcc8a55..84b323a0000 100644 --- a/homeassistant/components/wilight/translations/it.json +++ b/homeassistant/components/wilight/translations/it.json @@ -5,7 +5,7 @@ "not_supported_device": "Questo WiLight non \u00e8 attualmente supportato", "not_wilight_device": "Questo dispositivo non \u00e8 WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Vuoi configurare WiLight {name}? \n\nSupporta: {components}", diff --git a/homeassistant/components/wilight/translations/nl.json b/homeassistant/components/wilight/translations/nl.json index c04105e0878..c2820f0ed6e 100644 --- a/homeassistant/components/wilight/translations/nl.json +++ b/homeassistant/components/wilight/translations/nl.json @@ -5,7 +5,7 @@ "not_supported_device": "Deze WiLight wordt momenteel niet ondersteund", "not_wilight_device": "Dit apparaat is geen WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Wil je WiLight {name} ? \n\n Het ondersteunt: {components}", diff --git a/homeassistant/components/wilight/translations/no.json b/homeassistant/components/wilight/translations/no.json index 89cf91cbe63..170739145da 100644 --- a/homeassistant/components/wilight/translations/no.json +++ b/homeassistant/components/wilight/translations/no.json @@ -5,7 +5,7 @@ "not_supported_device": "Dette WiLight st\u00f8ttes forel\u00f8pig ikke", "not_wilight_device": "Denne enheten er ikke WiLight" }, - "flow_title": "", + "flow_title": "{name}", "step": { "confirm": { "description": "Vil du konfigurere WiLight {name} ? \n\n Den st\u00f8tter: {components}", diff --git a/homeassistant/components/wilight/translations/pl.json b/homeassistant/components/wilight/translations/pl.json index 45c3d1f8990..93957d016f5 100644 --- a/homeassistant/components/wilight/translations/pl.json +++ b/homeassistant/components/wilight/translations/pl.json @@ -5,7 +5,7 @@ "not_supported_device": "Ten WiLight nie jest obecnie obs\u0142ugiwany", "not_wilight_device": "To urz\u0105dzenie nie jest urz\u0105dzeniem WiLight" }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "Czy chcesz skonfigurowa\u0107 WiLight {name}?\n\nObs\u0142uguje: {components}", diff --git a/homeassistant/components/wilight/translations/ru.json b/homeassistant/components/wilight/translations/ru.json index 9842b810f38..7d04a13518d 100644 --- a/homeassistant/components/wilight/translations/ru.json +++ b/homeassistant/components/wilight/translations/ru.json @@ -5,7 +5,7 @@ "not_supported_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0432 \u043d\u0430\u0441\u0442\u043e\u044f\u0449\u0435\u0435 \u0432\u0440\u0435\u043c\u044f \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f.", "not_wilight_device": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043d\u0435 WiLight." }, - "flow_title": "WiLight: {name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u0412\u044b \u0443\u0432\u0435\u0440\u0435\u043d\u044b, \u0447\u0442\u043e \u0445\u043e\u0442\u0438\u0442\u0435 \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c WiLight {name}? \n\n\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442: {components}", diff --git a/homeassistant/components/wilight/translations/zh-Hant.json b/homeassistant/components/wilight/translations/zh-Hant.json index fed2fb77904..fe6c36f21d2 100644 --- a/homeassistant/components/wilight/translations/zh-Hant.json +++ b/homeassistant/components/wilight/translations/zh-Hant.json @@ -5,7 +5,7 @@ "not_supported_device": "\u4e0d\u652f\u63f4\u6b64\u6b3e WiLight \u88dd\u7f6e\u3002", "not_wilight_device": "\u6b64\u88dd\u7f6e\u4e26\u975e WiLight" }, - "flow_title": "WiLight\uff1a{name}", + "flow_title": "{name}", "step": { "confirm": { "description": "\u662f\u5426\u8981\u8a2d\u5b9a WiLight {name}\uff1f\n\n\u652f\u63f4\uff1a{components}", diff --git a/homeassistant/components/wink/services.yaml b/homeassistant/components/wink/services.yaml index ac050fd0087..851f3bb9a43 100644 --- a/homeassistant/components/wink/services.yaml +++ b/homeassistant/components/wink/services.yaml @@ -1,227 +1,431 @@ # Describes the format for available Wink services pair_new_device: + name: Pair new device description: Pair a new device to a Wink Hub. fields: hub_name: + name: Hub name description: The name of the hub to pair a new device to. + required: true example: "My hub" + selector: + text: pairing_mode: - description: One of ["zigbee", "zwave", "zwave_exclusion", "zwave_network_rediscovery", "lutron", "bluetooth", "kidde"]. - example: "zigbee" + name: Pairing mode + description: Mode. + required: true + selector: + select: + options: + - 'bluetooth' + - 'kidde' + - 'lutron' + - 'zigbee' + - 'zwave' + - 'zwave_exclusion' + - 'zwave_network_rediscovery' kidde_radio_code: + name: Kidde radio code description: "A string of 8 1s and 0s one for each dip switch on the kidde device left --> right = 1 --> 8. Down = 1 and Up = 0" example: "10101010" + selector: + text: rename_wink_device: + name: Rename wink device description: Rename the provided device. + target: + entity: + integration: wink fields: - entity_id: - description: The entity_id of the device to rename. - example: binary_sensor.front_door_opened name: + name: Name description: The name to change it to. + required: true example: back_door + selector: + text: delete_wink_device: + name: Delete wink device description: Remove/unpair device from Wink. - fields: - entity_id: - description: The entity_id of the device to delete. + target: + entity: + integration: wink pull_newly_added_devices_from_wink: + name: Pull newly added devices from wink description: Pull newly paired devices from Wink. refresh_state_from_wink: + name: Refresh state from wink description: Pull the latest states for every device. set_siren_volume: + name: Set siren volume description: Set the volume of the siren for a Dome siren/chime. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" volume: - description: Volume level. One of ["low", "medium", "high"]. - example: "high" + name: Volume + description: Volume level. + required: true + selector: + select: + options: + - 'low' + - 'medium' + - 'high' enable_chime: + name: Enable chime description: Enable the chime of a Dome siren with the provided sound. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" tone: + name: Tone description: >- - The tone to use for the chime. One of ["doorbell", "fur_elise", - "doorbell_extended", "alert", "william_tell", "rondo_alla_turca", - "police_siren", "evacuation", "beep_beep", "beep", "inactive"] - example: "doorbell" + The tone to use for the chime. + required: true + selector: + select: + options: + - 'alert' + - 'beep' + - 'beep_beep' + - 'doorbell' + - 'doorbell_extended' + - 'evacuation' + - 'fur_elise' + - 'inactive' + - 'police_siren' + - 'rondo_alla_turca' + - 'william_tell' set_siren_tone: + name: Set siren tone description: Set the sound to use when the siren is enabled. (This doesn't enable the siren) + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" tone: + name: Tone description: >- - The tone to use for the chime. One of ["doorbell", "fur_elise", - "doorbell_extended", "alert", "william_tell", "rondo_alla_turca", - "police_siren", "evacuation", "beep_beep", "beep", "inactive"] - example: "alert" + The tone to use for the chime. + required: true + selector: + select: + options: + - 'alert' + - 'beep' + - 'beep_beep' + - 'doorbell' + - 'doorbell_extended' + - 'evacuation' + - 'fur_elise' + - 'inactive' + - 'police_siren' + - 'rondo_alla_turca' + - 'william_tell' siren_set_auto_shutoff: + name: Siren set auto shutoff description: How long to sound the siren before turning off. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" auto_shutoff: + name: Auto shutoff description: >- - The time in seconds to sound the siren. One of [None, -1, 30, 60, 120] - (None and -1 are forever. Use None for gocontrol, and -1 for Dome) - example: 60 + The time in seconds to sound the siren. (None and -1 are forever. Use None for gocontrol, and -1 for Dome) + required: true + selector: + select: + options: + - 'None' + - '-1' + - '30' + - '60' + - '120' set_siren_strobe_enabled: + name: Set siren strobe enabled description: Enable or disable the strobe light when the siren is sounding. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" enabled: + name: Enabled description: "True or False" + required: true + selector: + boolean: set_chime_strobe_enabled: + name: Set chime strobe enabled description: Enable or disable the strobe light when the chime is sounding. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" enabled: + name: Enabled description: "True or False" + required: true + selector: + boolean: enable_siren: + name: Enable siren description: Enable/disable the siren. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set - example: "switch.dome_siren" enabled: + name: Enabled description: "true or false" + required: true + selector: + boolean: set_chime_volume: + name: Set chime volume description: Set the volume of the chime for a Dome siren/chime. + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name(s) of the entities to set. - example: "switch.dome_siren" volume: - description: Volume level. One of ["low", "medium", "high"] - example: "low" + name: Volume + description: Volume level. + required: true + selector: + select: + options: + - 'low' + - 'medium' + - 'high' set_nimbus_dial_configuration: + name: Set nimbus dial configuration description: Set the configuration of an individual nimbus dial + target: + entity: + integration: wink + domain: switch fields: - entity_id: - description: Name of the entity to set. - example: "wink.nimbus_dial_3" rotation: - description: Direction dial hand should spin ["cw" or "ccw"] - example: "cw" + name: Rotation + description: Direction dial hand should spin. + selector: + select: + options: + - 'cw' + - 'ccw' ticks: + name: Ticks description: Number of times the hand should move - example: 12 + selector: + number: + min: 0 + max: 3600 scale: - description: How the dial should move in response to higher values ["log" or "linear"] - example: "linear" + name: Scale + description: How the dial should move in response to higher values. + selector: + select: + options: + - 'linear' + - 'log' min_value: + name: minimum value description: The minimum value allowed to be set example: 0 + selector: + text: max_value: + name: Maximum value description: The maximum value allowed to be set example: 500 + selector: + text: min_position: - description: The minimum position the dial hand can rotate to generally [0-360] - example: 0 + name: Minimum position + description: The minimum position the dial hand can rotate to generally. + selector: + number: + min: 0 + max: 360 max_position: - description: The maximum position the dial hand can rotate to generally [0-360] - example: 360 + name: Maximum position + description: The maximum position the dial hand can rotate to generally. + selector: + number: + min: 0 + max: 360 set_nimbus_dial_state: + name: Set nimbus dial state description: Set the value and labels of an individual nimbus dial + target: + entity: + integration: wink fields: - entity_id: - description: Name of the entity to set. - example: "wink.nimbus_dial_3" value: + name: Value description: The value that should be set (Should be between min_value and max_value) + required: true example: 250 + selector: + text: labels: + name: Labels description: >- The values shown on the dial labels ["Dial 1", "test"] the first value is what is shown by default the second value is shown when the nimbus is pressed. example: ["example", "test"] + selector: + object: set_lock_vacation_mode: + name: Set lock vacation mode description: Set vacation mode for all or specified locks. Disables all user codes. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock enabled: + name: Enabled description: enable or disable. true or false. - example: true + required: true + selector: + boolean: set_lock_alarm_mode: + name: Set lock alarm mode description: Set alarm mode for all or specified locks. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock mode: - description: One of tamper, activity, or forced_entry. - example: tamper + name: Mode + description: Select mode. + required: true + selector: + select: + options: + - 'activity' + - 'forced_entry' + - 'tamper' set_lock_alarm_sensitivity: + name: Set lock alarm sensitivity description: Set alarm sensitivity for all or specified locks. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock sensitivity: - description: One of low, medium_low, medium, medium_high, high. - example: medium + name: Sensitivity + description: Choose the sensitivity. + required: true + selector: + select: + options: + - 'low' + - 'medium_low' + - 'medium' + - 'medium_high' + - 'high' set_lock_alarm_state: + name: Set lok alarm state description: Set alarm state. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock enabled: - description: enable or disable. true or false. - example: true + name: Enabled + description: enable or disable. + required: true + selector: + boolean: set_lock_beeper_state: + name: Set lock beeper state description: Set beeper state. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock enabled: - description: enable or disable. true or false. - example: true + name: Enabled + description: enable or disable. + required: true + selector: + boolean: add_new_lock_key_code: + name: Add new lock key code description: Add a new user key code. fields: entity_id: + name: Entity description: Name of lock to unlock. - example: "lock.front_door" + selector: + entity: + integration: wink + domain: lock name: + name: Name description: name of the new key code. + required: true example: Bob + selector: + text: code: + name: Code description: new key code, length must match length of other codes. Default length is 4. + required: true example: 1234 + selector: + text: diff --git a/homeassistant/components/withings/binary_sensor.py b/homeassistant/components/withings/binary_sensor.py index a7d0a80e8e3..ecb52530d7e 100644 --- a/homeassistant/components/withings/binary_sensor.py +++ b/homeassistant/components/withings/binary_sensor.py @@ -1,8 +1,6 @@ """Sensors flow for Withings.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.binary_sensor import ( DEVICE_CLASS_OCCUPANCY, DOMAIN as BINARY_SENSOR_DOMAIN, @@ -10,7 +8,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import BaseWithingsSensor, async_create_entities @@ -18,7 +16,7 @@ from .common import BaseWithingsSensor, async_create_entities async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" entities = await async_create_entities( diff --git a/homeassistant/components/withings/common.py b/homeassistant/components/withings/common.py index c2a91275d72..a187786c995 100644 --- a/homeassistant/components/withings/common.py +++ b/homeassistant/components/withings/common.py @@ -480,7 +480,7 @@ class ConfigEntryWithingsApi(AbstractWithingsApi): hass: HomeAssistant, config_entry: ConfigEntry, implementation: AbstractOAuth2Implementation, - ): + ) -> None: """Initialize object.""" self._hass = hass self._config_entry = config_entry @@ -564,7 +564,7 @@ class DataManager: api: ConfigEntryWithingsApi, user_id: int, webhook_config: WebhookConfig, - ): + ) -> None: """Initialize the data manager.""" self._hass = hass self._api = api diff --git a/homeassistant/components/withings/config_flow.py b/homeassistant/components/withings/config_flow.py index f841c61fbcb..29c1e162ed4 100644 --- a/homeassistant/components/withings/config_flow.py +++ b/homeassistant/components/withings/config_flow.py @@ -6,7 +6,6 @@ import logging import voluptuous as vol from withings_api.common import AuthScope -from homeassistant import config_entries from homeassistant.components.withings import const from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.helpers import config_entry_oauth2_flow @@ -19,7 +18,7 @@ class WithingsFlowHandler( """Handle a config flow.""" DOMAIN = const.DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL + # Temporarily holds authorization data during the profile step. _current_data: dict[str, None | str | int] = {} @@ -60,7 +59,7 @@ class WithingsFlowHandler( if profile: existing_entries = [ config_entry - for config_entry in self.hass.config_entries.async_entries(const.DOMAIN) + for config_entry in self._async_current_entries() if slugify(config_entry.data.get(const.PROFILE)) == slugify(profile) ] diff --git a/homeassistant/components/withings/sensor.py b/homeassistant/components/withings/sensor.py index e26804f1f0a..ca7391eb58e 100644 --- a/homeassistant/components/withings/sensor.py +++ b/homeassistant/components/withings/sensor.py @@ -1,12 +1,10 @@ """Sensors flow for Withings.""" from __future__ import annotations -from typing import Callable - from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .common import BaseWithingsSensor, async_create_entities @@ -14,7 +12,7 @@ from .common import BaseWithingsSensor, async_create_entities async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the sensor config entry.""" diff --git a/homeassistant/components/withings/strings.json b/homeassistant/components/withings/strings.json index db18bb0f334..81b0bbb79b5 100644 --- a/homeassistant/components/withings/strings.json +++ b/homeassistant/components/withings/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "profile": { "title": "User Profile.", diff --git a/homeassistant/components/withings/translations/ca.json b/homeassistant/components/withings/translations/ca.json index 2d299c659ef..b548735d426 100644 --- a/homeassistant/components/withings/translations/ca.json +++ b/homeassistant/components/withings/translations/ca.json @@ -12,7 +12,7 @@ "error": { "already_configured": "El compte ja ha estat configurat" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Selecciona el m\u00e8tode d'autenticaci\u00f3" diff --git a/homeassistant/components/withings/translations/de.json b/homeassistant/components/withings/translations/de.json index b4bd6a0c449..7d0aead7a66 100644 --- a/homeassistant/components/withings/translations/de.json +++ b/homeassistant/components/withings/translations/de.json @@ -21,7 +21,7 @@ "data": { "profile": "Profilname" }, - "description": "Welches Profil hast du auf der Withings-Website ausgew\u00e4hlt? Es ist wichtig, dass die Profile \u00fcbereinstimmen, da sonst die Daten falsch beschriftet werden.", + "description": "Gib einen eindeutigen Profilnamen f\u00fcr diese Daten an. Normalerweise ist dies der Name des Profils, das du im vorherigen Schritt ausgew\u00e4hlt hast.", "title": "Benutzerprofil" }, "reauth": { diff --git a/homeassistant/components/withings/translations/en.json b/homeassistant/components/withings/translations/en.json index 45d1a642da3..e8acc8c3440 100644 --- a/homeassistant/components/withings/translations/en.json +++ b/homeassistant/components/withings/translations/en.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Account is already configured" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Pick Authentication Method" diff --git a/homeassistant/components/withings/translations/et.json b/homeassistant/components/withings/translations/et.json index 3cb42f8cf7e..5395069522f 100644 --- a/homeassistant/components/withings/translations/et.json +++ b/homeassistant/components/withings/translations/et.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Konto on juba seadistatud" }, - "flow_title": "", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Vali tuvastusmeetod" diff --git a/homeassistant/components/withings/translations/it.json b/homeassistant/components/withings/translations/it.json index 8fb4dee9918..77e29c71e0c 100644 --- a/homeassistant/components/withings/translations/it.json +++ b/homeassistant/components/withings/translations/it.json @@ -12,7 +12,7 @@ "error": { "already_configured": "L'account \u00e8 gi\u00e0 configurato" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Scegli il metodo di autenticazione" diff --git a/homeassistant/components/withings/translations/nl.json b/homeassistant/components/withings/translations/nl.json index d5a8d623349..a7400e3a693 100644 --- a/homeassistant/components/withings/translations/nl.json +++ b/homeassistant/components/withings/translations/nl.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Account is al geconfigureerd" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Kies een authenticatie methode" diff --git a/homeassistant/components/withings/translations/no.json b/homeassistant/components/withings/translations/no.json index 2dd7407ad92..ff8d18eec64 100644 --- a/homeassistant/components/withings/translations/no.json +++ b/homeassistant/components/withings/translations/no.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Kontoen er allerede konfigurert" }, - "flow_title": "", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Velg godkjenningsmetode" diff --git a/homeassistant/components/withings/translations/pl.json b/homeassistant/components/withings/translations/pl.json index 0eeac7899ae..638171af846 100644 --- a/homeassistant/components/withings/translations/pl.json +++ b/homeassistant/components/withings/translations/pl.json @@ -12,7 +12,7 @@ "error": { "already_configured": "Konto jest ju\u017c skonfigurowane" }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "Wybierz metod\u0119 uwierzytelniania" diff --git a/homeassistant/components/withings/translations/ru.json b/homeassistant/components/withings/translations/ru.json index d8cfd6c0b3b..7127f9545fa 100644 --- a/homeassistant/components/withings/translations/ru.json +++ b/homeassistant/components/withings/translations/ru.json @@ -3,7 +3,7 @@ "abort": { "already_configured": "\u041e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0430 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044f \u0434\u043b\u044f \u043f\u0440\u043e\u0444\u0438\u043b\u044f.", "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "no_url_available": "URL-\u0430\u0434\u0440\u0435\u0441 \u043d\u0435\u0434\u043e\u0441\u0442\u0443\u043f\u0435\u043d. \u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439]({docs_url}) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e\u0431 \u044d\u0442\u043e\u0439 \u043e\u0448\u0438\u0431\u043a\u0435." }, "create_entry": { @@ -12,7 +12,7 @@ "error": { "already_configured": "\u042d\u0442\u0430 \u0443\u0447\u0451\u0442\u043d\u0430\u044f \u0437\u0430\u043f\u0438\u0441\u044c \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u0430 \u0432 Home Assistant." }, - "flow_title": "Withings: {profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u0441\u043f\u043e\u0441\u043e\u0431 \u0430\u0443\u0442\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438" diff --git a/homeassistant/components/withings/translations/zh-Hant.json b/homeassistant/components/withings/translations/zh-Hant.json index cd917f42b47..2ee7ce3d3da 100644 --- a/homeassistant/components/withings/translations/zh-Hant.json +++ b/homeassistant/components/withings/translations/zh-Hant.json @@ -12,7 +12,7 @@ "error": { "already_configured": "\u5e33\u865f\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210" }, - "flow_title": "Withings\uff1a{profile}", + "flow_title": "{profile}", "step": { "pick_implementation": { "title": "\u9078\u64c7\u9a57\u8b49\u6a21\u5f0f" diff --git a/homeassistant/components/wled/__init__.py b/homeassistant/components/wled/__init__.py index 8c8c6d887e7..b44df82f889 100644 --- a/homeassistant/components/wled/__init__.py +++ b/homeassistant/components/wled/__init__.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Any from wled import WLED, Device as WLEDDevice, WLEDConnectionError, WLEDError @@ -11,22 +10,24 @@ from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.config_entries import ConfigEntry -from homeassistant.const import ATTR_NAME, CONF_HOST +from homeassistant.const import ( + ATTR_IDENTIFIERS, + ATTR_MANUFACTURER, + ATTR_MODEL, + ATTR_NAME, + ATTR_SW_VERSION, + CONF_HOST, +) from homeassistant.core import HomeAssistant from homeassistant.helpers.aiohttp_client import async_get_clientsession +from homeassistant.helpers.entity import DeviceInfo from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, DataUpdateCoordinator, UpdateFailed, ) -from .const import ( - ATTR_IDENTIFIERS, - ATTR_MANUFACTURER, - ATTR_MODEL, - ATTR_SOFTWARE_VERSION, - DOMAIN, -) +from .const import DOMAIN SCAN_INTERVAL = timedelta(seconds=5) PLATFORMS = (LIGHT_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN) @@ -101,7 +102,7 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): hass: HomeAssistant, *, host: str, - ): + ) -> None: """Initialize global WLED data updater.""" self.wled = WLED(host, session=async_get_clientsession(hass)) @@ -128,49 +129,15 @@ class WLEDDataUpdateCoordinator(DataUpdateCoordinator[WLEDDevice]): class WLEDEntity(CoordinatorEntity): """Defines a base WLED entity.""" - def __init__( - self, - *, - entry_id: str, - coordinator: WLEDDataUpdateCoordinator, - name: str, - icon: str, - enabled_default: bool = True, - ) -> None: - """Initialize the WLED entity.""" - super().__init__(coordinator) - self._enabled_default = enabled_default - self._entry_id = entry_id - self._icon = icon - self._name = name - self._unsub_dispatcher = None + coordinator: WLEDDataUpdateCoordinator @property - def name(self) -> str: - """Return the name of the entity.""" - return self._name - - @property - def icon(self) -> str: - """Return the mdi icon of the entity.""" - return self._icon - - @property - def entity_registry_enabled_default(self) -> bool: - """Return if the entity should be enabled when first added to the entity registry.""" - return self._enabled_default - - -class WLEDDeviceEntity(WLEDEntity): - """Defines a WLED device entity.""" - - @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> DeviceInfo: """Return device information about this WLED device.""" return { ATTR_IDENTIFIERS: {(DOMAIN, self.coordinator.data.info.mac_address)}, ATTR_NAME: self.coordinator.data.info.name, ATTR_MANUFACTURER: self.coordinator.data.info.brand, ATTR_MODEL: self.coordinator.data.info.product, - ATTR_SOFTWARE_VERSION: self.coordinator.data.info.version, + ATTR_SW_VERSION: self.coordinator.data.info.version, } diff --git a/homeassistant/components/wled/config_flow.py b/homeassistant/components/wled/config_flow.py index e3f0d8224e6..bb9d4c0cfe5 100644 --- a/homeassistant/components/wled/config_flow.py +++ b/homeassistant/components/wled/config_flow.py @@ -1,18 +1,16 @@ """Config flow to configure the WLED integration.""" from __future__ import annotations +from typing import Any + import voluptuous as vol from wled import WLED, WLEDConnectionError -from homeassistant.config_entries import ( - CONN_CLASS_LOCAL_POLL, - SOURCE_ZEROCONF, - ConfigFlow, -) +from homeassistant.config_entries import SOURCE_ZEROCONF, ConfigFlow from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.typing import ConfigType +from homeassistant.helpers.typing import DiscoveryInfoType from .const import DOMAIN @@ -21,18 +19,17 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a WLED config flow.""" VERSION = 1 - CONNECTION_CLASS = CONN_CLASS_LOCAL_POLL - async def async_step_user(self, user_input: ConfigType | None = None) -> FlowResult: + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle a flow initiated by the user.""" return await self._handle_config_flow(user_input) async def async_step_zeroconf( - self, discovery_info: ConfigType | None = None + self, discovery_info: DiscoveryInfoType ) -> FlowResult: """Handle zeroconf discovery.""" - if discovery_info is None: - return self.async_abort(reason="cannot_connect") # Hostname is format: wled-livingroom.local. host = discovery_info["hostname"].rstrip(".") @@ -51,13 +48,13 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return await self._handle_config_flow(discovery_info, True) async def async_step_zeroconf_confirm( - self, user_input: ConfigType = None + self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Handle a flow initiated by zeroconf.""" return await self._handle_config_flow(user_input) async def _handle_config_flow( - self, user_input: ConfigType | None = None, prepare: bool = False + self, user_input: dict[str, Any] | None = None, prepare: bool = False ) -> FlowResult: """Config flow handler for WLED.""" source = self.context.get("source") @@ -68,6 +65,9 @@ class WLEDFlowHandler(ConfigFlow, domain=DOMAIN): return self._show_confirm_dialog() return self._show_setup_form() + # if prepare is True, user_input can not be None. + assert user_input is not None + if source == SOURCE_ZEROCONF: user_input[CONF_HOST] = self.context.get(CONF_HOST) user_input[CONF_MAC] = self.context.get(CONF_MAC) diff --git a/homeassistant/components/wled/const.py b/homeassistant/components/wled/const.py index e0880dd40fd..7cc52601d79 100644 --- a/homeassistant/components/wled/const.py +++ b/homeassistant/components/wled/const.py @@ -7,12 +7,9 @@ DOMAIN = "wled" ATTR_COLOR_PRIMARY = "color_primary" ATTR_DURATION = "duration" ATTR_FADE = "fade" -ATTR_IDENTIFIERS = "identifiers" ATTR_INTENSITY = "intensity" ATTR_LED_COUNT = "led_count" -ATTR_MANUFACTURER = "manufacturer" ATTR_MAX_POWER = "max_power" -ATTR_MODEL = "model" ATTR_ON = "on" ATTR_PALETTE = "palette" ATTR_PLAYLIST = "playlist" diff --git a/homeassistant/components/wled/light.py b/homeassistant/components/wled/light.py index 9d25a1bcbcf..20960ad0bb7 100644 --- a/homeassistant/components/wled/light.py +++ b/homeassistant/components/wled/light.py @@ -2,7 +2,7 @@ from __future__ import annotations from functools import partial -from typing import Any, Callable +from typing import Any import voluptuous as vol @@ -24,13 +24,13 @@ from homeassistant.components.light import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_registry import ( async_get_registry as async_get_entity_registry, ) import homeassistant.util.color as color_util -from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler +from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( ATTR_COLOR_PRIMARY, ATTR_INTENSITY, @@ -52,12 +52,12 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up WLED light based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_EFFECT, @@ -93,27 +93,17 @@ async def async_setup_entry( update_segments() -class WLEDMasterLight(LightEntity, WLEDDeviceEntity): +class WLEDMasterLight(WLEDEntity, LightEntity): """Defines a WLED master light.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator): + _attr_supported_features = SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + _attr_icon = "mdi:led-strip-variant" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED master light.""" - super().__init__( - entry_id=entry_id, - coordinator=coordinator, - name=f"{coordinator.data.info.name} Master", - icon="mdi:led-strip-variant", - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}" - - @property - def supported_features(self) -> int: - """Flag supported features.""" - return SUPPORT_BRIGHTNESS | SUPPORT_TRANSITION + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Master" + self._attr_unique_id = coordinator.data.info.mac_address @property def brightness(self) -> int | None: @@ -128,7 +118,7 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data = {ATTR_ON: False} + data: dict[str, bool | int] = {ATTR_ON: False} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -139,7 +129,7 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data = {ATTR_ON: True} + data: dict[str, bool | int] = {ATTR_ON: True} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -172,33 +162,26 @@ class WLEDMasterLight(LightEntity, WLEDDeviceEntity): await self.coordinator.wled.preset(**data) -class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): +class WLEDSegmentLight(WLEDEntity, LightEntity): """Defines a WLED light based on a segment.""" - def __init__( - self, entry_id: str, coordinator: WLEDDataUpdateCoordinator, segment: int - ): + _attr_icon = "mdi:led-strip-variant" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator, segment: int) -> None: """Initialize WLED segment light.""" + super().__init__(coordinator=coordinator) self._rgbw = coordinator.data.info.leds.rgbw self._segment = segment # If this is the one and only segment, use a simpler name - name = f"{coordinator.data.info.name} Segment {self._segment}" + self._attr_name = f"{coordinator.data.info.name} Segment {segment}" if len(coordinator.data.state.segments) == 1: - name = coordinator.data.info.name + self._attr_name = coordinator.data.info.name - super().__init__( - entry_id=entry_id, - coordinator=coordinator, - name=name, - icon="mdi:led-strip-variant", + self._attr_unique_id = ( + f"{self.coordinator.data.info.mac_address}_{self._segment}" ) - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}_{self._segment}" - @property def available(self) -> bool: """Return True if entity is available.""" @@ -231,7 +214,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): } @property - def hs_color(self) -> tuple[float, float] | None: + def hs_color(self) -> tuple[float, float]: """Return the hue and saturation color value [float, float].""" color = self.coordinator.data.state.segments[self._segment].color_primary return color_util.color_RGB_to_hs(*color[:3]) @@ -296,7 +279,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_off(self, **kwargs: Any) -> None: """Turn off the light.""" - data = {ATTR_ON: False} + data: dict[str, bool | int] = {ATTR_ON: False} if ATTR_TRANSITION in kwargs: # WLED uses 100ms per unit, so 10 = 1 second. @@ -313,7 +296,10 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): @wled_exception_handler async def async_turn_on(self, **kwargs: Any) -> None: """Turn on the light.""" - data = {ATTR_ON: True, ATTR_SEGMENT_ID: self._segment} + data: dict[str, Any] = { + ATTR_ON: True, + ATTR_SEGMENT_ID: self._segment, + } if ATTR_COLOR_TEMP in kwargs: mireds = color_util.color_temperature_kelvin_to_mired( @@ -386,7 +372,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): speed: int | None = None, ) -> None: """Set the effect of a WLED light.""" - data = {ATTR_SEGMENT_ID: self._segment} + data: dict[str, bool | int | str | None] = {ATTR_SEGMENT_ID: self._segment} if effect is not None: data[ATTR_EFFECT] = effect @@ -420,7 +406,7 @@ class WLEDSegmentLight(LightEntity, WLEDDeviceEntity): def async_update_segments( entry: ConfigEntry, coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDSegmentLight], + current: dict[int, WLEDSegmentLight | WLEDMasterLight], async_add_entities, ) -> None: """Update segments.""" @@ -433,12 +419,12 @@ def async_update_segments( # Process new segments, add them to Home Assistant new_entities = [] for segment_id in segment_ids - current_ids: - current[segment_id] = WLEDSegmentLight(entry.entry_id, coordinator, segment_id) + current[segment_id] = WLEDSegmentLight(coordinator, segment_id) new_entities.append(current[segment_id]) # More than 1 segment now? Add master controls if len(current_ids) < 2 and len(segment_ids) > 1: - current[-1] = WLEDMasterLight(entry.entry_id, coordinator) + current[-1] = WLEDMasterLight(coordinator) new_entities.append(current[-1]) if new_entities: @@ -460,7 +446,7 @@ def async_update_segments( async def async_remove_entity( index: int, coordinator: WLEDDataUpdateCoordinator, - current: dict[int, WLEDSegmentLight], + current: dict[int, WLEDSegmentLight | WLEDMasterLight], ) -> None: """Remove WLED segment light from Home Assistant.""" entity = current[index] diff --git a/homeassistant/components/wled/sensor.py b/homeassistant/components/wled/sensor.py index 96c79452790..73c012f25c7 100644 --- a/homeassistant/components/wled/sensor.py +++ b/homeassistant/components/wled/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations from datetime import timedelta -from typing import Any, Callable +from typing import Any from homeassistant.components.sensor import DEVICE_CLASS_CURRENT, SensorEntity from homeassistant.config_entries import ConfigEntry @@ -14,84 +14,46 @@ from homeassistant.const import ( SIGNAL_STRENGTH_DECIBELS_MILLIWATT, ) from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.dt import utcnow -from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity +from . import WLEDDataUpdateCoordinator, WLEDEntity from .const import ATTR_LED_COUNT, ATTR_MAX_POWER, CURRENT_MA, DOMAIN async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up WLED sensor based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] sensors = [ - WLEDEstimatedCurrentSensor(entry.entry_id, coordinator), - WLEDUptimeSensor(entry.entry_id, coordinator), - WLEDFreeHeapSensor(entry.entry_id, coordinator), - WLEDWifiBSSIDSensor(entry.entry_id, coordinator), - WLEDWifiChannelSensor(entry.entry_id, coordinator), - WLEDWifiRSSISensor(entry.entry_id, coordinator), - WLEDWifiSignalSensor(entry.entry_id, coordinator), + WLEDEstimatedCurrentSensor(coordinator), + WLEDUptimeSensor(coordinator), + WLEDFreeHeapSensor(coordinator), + WLEDWifiBSSIDSensor(coordinator), + WLEDWifiChannelSensor(coordinator), + WLEDWifiRSSISensor(coordinator), + WLEDWifiSignalSensor(coordinator), ] async_add_entities(sensors, True) -class WLEDSensor(WLEDDeviceEntity, SensorEntity): - """Defines a WLED sensor.""" - - def __init__( - self, - *, - coordinator: WLEDDataUpdateCoordinator, - enabled_default: bool = True, - entry_id: str, - icon: str, - key: str, - name: str, - unit_of_measurement: str | None = None, - ) -> None: - """Initialize WLED sensor.""" - self._unit_of_measurement = unit_of_measurement - self._key = key - - super().__init__( - entry_id=entry_id, - coordinator=coordinator, - name=name, - icon=icon, - enabled_default=enabled_default, - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}_{self._key}" - - @property - def unit_of_measurement(self) -> str: - """Return the unit this state is expressed in.""" - return self._unit_of_measurement - - -class WLEDEstimatedCurrentSensor(WLEDSensor): +class WLEDEstimatedCurrentSensor(WLEDEntity, SensorEntity): """Defines a WLED estimated current sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:power" + _attr_unit_of_measurement = CURRENT_MA + _attr_device_class = DEVICE_CLASS_CURRENT + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED estimated current sensor.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:power", - key="estimated_current", - name=f"{coordinator.data.info.name} Estimated Current", - unit_of_measurement=CURRENT_MA, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Estimated Current" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_estimated_current" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -106,25 +68,18 @@ class WLEDEstimatedCurrentSensor(WLEDSensor): """Return the state of the sensor.""" return self.coordinator.data.info.leds.power - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_CURRENT - -class WLEDUptimeSensor(WLEDSensor): +class WLEDUptimeSensor(WLEDEntity, SensorEntity): """Defines a WLED uptime sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_device_class = DEVICE_CLASS_TIMESTAMP + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED uptime sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:clock-outline", - key="uptime", - name=f"{coordinator.data.info.name} Uptime", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Uptime" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_uptime" @property def state(self) -> str: @@ -132,26 +87,19 @@ class WLEDUptimeSensor(WLEDSensor): uptime = utcnow() - timedelta(seconds=self.coordinator.data.info.uptime) return uptime.replace(microsecond=0).isoformat() - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_TIMESTAMP - -class WLEDFreeHeapSensor(WLEDSensor): +class WLEDFreeHeapSensor(WLEDEntity, SensorEntity): """Defines a WLED free heap sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:memory" + _attr_entity_registry_enabled_default = False + _attr_unit_of_measurement = DATA_BYTES + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED free heap sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:memory", - key="free_heap", - name=f"{coordinator.data.info.name} Free Memory", - unit_of_measurement=DATA_BYTES, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Free Memory" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_free_heap" @property def state(self) -> int: @@ -159,20 +107,18 @@ class WLEDFreeHeapSensor(WLEDSensor): return self.coordinator.data.info.free_heap -class WLEDWifiSignalSensor(WLEDSensor): +class WLEDWifiSignalSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi signal sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:wifi" + _attr_unit_of_measurement = PERCENTAGE + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi signal sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_signal", - name=f"{coordinator.data.info.name} Wi-Fi Signal", - unit_of_measurement=PERCENTAGE, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi Signal" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_signal" @property def state(self) -> int: @@ -180,45 +126,36 @@ class WLEDWifiSignalSensor(WLEDSensor): return self.coordinator.data.info.wifi.signal -class WLEDWifiRSSISensor(WLEDSensor): +class WLEDWifiRSSISensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi RSSI sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_device_class = DEVICE_CLASS_SIGNAL_STRENGTH + _attr_unit_of_measurement = SIGNAL_STRENGTH_DECIBELS_MILLIWATT + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi RSSI sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_rssi", - name=f"{coordinator.data.info.name} Wi-Fi RSSI", - unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT, - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi RSSI" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_rssi" @property def state(self) -> int: """Return the state of the sensor.""" return self.coordinator.data.info.wifi.rssi - @property - def device_class(self) -> str | None: - """Return the class of this sensor.""" - return DEVICE_CLASS_SIGNAL_STRENGTH - -class WLEDWifiChannelSensor(WLEDSensor): +class WLEDWifiChannelSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi Channel sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:wifi" + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi Channel sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_channel", - name=f"{coordinator.data.info.name} Wi-Fi Channel", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi Channel" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_channel" @property def state(self) -> int: @@ -226,19 +163,17 @@ class WLEDWifiChannelSensor(WLEDSensor): return self.coordinator.data.info.wifi.channel -class WLEDWifiBSSIDSensor(WLEDSensor): +class WLEDWifiBSSIDSensor(WLEDEntity, SensorEntity): """Defines a WLED Wi-Fi BSSID sensor.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:wifi" + _attr_entity_registry_enabled_default = False + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED Wi-Fi BSSID sensor.""" - super().__init__( - coordinator=coordinator, - enabled_default=False, - entry_id=entry_id, - icon="mdi:wifi", - key="wifi_bssid", - name=f"{coordinator.data.info.name} Wi-Fi BSSID", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Wi-Fi BSSID" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_wifi_bssid" @property def state(self) -> str: diff --git a/homeassistant/components/wled/services.yaml b/homeassistant/components/wled/services.yaml index 3ade18cb70e..f8d636686be 100644 --- a/homeassistant/components/wled/services.yaml +++ b/homeassistant/components/wled/services.yaml @@ -15,13 +15,10 @@ effect: intensity: name: Effect intensity description: Intensity of the effect. Number between 0 and 255. - example: 100 selector: number: min: 0 max: 255 - step: 1 - mode: slider palette: name: Color palette description: Name or ID of the WLED light palette. @@ -30,20 +27,16 @@ effect: text: speed: name: Effect speed - description: Speed of the effect. Number between 0 (slow) and 255 (fast). - example: 150 + description: Speed of the effect. selector: number: min: 0 max: 255 - step: 1 - mode: slider reverse: name: Reverse effect description: Reverse the effect. Either true to reverse or false otherwise. default: false - example: false selector: boolean: @@ -58,7 +51,6 @@ preset: preset: name: Preset ID description: ID of the WLED preset - example: 6 selector: number: min: -1 diff --git a/homeassistant/components/wled/strings.json b/homeassistant/components/wled/strings.json index f60a4bf3563..c42a6cdffb1 100644 --- a/homeassistant/components/wled/strings.json +++ b/homeassistant/components/wled/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "description": "Set up your WLED to integrate with Home Assistant.", diff --git a/homeassistant/components/wled/switch.py b/homeassistant/components/wled/switch.py index f262b5a3fa4..2d1801a0c5e 100644 --- a/homeassistant/components/wled/switch.py +++ b/homeassistant/components/wled/switch.py @@ -1,14 +1,14 @@ """Support for WLED switches.""" from __future__ import annotations -from typing import Any, Callable +from typing import Any from homeassistant.components.switch import SwitchEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback -from . import WLEDDataUpdateCoordinator, WLEDDeviceEntity, wled_exception_handler +from . import WLEDDataUpdateCoordinator, WLEDEntity, wled_exception_handler from .const import ( ATTR_DURATION, ATTR_FADE, @@ -23,55 +23,29 @@ PARALLEL_UPDATES = 1 async def async_setup_entry( hass: HomeAssistant, entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up WLED switch based on a config entry.""" coordinator: WLEDDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id] switches = [ - WLEDNightlightSwitch(entry.entry_id, coordinator), - WLEDSyncSendSwitch(entry.entry_id, coordinator), - WLEDSyncReceiveSwitch(entry.entry_id, coordinator), + WLEDNightlightSwitch(coordinator), + WLEDSyncSendSwitch(coordinator), + WLEDSyncReceiveSwitch(coordinator), ] async_add_entities(switches, True) -class WLEDSwitch(WLEDDeviceEntity, SwitchEntity): - """Defines a WLED switch.""" - - def __init__( - self, - *, - entry_id: str, - coordinator: WLEDDataUpdateCoordinator, - name: str, - icon: str, - key: str, - ) -> None: - """Initialize WLED switch.""" - self._key = key - super().__init__( - entry_id=entry_id, coordinator=coordinator, name=name, icon=icon - ) - - @property - def unique_id(self) -> str: - """Return the unique ID for this sensor.""" - return f"{self.coordinator.data.info.mac_address}_{self._key}" - - -class WLEDNightlightSwitch(WLEDSwitch): +class WLEDNightlightSwitch(WLEDEntity, SwitchEntity): """Defines a WLED nightlight switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:weather-night" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED nightlight switch.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:weather-night", - key="nightlight", - name=f"{coordinator.data.info.name} Nightlight", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Nightlight" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_nightlight" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -98,18 +72,16 @@ class WLEDNightlightSwitch(WLEDSwitch): await self.coordinator.wled.nightlight(on=True) -class WLEDSyncSendSwitch(WLEDSwitch): +class WLEDSyncSendSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync send switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator) -> None: + _attr_icon = "mdi:upload-network-outline" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync send switch.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:upload-network-outline", - key="sync_send", - name=f"{coordinator.data.info.name} Sync Send", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Sync Send" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_send" @property def extra_state_attributes(self) -> dict[str, Any] | None: @@ -132,18 +104,16 @@ class WLEDSyncSendSwitch(WLEDSwitch): await self.coordinator.wled.sync(send=True) -class WLEDSyncReceiveSwitch(WLEDSwitch): +class WLEDSyncReceiveSwitch(WLEDEntity, SwitchEntity): """Defines a WLED sync receive switch.""" - def __init__(self, entry_id: str, coordinator: WLEDDataUpdateCoordinator): + _attr_icon = "mdi:download-network-outline" + + def __init__(self, coordinator: WLEDDataUpdateCoordinator) -> None: """Initialize WLED sync receive switch.""" - super().__init__( - coordinator=coordinator, - entry_id=entry_id, - icon="mdi:download-network-outline", - key="sync_receive", - name=f"{coordinator.data.info.name} Sync Receive", - ) + super().__init__(coordinator=coordinator) + self._attr_name = f"{coordinator.data.info.name} Sync Receive" + self._attr_unique_id = f"{coordinator.data.info.mac_address}_sync_receive" @property def extra_state_attributes(self) -> dict[str, Any] | None: diff --git a/homeassistant/components/wled/translations/ca.json b/homeassistant/components/wled/translations/ca.json index 512a7de3ca4..bbc5b5232cf 100644 --- a/homeassistant/components/wled/translations/ca.json +++ b/homeassistant/components/wled/translations/ca.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/en.json b/homeassistant/components/wled/translations/en.json index 073db955f93..8ebf6f4d91b 100644 --- a/homeassistant/components/wled/translations/en.json +++ b/homeassistant/components/wled/translations/en.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/et.json b/homeassistant/components/wled/translations/et.json index 9fb057a43d8..4771c0b3af4 100644 --- a/homeassistant/components/wled/translations/et.json +++ b/homeassistant/components/wled/translations/et.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/it.json b/homeassistant/components/wled/translations/it.json index d34e1a98301..874639638b7 100644 --- a/homeassistant/components/wled/translations/it.json +++ b/homeassistant/components/wled/translations/it.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/nl.json b/homeassistant/components/wled/translations/nl.json index 3e7b16a7f4a..a06d49c8902 100644 --- a/homeassistant/components/wled/translations/nl.json +++ b/homeassistant/components/wled/translations/nl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/no.json b/homeassistant/components/wled/translations/no.json index a81683ce4c8..0e5df905e29 100644 --- a/homeassistant/components/wled/translations/no.json +++ b/homeassistant/components/wled/translations/no.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/pl.json b/homeassistant/components/wled/translations/pl.json index 6552b6de239..423c30d1fe8 100644 --- a/homeassistant/components/wled/translations/pl.json +++ b/homeassistant/components/wled/translations/pl.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/ru.json b/homeassistant/components/wled/translations/ru.json index deef7e358f1..1aefafca5f1 100644 --- a/homeassistant/components/wled/translations/ru.json +++ b/homeassistant/components/wled/translations/ru.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "WLED: {name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wled/translations/zh-Hant.json b/homeassistant/components/wled/translations/zh-Hant.json index 8841f15a425..0980bcf59aa 100644 --- a/homeassistant/components/wled/translations/zh-Hant.json +++ b/homeassistant/components/wled/translations/zh-Hant.json @@ -7,7 +7,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "WLED\uff1a{name}", + "flow_title": "{name}", "step": { "user": { "data": { diff --git a/homeassistant/components/wolflink/config_flow.py b/homeassistant/components/wolflink/config_flow.py index 20dbd8ef9b7..491b7a1232d 100644 --- a/homeassistant/components/wolflink/config_flow.py +++ b/homeassistant/components/wolflink/config_flow.py @@ -22,7 +22,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Wolf SmartSet Service.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL def __init__(self): """Initialize with empty username and password.""" diff --git a/homeassistant/components/wunderground/sensor.py b/homeassistant/components/wunderground/sensor.py index 67eab97b4c3..887e2264a70 100644 --- a/homeassistant/components/wunderground/sensor.py +++ b/homeassistant/components/wunderground/sensor.py @@ -74,7 +74,7 @@ class WUSensorConfig: icon: str = "mdi:gauge", extra_state_attributes=None, device_class=None, - ): + ) -> None: """Initialize sensor configuration. :param friendly_name: Friendly name @@ -106,7 +106,7 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): icon: str | None = "mdi:gauge", unit_of_measurement: str | None = None, device_class=None, - ): + ) -> None: """Initialize current conditions sensor configuration. :param friendly_name: Friendly name of sensor @@ -133,7 +133,9 @@ class WUCurrentConditionsSensorConfig(WUSensorConfig): class WUDailyTextForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for daily text forecasts.""" - def __init__(self, period: int, field: str, unit_of_measurement: str | None = None): + def __init__( + self, period: int, field: str, unit_of_measurement: str | None = None + ) -> None: """Initialize daily text forecast sensor configuration. :param period: forecast period number @@ -170,7 +172,7 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): ha_unit: str | None = None, icon=None, device_class=None, - ): + ) -> None: """Initialize daily simple forecast sensor configuration. :param friendly_name: friendly_name of the sensor @@ -213,7 +215,7 @@ class WUDailySimpleForecastSensorConfig(WUSensorConfig): class WUHourlyForecastSensorConfig(WUSensorConfig): """Helper for defining sensor configurations for hourly text forecasts.""" - def __init__(self, period: int, field: int): + def __init__(self, period: int, field: int) -> None: """Initialize hourly forecast sensor configuration. :param period: forecast period number @@ -280,7 +282,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): unit_of_measurement: str, icon: str, device_class=None, - ): + ) -> None: """Initialize almanac sensor configuration. :param friendly_name: Friendly name @@ -303,7 +305,7 @@ class WUAlmanacSensorConfig(WUSensorConfig): class WUAlertsSensorConfig(WUSensorConfig): """Helper for defining field configuration for alerts.""" - def __init__(self, friendly_name: str | Callable): + def __init__(self, friendly_name: str | Callable) -> None: """Initialiize alerts sensor configuration. :param friendly_name: Friendly name @@ -1120,7 +1122,9 @@ async def async_setup_platform( class WUndergroundSensor(SensorEntity): """Implementing the WUnderground sensor.""" - def __init__(self, hass: HomeAssistant, rest, condition, unique_id_base: str): + def __init__( + self, hass: HomeAssistant, rest, condition, unique_id_base: str + ) -> None: """Initialize the sensor.""" self.rest = rest self._condition = condition diff --git a/homeassistant/components/xbox/api.py b/homeassistant/components/xbox/api.py index bb38b235c0c..04714afa33e 100644 --- a/homeassistant/components/xbox/api.py +++ b/homeassistant/components/xbox/api.py @@ -14,7 +14,7 @@ class AsyncConfigEntryAuth(AuthenticationManager): self, websession: ClientSession, oauth_session: config_entry_oauth2_flow.OAuth2Session, - ): + ) -> None: """Initialize xbox auth.""" # Leaving out client credentials as they are handled by Home Assistant super().__init__(websession, "", "", "") diff --git a/homeassistant/components/xbox/base_sensor.py b/homeassistant/components/xbox/base_sensor.py index c149ce74c32..c463b31d3c5 100644 --- a/homeassistant/components/xbox/base_sensor.py +++ b/homeassistant/components/xbox/base_sensor.py @@ -12,7 +12,9 @@ from .const import DOMAIN class XboxBaseSensorEntity(CoordinatorEntity): """Base Sensor for the Xbox Integration.""" - def __init__(self, coordinator: XboxUpdateCoordinator, xuid: str, attribute: str): + def __init__( + self, coordinator: XboxUpdateCoordinator, xuid: str, attribute: str + ) -> None: """Initialize Xbox binary sensor.""" super().__init__(coordinator) self.xuid = xuid @@ -53,7 +55,7 @@ class XboxBaseSensorEntity(CoordinatorEntity): # We need to also remove the 'mode=Padding' query because with it, it results in an error 400. url = URL(self.data.display_pic) if url.host == "images-eds.xboxlive.com": - url = url.with_host("images-eds-ssl.xboxlive.com") + url = url.with_host("images-eds-ssl.xboxlive.com").with_scheme("https") query = dict(url.query) query.pop("mode", None) return str(url.with_query(query)) diff --git a/homeassistant/components/xbox/config_flow.py b/homeassistant/components/xbox/config_flow.py index 1d1f0cd8465..ef00d1381e5 100644 --- a/homeassistant/components/xbox/config_flow.py +++ b/homeassistant/components/xbox/config_flow.py @@ -1,7 +1,6 @@ """Config flow for xbox.""" import logging -from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -13,7 +12,6 @@ class OAuth2FlowHandler( """Config flow to handle xbox OAuth2 authentication.""" DOMAIN = DOMAIN - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL @property def logger(self) -> logging.Logger: diff --git a/homeassistant/components/xbox/media_source.py b/homeassistant/components/xbox/media_source.py index aeaa233a6ed..06581088823 100644 --- a/homeassistant/components/xbox/media_source.py +++ b/homeassistant/components/xbox/media_source.py @@ -74,7 +74,7 @@ class XboxSource(MediaSource): name: str = "Xbox Game Media" - def __init__(self, hass: HomeAssistant, client: XboxLiveClient): + def __init__(self, hass: HomeAssistant, client: XboxLiveClient) -> None: """Initialize Xbox source.""" super().__init__(DOMAIN) diff --git a/homeassistant/components/xbox/translations/ru.json b/homeassistant/components/xbox/translations/ru.json index b47907ffb86..5719a5d9d8a 100644 --- a/homeassistant/components/xbox/translations/ru.json +++ b/homeassistant/components/xbox/translations/ru.json @@ -2,7 +2,7 @@ "config": { "abort": { "authorize_url_timeout": "\u0418\u0441\u0442\u0435\u043a\u043b\u043e \u0432\u0440\u0435\u043c\u044f \u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438 \u0441\u0441\u044b\u043b\u043a\u0438 \u0430\u0432\u0442\u043e\u0440\u0438\u0437\u0430\u0446\u0438\u0438.", - "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438.", + "missing_configuration": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u0432\u0435\u0440\u0448\u0438\u0442\u044c \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0443. \u041f\u043e\u0436\u0430\u043b\u0443\u0439\u0441\u0442\u0430, \u043e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 \u0434\u043e\u043a\u0443\u043c\u0435\u043d\u0442\u0430\u0446\u0438\u0435\u0439.", "single_instance_allowed": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0430 \u0443\u0436\u0435 \u0432\u044b\u043f\u043e\u043b\u043d\u0435\u043d\u0430. \u0412\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u0442\u043e\u043b\u044c\u043a\u043e \u043e\u0434\u043d\u0443 \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e." }, "create_entry": { diff --git a/homeassistant/components/xiaomi/device_tracker.py b/homeassistant/components/xiaomi/device_tracker.py index 4fe485b193b..27c9aae89c9 100644 --- a/homeassistant/components/xiaomi/device_tracker.py +++ b/homeassistant/components/xiaomi/device_tracker.py @@ -6,7 +6,7 @@ import voluptuous as vol from homeassistant.components.device_tracker import ( DOMAIN, - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, DeviceScanner, ) from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, HTTP_OK @@ -14,7 +14,7 @@ import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_HOST): cv.string, vol.Required(CONF_USERNAME, default="admin"): cv.string, diff --git a/homeassistant/components/xiaomi_aqara/config_flow.py b/homeassistant/components/xiaomi_aqara/config_flow.py index c080aec508d..68c688d3eb1 100644 --- a/homeassistant/components/xiaomi_aqara/config_flow.py +++ b/homeassistant/components/xiaomi_aqara/config_flow.py @@ -46,7 +46,6 @@ class XiaomiAqaraFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Aqara config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize.""" diff --git a/homeassistant/components/xiaomi_aqara/services.yaml b/homeassistant/components/xiaomi_aqara/services.yaml index 9d8c87e5863..75a9b9156c1 100644 --- a/homeassistant/components/xiaomi_aqara/services.yaml +++ b/homeassistant/components/xiaomi_aqara/services.yaml @@ -1,39 +1,71 @@ add_device: + name: Add device description: Enables the join permission of the Xiaomi Aqara Gateway for 30 seconds. A new device can be added afterwards by pressing the pairing button once. fields: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: play_ringtone: + name: play ringtone description: Play a specific ringtone. The version of the gateway firmware must be 1.4.1_145 at least. fields: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: ringtone_id: + name: Ringtone ID description: One of the allowed ringtone ids. + required: true example: 8 + selector: + text: ringtone_vol: + name: Ringtone volume description: The volume in percent. - example: 30 + selector: + number: + min: 0 + max: 100 remove_device: + name: Remove device description: Removes a specific device. The removal is required if a device shall be paired with another gateway. fields: device_id: + name: Device ID description: Hardware address of the device to remove. + required: true example: 158d0000000000 + selector: + text: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: stop_ringtone: + name: Stop ringtone description: Stops a playing ringtone immediately. fields: gw_mac: + name: Gateway MAC description: MAC address of the Xiaomi Aqara Gateway. + required: true example: 34ce00880088 + selector: + text: diff --git a/homeassistant/components/xiaomi_aqara/strings.json b/homeassistant/components/xiaomi_aqara/strings.json index a2c8a226c95..b1675992174 100644 --- a/homeassistant/components/xiaomi_aqara/strings.json +++ b/homeassistant/components/xiaomi_aqara/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "user": { "title": "Xiaomi Aqara Gateway", diff --git a/homeassistant/components/xiaomi_aqara/translations/ca.json b/homeassistant/components/xiaomi_aqara/translations/ca.json index 6c43f026e2a..46037fa5eea 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ca.json +++ b/homeassistant/components/xiaomi_aqara/translations/ca.json @@ -12,7 +12,7 @@ "invalid_key": "Clau de la passarel\u00b7la no v\u00e0lida", "invalid_mac": "Adre\u00e7a MAC no v\u00e0lida" }, - "flow_title": "Passarel\u00b7la Xiaomi Aqara: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/en.json b/homeassistant/components/xiaomi_aqara/translations/en.json index d51687a0790..53776111d37 100644 --- a/homeassistant/components/xiaomi_aqara/translations/en.json +++ b/homeassistant/components/xiaomi_aqara/translations/en.json @@ -12,7 +12,7 @@ "invalid_key": "Invalid gateway key", "invalid_mac": "Invalid Mac Address" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/et.json b/homeassistant/components/xiaomi_aqara/translations/et.json index df00de81790..cc94b0e9b95 100644 --- a/homeassistant/components/xiaomi_aqara/translations/et.json +++ b/homeassistant/components/xiaomi_aqara/translations/et.json @@ -12,7 +12,7 @@ "invalid_key": "Vigane l\u00fc\u00fcsi v\u00f5ti", "invalid_mac": "Vigane MAC aadress" }, - "flow_title": "Xiaomi Aqara l\u00fc\u00fcs: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/it.json b/homeassistant/components/xiaomi_aqara/translations/it.json index 275729e4e81..a730a2e24d9 100644 --- a/homeassistant/components/xiaomi_aqara/translations/it.json +++ b/homeassistant/components/xiaomi_aqara/translations/it.json @@ -12,7 +12,7 @@ "invalid_key": "Chiave gateway non valida", "invalid_mac": "Indirizzo Mac non valido" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/nl.json b/homeassistant/components/xiaomi_aqara/translations/nl.json index 45a531249a4..9ef660a8e7f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/nl.json +++ b/homeassistant/components/xiaomi_aqara/translations/nl.json @@ -12,7 +12,7 @@ "invalid_key": "Ongeldige gatewaysleutel", "invalid_mac": "Ongeldig MAC-adres" }, - "flow_title": "Xiaomi Aqara Gateway: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/no.json b/homeassistant/components/xiaomi_aqara/translations/no.json index 5a46d66fcf0..081b0e5e990 100644 --- a/homeassistant/components/xiaomi_aqara/translations/no.json +++ b/homeassistant/components/xiaomi_aqara/translations/no.json @@ -12,7 +12,7 @@ "invalid_key": "Ugyldig gateway-n\u00f8kkel", "invalid_mac": "Ugyldig MAC-adresse" }, - "flow_title": "", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/pl.json b/homeassistant/components/xiaomi_aqara/translations/pl.json index 28df744d8e7..da8a803d81f 100644 --- a/homeassistant/components/xiaomi_aqara/translations/pl.json +++ b/homeassistant/components/xiaomi_aqara/translations/pl.json @@ -12,7 +12,7 @@ "invalid_key": "Nieprawid\u0142owy klucz bramki", "invalid_mac": "Nieprawid\u0142owy adres MAC" }, - "flow_title": "Bramka Xiaomi Aqara: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/ru.json b/homeassistant/components/xiaomi_aqara/translations/ru.json index 4ede8019a4f..46b46e2beec 100644 --- a/homeassistant/components/xiaomi_aqara/translations/ru.json +++ b/homeassistant/components/xiaomi_aqara/translations/ru.json @@ -12,7 +12,7 @@ "invalid_key": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 \u043a\u043b\u044e\u0447 \u0448\u043b\u044e\u0437\u0430.", "invalid_mac": "\u041d\u0435\u0432\u0435\u0440\u043d\u044b\u0439 MAC-\u0430\u0434\u0440\u0435\u0441." }, - "flow_title": "\u0428\u043b\u044e\u0437 Xiaomi Aqara: {name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json index 56c530682a3..26c49e82c16 100644 --- a/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_aqara/translations/zh-Hant.json @@ -12,7 +12,7 @@ "invalid_key": "\u7db2\u95dc\u5bc6\u9470\u7121\u6548", "invalid_mac": "\u7121\u6548\u7684 Mac \u4f4d\u5740" }, - "flow_title": "\u5c0f\u7c73 Aqara \u7db2\u95dc\uff1a{name}", + "flow_title": "{name}", "step": { "select": { "data": { diff --git a/homeassistant/components/xiaomi_miio/config_flow.py b/homeassistant/components/xiaomi_miio/config_flow.py index 9320972abcb..59eee0e6e04 100644 --- a/homeassistant/components/xiaomi_miio/config_flow.py +++ b/homeassistant/components/xiaomi_miio/config_flow.py @@ -36,7 +36,6 @@ class XiaomiMiioFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Xiaomi Miio config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL def __init__(self): """Initialize.""" diff --git a/homeassistant/components/xiaomi_miio/fan.py b/homeassistant/components/xiaomi_miio/fan.py index 6d18131cdeb..a485654e638 100644 --- a/homeassistant/components/xiaomi_miio/fan.py +++ b/homeassistant/components/xiaomi_miio/fan.py @@ -551,7 +551,9 @@ async def async_setup_entry(hass, config_entry, async_add_entities): if model in MODELS_PURIFIER_MIOT: air_purifier = AirPurifierMiot(host, token) - entity = XiaomiAirPurifierMiot(name, air_purifier, config_entry, unique_id) + entity = XiaomiAirPurifierMiot( + name, air_purifier, config_entry, unique_id, allowed_failures=2 + ) elif model.startswith("zhimi.airpurifier."): air_purifier = AirPurifier(host, token) entity = XiaomiAirPurifier(name, air_purifier, config_entry, unique_id) @@ -769,9 +771,11 @@ class XiaomiGenericDevice(XiaomiMiioEntity, FanEntity): class XiaomiAirPurifier(XiaomiGenericDevice): """Representation of a Xiaomi Air Purifier.""" - def __init__(self, name, device, entry, unique_id): + def __init__(self, name, device, entry, unique_id, allowed_failures=0): """Initialize the plug switch.""" super().__init__(name, device, entry, unique_id) + self._allowed_failures = allowed_failures + self._failure = 0 if self._model == MODEL_AIRPURIFIER_PRO: self._device_features = FEATURE_FLAGS_AIRPURIFIER_PRO @@ -822,10 +826,24 @@ class XiaomiAirPurifier(XiaomiGenericDevice): } ) + self._failure = 0 + except DeviceException as ex: - if self._available: - self._available = False - _LOGGER.error("Got exception while fetching the state: %s", ex) + self._failure += 1 + if self._failure < self._allowed_failures: + _LOGGER.info( + "Got exception while fetching the state: %s, failure: %d", + ex, + self._failure, + ) + else: + if self._available: + self._available = False + _LOGGER.error( + "Got exception while fetching the state: %s, failure: %d", + ex, + self._failure, + ) @property def speed_list(self) -> list: diff --git a/homeassistant/components/xiaomi_miio/remote.py b/homeassistant/components/xiaomi_miio/remote.py index 7d75e943d4d..5428d8a7bde 100644 --- a/homeassistant/components/xiaomi_miio/remote.py +++ b/homeassistant/components/xiaomi_miio/remote.py @@ -143,7 +143,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= "Timeout. No infrared command captured", title="Xiaomi Miio Remote" ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_LEARN, diff --git a/homeassistant/components/xiaomi_miio/sensor.py b/homeassistant/components/xiaomi_miio/sensor.py index ac9a7ab4543..16ca4d3e7ec 100644 --- a/homeassistant/components/xiaomi_miio/sensor.py +++ b/homeassistant/components/xiaomi_miio/sensor.py @@ -12,7 +12,11 @@ from miio.gateway.gateway import ( ) import voluptuous as vol -from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity +from homeassistant.components.sensor import ( + PLATFORM_SCHEMA, + STATE_CLASS_MEASUREMENT, + SensorEntity, +) from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_LEVEL, @@ -66,17 +70,27 @@ class SensorType: unit: str = None icon: str = None device_class: str = None + state_class: str = None GATEWAY_SENSOR_TYPES = { "temperature": SensorType( - unit=TEMP_CELSIUS, icon=None, device_class=DEVICE_CLASS_TEMPERATURE + unit=TEMP_CELSIUS, + icon=None, + device_class=DEVICE_CLASS_TEMPERATURE, + state_class=STATE_CLASS_MEASUREMENT, ), "humidity": SensorType( - unit=PERCENTAGE, icon=None, device_class=DEVICE_CLASS_HUMIDITY + unit=PERCENTAGE, + icon=None, + device_class=DEVICE_CLASS_HUMIDITY, + state_class=STATE_CLASS_MEASUREMENT, ), "pressure": SensorType( - unit=PRESSURE_HPA, icon=None, device_class=DEVICE_CLASS_PRESSURE + unit=PRESSURE_HPA, + icon=None, + device_class=DEVICE_CLASS_PRESSURE, + state_class=STATE_CLASS_MEASUREMENT, ), "load_power": SensorType( unit=POWER_WATT, icon=None, device_class=DEVICE_CLASS_POWER @@ -245,6 +259,11 @@ class XiaomiGatewaySensor(XiaomiGatewayDevice, SensorEntity): """Return the device class of this entity.""" return GATEWAY_SENSOR_TYPES[self._data_key].device_class + @property + def state_class(self): + """Return the state class of this entity.""" + return GATEWAY_SENSOR_TYPES[self._data_key].state_class + @property def state(self): """Return the state of the sensor.""" diff --git a/homeassistant/components/xiaomi_miio/services.yaml b/homeassistant/components/xiaomi_miio/services.yaml index f0312f01991..90d31765307 100644 --- a/homeassistant/components/xiaomi_miio/services.yaml +++ b/homeassistant/components/xiaomi_miio/services.yaml @@ -1,365 +1,621 @@ fan_set_buzzer_on: + name: Fan set buzzer on description: Turn the buzzer on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_buzzer_off: + name: Fan set buzzer off description: Turn the buzzer off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_led_on: + name: Fan set LED on description: Turn the led on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_led_off: + name: Fan set LED off description: Turn the led off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_child_lock_on: + name: Fan set child lock on description: Turn the child lock on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_child_lock_off: + name: Fan set child lock off description: Turn the child lock off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_favorite_level: + name: Fan set favorite level description: Set the favorite level. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan level: - description: Level, between 0 and 16. - example: 1 + name: Level + description: Level. + required: true + selector: + number: + min: 0 + max: 17 fan_set_fan_level: + name: Fan set level description: Set the fan level. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan level: - description: Level, between 1 and 3. - example: 1 + name: Level + description: Level. + selector: + number: + min: 1 + max: 3 fan_set_led_brightness: + name: Fan set LED brightness description: Set the led brightness. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan brightness: description: Brightness (0 = Bright, 1 = Dim, 2 = Off) - example: 1 + required: true + selector: + number: + min: 0 + max: 2 fan_set_auto_detect_on: + name: Fan set auto detect on description: Turn the auto detect on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_auto_detect_off: + name: Fan set auto detect off description: Turn the auto detect off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_learn_mode_on: + name: Fan set learn mode on description: Turn the learn mode on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_learn_mode_off: + name: Fan set learn mode off description: Turn the learn mode off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_volume: + name: Fan set volume description: Set the sound volume. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan volume: - description: Volume, between 0 and 100. - example: 50 + description: Volume. + required: true + selector: + number: + min: 0 + max: 100 fan_reset_filter: + name: Fan reset filter description: Reset the filter lifetime and usage. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_extra_features: + name: Fan set extra features description: Manipulates a storage register which advertises extra features. The Mi Home app evaluates the value. A feature called "turbo mode" is unlocked in the app on value 1. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan features: + name: Features description: Integer, known values are 0 (default) and 1 (turbo mode). - example: 1 + required: true + selector: + number: + min: 0 + max: 1 fan_set_target_humidity: + name: Fan set target humidity description: Set the target humidity. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan humidity: - description: Target humidity. Allowed values are 30, 40, 50, 60, 70 and 80. - example: 50 + name: Humidity + description: Target humidity. + required: true + selector: + number: + min: 30 + max: 80 + step: 10 + unit_of_measurement: '%' fan_set_dry_on: + name: Fan set dry on description: Turn the dry mode on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_dry_off: + name: Fan set dry off description: Turn the dry mode off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "fan.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: fan fan_set_motor_speed: + name: Fan set motor speed description: Set the target motor speed. fields: entity_id: description: Name of the xiaomi miio entity. - example: 'fan.xiaomi_miio_device' + selector: + entity: + integration: xiaomi_miio + domain: fan motor_speed: - description: Set RPM of motor speed, between 200 and 2000. - example: 1100 + name: Motor speed + description: Set motor speed. + required: true + selector: + number: + min: 200 + max: 2000 + unit_of_measurement: 'RPM' light_set_scene: + name: Light set scene description: Set a fixed scene. fields: entity_id: description: Name of the light entity. - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light scene: - description: Number of the fixed scene, between 1 and 4. - example: 1 + name: Scene + description: Number of the fixed scene. + required: true + selector: + number: + min: 1 + max: 6 light_set_delayed_turn_off: + name: Light set delayed turn off description: Delayed turn off. fields: entity_id: description: Name of the light entity. - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light time_period: + name: Time period description: Time period for the delayed turn off. + required: true example: "5, '0:05', {'minutes': 5}" + selector: + object: light_reminder_on: + name: Light reminder on description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_reminder_off: + name: Light reminder off description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_night_light_mode_on: + name: Night light mode on description: Turn the eyecare mode on (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_night_light_mode_off: + name: Night light mode off description: Turn the eyecare mode fan_set_dry_off (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_eyecare_mode_on: + name: Light eyecare mode on description: Enable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light light_eyecare_mode_off: + name: Light eyecare mode off description: Disable the eye fatigue reminder/notification (EYECARE SMART LAMP 2 ONLY). fields: entity_id: description: "Name of the entity to act on." - example: "light.xiaomi_miio" + selector: + entity: + integration: xiaomi_miio + domain: light remote_learn_command: + name: Remote learn command description: 'Learn an IR command, press "Call Service", point the remote at the IR device, and the learned command will be shown as a notification in Overview.' + target: + entity: + integration: xiaomi_miio + domain: remote fields: - entity_id: - description: "Name of the entity to learn command from." - example: "remote.xiaomi_miio" slot: - description: "Define the slot used to save the IR command (Value from 1 to 1000000)" - example: "1" + name: Slot + description: "Define the slot used to save the IR command." + default: 1 + selector: + number: + min: 1 + max: 1000000 timeout: - description: "Define the timeout in seconds, before which the command must be learned." - example: "30" + name: Timeout + description: "Define the timeout, before which the command must be learned." + default: 10 + selector: + number: + min: 1 + max: 3600 + unit_of_measurement: seconds remote_set_led_on: + name: Remote set LED on description: 'Turn on blue LED.' - fields: - entity_id: - description: "Name of the entity to turn LED on." - example: "remote.xiaomi_miio" + target: + entity: + integration: xiaomi_miio + domain: remote remote_set_led_off: + name: Remote set LED off description: 'Turn off blue LED.' - fields: - entity_id: - description: "Name of the entity to turn LED off." - example: "remote.xiaomi_miio" + target: + entity: + integration: xiaomi_miio + domain: remote switch_set_wifi_led_on: + name: Switch set Wi-fi LED on description: Turn the wifi led on. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch switch_set_wifi_led_off: + name: Switch set Wi-fi LED off description: Turn the wifi led off. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch switch_set_power_price: + name: Switch set power price description: Set the power price. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch mode: - description: Power price, between 0 and 999. - example: 31 + name: Mode + description: Power price. + required: true + selector: + number: + min: 0 + max: 999 switch_set_power_mode: + name: Switch set power mode description: Set the power mode. fields: entity_id: description: Name of the xiaomi miio entity. - example: "switch.xiaomi_miio_device" + selector: + entity: + integration: xiaomi_miio + domain: switch mode: - description: Power mode, valid values are 'normal' and 'green'. - example: "green" + name: Mode + description: Power mode. + required: true + selector: + select: + options: + - 'green' + - 'normal' vacuum_remote_control_start: + name: Vacuum remote control start description: Start remote control of the vacuum cleaner. You can then move it with `remote_control_move`, when done call `remote_control_stop`. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + target: + entity: + integration: xiaomi_miio + domain: vacuum vacuum_remote_control_stop: + name: Vacuum remote control stop description: Stop remote control mode of the vacuum cleaner. - fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" + target: + entity: + integration: xiaomi_miio + domain: vacuum vacuum_remote_control_move: + name: Vacuum remote control move description: Remote control the vacuum cleaner, make sure you first set it in remote control mode with `remote_control_start`. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" velocity: - description: Speed, between -0.29 and 0.29. - example: "0.2" + name: Velocity + description: Speed. + selector: + number: + min: -0.29 + max: 0.29 + step: 0.01 rotation: + name: Rotation description: Rotation, between -179 degrees and 179 degrees. - example: "90" + selector: + number: + min: -179 + max: 179 + unit_of_measurement: '°' duration: + name: Duration description: Duration of the movement. - example: "1500" + selector: + number: + min: 1 + max: 86400 + unit_of_measurement: seconds vacuum_remote_control_move_step: + name: Vacuum remote control move step description: Remote control the vacuum cleaner, only makes one move and then stops. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" velocity: - description: Speed, between -0.29 and 0.29. - example: "0.2" + name: Velocity + description: Speed. + selector: + number: + min: -0.29 + max: 0.29 + step: 0.01 rotation: - description: Rotation, between -179 degrees and 179 degrees. - example: "90" + name: Rotation + description: Rotation. + selector: + number: + min: -179 + max: 179 + unit_of_measurement: '°' duration: + name: Duration description: Duration of the movement. - example: "1500" + selector: + number: + min: 1 + max: 86400 + unit_of_measurement: seconds vacuum_clean_zone: + name: Vacuum clean zone description: Start the cleaning operation in the selected areas for the number of repeats indicated. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" zone: + name: Zone description: Array of zones. Each zone is an array of 4 integer values. example: "[[23510,25311,25110,26362]]" + selector: + object: repeats: - description: Number of cleaning repeats for each zone between 1 and 3. - example: "1" + name: Repeats + description: Number of cleaning repeats for each zone. + selector: + number: + min: 1 + max: 3 vacuum_goto: + name: Vacuum go to description: Go to the specified coordinates. + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" x_coord: + name: X coordinate description: x-coordinate. example: 27500 + selector: + text: y_coord: + name: Y coordinate description: y-coordinate. example: 32000 + selector: + text: vacuum_clean_segment: + name: Vacuum clean segment description: Start cleaning of the specified segment(s). + target: + entity: + integration: xiaomi_miio + domain: vacuum fields: - entity_id: - description: Name of the vacuum entity. - example: "vacuum.xiaomi_vacuum_cleaner" segments: + name: Segments description: Segments. example: "[1,2]" + selector: + object: diff --git a/homeassistant/components/xiaomi_miio/strings.json b/homeassistant/components/xiaomi_miio/strings.json index e3d9376bc31..571df98eef1 100644 --- a/homeassistant/components/xiaomi_miio/strings.json +++ b/homeassistant/components/xiaomi_miio/strings.json @@ -8,7 +8,7 @@ "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/ca.json b/homeassistant/components/xiaomi_miio/translations/ca.json index f8dee0efe69..6ee0b1e16fd 100644 --- a/homeassistant/components/xiaomi_miio/translations/ca.json +++ b/homeassistant/components/xiaomi_miio/translations/ca.json @@ -9,7 +9,7 @@ "no_device_selected": "No hi ha cap dispositiu seleccionat, selecciona'n un.", "unknown_device": "No es reconeix el model del dispositiu, no es pot configurar el dispositiu mitjan\u00e7ant el flux de configuraci\u00f3." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/en.json b/homeassistant/components/xiaomi_miio/translations/en.json index 3d893ade2f0..f5629a86eca 100644 --- a/homeassistant/components/xiaomi_miio/translations/en.json +++ b/homeassistant/components/xiaomi_miio/translations/en.json @@ -9,7 +9,7 @@ "no_device_selected": "No device selected, please select one device.", "unknown_device": "The device model is not known, not able to setup the device using config flow." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/et.json b/homeassistant/components/xiaomi_miio/translations/et.json index a290f80ad31..acc03463883 100644 --- a/homeassistant/components/xiaomi_miio/translations/et.json +++ b/homeassistant/components/xiaomi_miio/translations/et.json @@ -9,7 +9,7 @@ "no_device_selected": "Seadmeid pole valitud, vali \u00fcks seade.", "unknown_device": "Seadme mudel pole teada, seadet ei saa seadistamisvoo abil seadistada." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/it.json b/homeassistant/components/xiaomi_miio/translations/it.json index 7eec7d7e424..8eb851cda5d 100644 --- a/homeassistant/components/xiaomi_miio/translations/it.json +++ b/homeassistant/components/xiaomi_miio/translations/it.json @@ -9,7 +9,7 @@ "no_device_selected": "Nessun dispositivo selezionato, selezionare un dispositivo.", "unknown_device": "Il modello del dispositivo non \u00e8 noto, non \u00e8 possibile configurare il dispositivo utilizzando il flusso di configurazione." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/nl.json b/homeassistant/components/xiaomi_miio/translations/nl.json index 394d43fc261..012b976bea3 100644 --- a/homeassistant/components/xiaomi_miio/translations/nl.json +++ b/homeassistant/components/xiaomi_miio/translations/nl.json @@ -9,7 +9,7 @@ "no_device_selected": "Geen apparaat geselecteerd, selecteer 1 apparaat alstublieft", "unknown_device": "Het apparaatmodel is niet bekend, niet in staat om het apparaat in te stellen met config flow." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/no.json b/homeassistant/components/xiaomi_miio/translations/no.json index 74a398a9ba6..e5ca4d2d004 100644 --- a/homeassistant/components/xiaomi_miio/translations/no.json +++ b/homeassistant/components/xiaomi_miio/translations/no.json @@ -9,7 +9,7 @@ "no_device_selected": "Ingen enhet valgt, vennligst velg en enhet.", "unknown_device": "Enhetsmodellen er ikke kjent, kan ikke konfigurere enheten ved hjelp av konfigurasjonsflyt." }, - "flow_title": "", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/pl.json b/homeassistant/components/xiaomi_miio/translations/pl.json index 8b7105b6736..a7c01ef346e 100644 --- a/homeassistant/components/xiaomi_miio/translations/pl.json +++ b/homeassistant/components/xiaomi_miio/translations/pl.json @@ -9,7 +9,7 @@ "no_device_selected": "Nie wybrano \u017cadnego urz\u0105dzenia, wybierz jedno urz\u0105dzenie", "unknown_device": "Model urz\u0105dzenia nie jest znany, nie mo\u017cna skonfigurowa\u0107 urz\u0105dzenia przy u\u017cyciu interfejsu u\u017cytkownika." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/ru.json b/homeassistant/components/xiaomi_miio/translations/ru.json index b17291746b3..ef729f33a1e 100644 --- a/homeassistant/components/xiaomi_miio/translations/ru.json +++ b/homeassistant/components/xiaomi_miio/translations/ru.json @@ -9,7 +9,7 @@ "no_device_selected": "\u0412\u044b\u0431\u0435\u0440\u0438\u0442\u0435 \u043e\u0434\u043d\u043e \u0438\u0437 \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432.", "unknown_device": "\u041c\u043e\u0434\u0435\u043b\u044c \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043d\u0435 \u0438\u0437\u0432\u0435\u0441\u0442\u043d\u0430, \u043d\u0435\u0432\u043e\u0437\u043c\u043e\u0436\u043d\u043e \u0434\u043e\u0431\u0430\u0432\u0438\u0442\u044c \u044d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0441 \u043f\u043e\u043c\u043e\u0449\u044c\u044e \u043c\u0430\u0441\u0442\u0435\u0440\u0430 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438." }, - "flow_title": "Xiaomi Miio: {name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json index 8dc36f11f55..c79f6906a45 100644 --- a/homeassistant/components/xiaomi_miio/translations/zh-Hant.json +++ b/homeassistant/components/xiaomi_miio/translations/zh-Hant.json @@ -9,7 +9,7 @@ "no_device_selected": "\u672a\u9078\u64c7\u88dd\u7f6e\uff0c\u8acb\u9078\u64c7\u4e00\u9805\u88dd\u7f6e\u3002", "unknown_device": "\u88dd\u7f6e\u578b\u865f\u672a\u77e5\uff0c\u7121\u6cd5\u4f7f\u7528\u8a2d\u5b9a\u6d41\u7a0b\u3002" }, - "flow_title": "Xiaomi Miio\uff1a{name}", + "flow_title": "{name}", "step": { "device": { "data": { diff --git a/homeassistant/components/xiaomi_miio/vacuum.py b/homeassistant/components/xiaomi_miio/vacuum.py index 8551a80ff89..d0bfc148594 100644 --- a/homeassistant/components/xiaomi_miio/vacuum.py +++ b/homeassistant/components/xiaomi_miio/vacuum.py @@ -150,7 +150,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): mirobo = MiroboVacuum(name, vacuum, config_entry, unique_id) entities.append(mirobo) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_START_REMOTE_CONTROL, diff --git a/homeassistant/components/xmpp/manifest.json b/homeassistant/components/xmpp/manifest.json index 46acec9e567..55df2587898 100644 --- a/homeassistant/components/xmpp/manifest.json +++ b/homeassistant/components/xmpp/manifest.json @@ -2,7 +2,7 @@ "domain": "xmpp", "name": "Jabber (XMPP)", "documentation": "https://www.home-assistant.io/integrations/xmpp", - "requirements": ["slixmpp==1.7.0"], + "requirements": ["slixmpp==1.7.1"], "codeowners": ["@fabaff", "@flowolf"], "iot_class": "cloud_push" } diff --git a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py index d3504bcc6da..13433086879 100644 --- a/homeassistant/components/yale_smart_alarm/alarm_control_panel.py +++ b/homeassistant/components/yale_smart_alarm/alarm_control_panel.py @@ -11,7 +11,7 @@ from yalesmartalarmclient.client import ( ) from homeassistant.components.alarm_control_panel import ( - PLATFORM_SCHEMA, + PLATFORM_SCHEMA as PARENT_PLATFORM_SCHEMA, AlarmControlPanelEntity, ) from homeassistant.components.alarm_control_panel.const import ( @@ -36,7 +36,7 @@ DEFAULT_AREA_ID = "1" _LOGGER = logging.getLogger(__name__) -PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( +PLATFORM_SCHEMA = PARENT_PLATFORM_SCHEMA.extend( { vol.Required(CONF_USERNAME): cv.string, vol.Required(CONF_PASSWORD): cv.string, diff --git a/homeassistant/components/yale_smart_alarm/manifest.json b/homeassistant/components/yale_smart_alarm/manifest.json index fd1fa3bee23..e900f4e0373 100644 --- a/homeassistant/components/yale_smart_alarm/manifest.json +++ b/homeassistant/components/yale_smart_alarm/manifest.json @@ -2,7 +2,7 @@ "domain": "yale_smart_alarm", "name": "Yale Smart Living", "documentation": "https://www.home-assistant.io/integrations/yale_smart_alarm", - "requirements": ["yalesmartalarmclient==0.1.6"], - "codeowners": [], + "requirements": ["yalesmartalarmclient==0.3.3"], + "codeowners": ["@gjohansson-ST"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/yamaha/media_player.py b/homeassistant/components/yamaha/media_player.py index 5147e57dfc6..3f79be43f6e 100644 --- a/homeassistant/components/yamaha/media_player.py +++ b/homeassistant/components/yamaha/media_player.py @@ -77,7 +77,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( class YamahaConfigInfo: """Configuration Info for Yamaha Receivers.""" - def __init__(self, config: None, discovery_info: None): + def __init__(self, config: None, discovery_info: None) -> None: """Initialize the Configuration Info for Yamaha Receiver.""" self.name = config.get(CONF_NAME) self.host = config.get(CONF_HOST) @@ -152,7 +152,7 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info= async_add_entities(entities) # Register Service 'select_scene' - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SELECT_SCENE, {vol.Required(ATTR_SCENE): cv.string}, diff --git a/homeassistant/components/yamaha/services.yaml b/homeassistant/components/yamaha/services.yaml index e4d85885d54..fe2b2c66384 100644 --- a/homeassistant/components/yamaha/services.yaml +++ b/homeassistant/components/yamaha/services.yaml @@ -1,21 +1,36 @@ enable_output: + name: Enable output description: Enable or disable an output port + target: + entity: + integration: yamaha + domain: media_player fields: - entity_id: - description: Name(s) of entities to enable/disable port on. - example: "media_player.yamaha" port: + name: Port description: Name of port to enable/disable. + required: true example: "hdmi1" + selector: + text: enabled: - description: Boolean indicating if port should be enabled or not. - example: true + name: Enabled + description: Indicate if port should be enabled or not. + required: true + selector: + boolean: select_scene: + name: Select scene description: "Select a scene on the receiver" + target: + entity: + integration: yamaha + domain: media_player fields: - entity_id: - description: Name(s) of entities to enable/disable port on. - example: "media_player.yamaha" scene: + name: Scene description: Name of the scene. Standard for RX-V437 is 'BD/DVD Movie Viewing', 'TV Viewing', 'NET Audio Listening' or 'Radio Listening' + required: true example: "TV Viewing" + selector: + text: diff --git a/homeassistant/components/yeelight/__init__.py b/homeassistant/components/yeelight/__init__.py index a51323b516e..b18f18b6fa4 100644 --- a/homeassistant/components/yeelight/__init__.py +++ b/homeassistant/components/yeelight/__init__.py @@ -8,7 +8,7 @@ import logging import voluptuous as vol from yeelight import Bulb, BulbException, discover_bulbs -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry, ConfigEntryNotReady from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -19,7 +19,7 @@ from homeassistant.const import ( from homeassistant.core import HomeAssistant, callback import homeassistant.helpers.config_validation as cv from homeassistant.helpers.dispatcher import async_dispatcher_connect, dispatcher_send -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from homeassistant.helpers.event import async_track_time_interval _LOGGER = logging.getLogger(__name__) @@ -48,8 +48,8 @@ DATA_CONFIG_ENTRIES = "config_entries" DATA_CUSTOM_EFFECTS = "custom_effects" DATA_SCAN_INTERVAL = "scan_interval" DATA_DEVICE = "device" -DATA_UNSUB_UPDATE_LISTENER = "unsub_update_listener" DATA_REMOVE_INIT_DISPATCHER = "remove_init_dispatcher" +DATA_PLATFORMS_LOADED = "platforms_loaded" ATTR_COUNT = "count" ATTR_ACTION = "action" @@ -179,81 +179,115 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: return True -async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: - """Set up Yeelight from a config entry.""" +async def _async_initialize( + hass: HomeAssistant, + entry: ConfigEntry, + host: str, + device: YeelightDevice | None = None, +) -> None: + entry_data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = { + DATA_PLATFORMS_LOADED: False + } + entry.async_on_unload(entry.add_update_listener(_async_update_listener)) - async def _initialize(host: str, capabilities: dict | None = None) -> None: - remove_dispatcher = async_dispatcher_connect( - hass, - DEVICE_INITIALIZED.format(host), - _load_platforms, - ) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][ - DATA_REMOVE_INIT_DISPATCHER - ] = remove_dispatcher - - device = await _async_get_device(hass, host, entry, capabilities) - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id][DATA_DEVICE] = device - - await device.async_setup() - - async def _load_platforms(): + @callback + def _async_load_platforms(): + if entry_data[DATA_PLATFORMS_LOADED]: + return + entry_data[DATA_PLATFORMS_LOADED] = True hass.config_entries.async_setup_platforms(entry, PLATFORMS) - # Move options from data for imported entries - # Initialize options with default values for other entries - if not entry.options: - hass.config_entries.async_update_entry( - entry, - data={ - CONF_HOST: entry.data.get(CONF_HOST), - CONF_ID: entry.data.get(CONF_ID), - }, - options={ - CONF_NAME: entry.data.get(CONF_NAME, ""), - CONF_MODEL: entry.data.get(CONF_MODEL, ""), - CONF_TRANSITION: entry.data.get(CONF_TRANSITION, DEFAULT_TRANSITION), - CONF_MODE_MUSIC: entry.data.get(CONF_MODE_MUSIC, DEFAULT_MODE_MUSIC), - CONF_SAVE_ON_CHANGE: entry.data.get( - CONF_SAVE_ON_CHANGE, DEFAULT_SAVE_ON_CHANGE - ), - CONF_NIGHTLIGHT_SWITCH: entry.data.get( - CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH - ), - }, - ) + if not device: + device = await _async_get_device(hass, host, entry) + entry_data[DATA_DEVICE] = device - hass.data[DOMAIN][DATA_CONFIG_ENTRIES][entry.entry_id] = { - DATA_UNSUB_UPDATE_LISTENER: entry.add_update_listener(_async_update_listener) - } + entry.async_on_unload( + async_dispatcher_connect( + hass, + DEVICE_INITIALIZED.format(host), + _async_load_platforms, + ) + ) + + entry.async_on_unload(device.async_unload) + await device.async_setup() + + +@callback +def _async_populate_entry_options(hass: HomeAssistant, entry: ConfigEntry) -> None: + """Move options from data for imported entries. + + Initialize options with default values for other entries. + """ + if entry.options: + return + + hass.config_entries.async_update_entry( + entry, + data={ + CONF_HOST: entry.data.get(CONF_HOST), + CONF_ID: entry.data.get(CONF_ID), + }, + options={ + CONF_NAME: entry.data.get(CONF_NAME, ""), + CONF_MODEL: entry.data.get(CONF_MODEL, ""), + CONF_TRANSITION: entry.data.get(CONF_TRANSITION, DEFAULT_TRANSITION), + CONF_MODE_MUSIC: entry.data.get(CONF_MODE_MUSIC, DEFAULT_MODE_MUSIC), + CONF_SAVE_ON_CHANGE: entry.data.get( + CONF_SAVE_ON_CHANGE, DEFAULT_SAVE_ON_CHANGE + ), + CONF_NIGHTLIGHT_SWITCH: entry.data.get( + CONF_NIGHTLIGHT_SWITCH, DEFAULT_NIGHTLIGHT_SWITCH + ), + }, + ) + + +async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Set up Yeelight from a config entry.""" + _async_populate_entry_options(hass, entry) if entry.data.get(CONF_HOST): - # manually added device - await _initialize(entry.data[CONF_HOST]) - else: - # discovery - scanner = YeelightScanner.async_get(hass) - scanner.async_register_callback(entry.data[CONF_ID], _initialize) + try: + device = await _async_get_device(hass, entry.data[CONF_HOST], entry) + except OSError as ex: + # If CONF_ID is not valid we cannot fallback to discovery + # so we must retry by raising ConfigEntryNotReady + if not entry.data.get(CONF_ID): + raise ConfigEntryNotReady from ex + # Otherwise fall through to discovery + else: + # manually added device + await _async_initialize(hass, entry, entry.data[CONF_HOST], device=device) + return True + # discovery + scanner = YeelightScanner.async_get(hass) + + async def _async_from_discovery(host: str) -> None: + await _async_initialize(hass, entry, host) + + scanner.async_register_callback(entry.data[CONF_ID], _async_from_discovery) return True async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): """Unload a config entry.""" - unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS) - if unload_ok: - data = hass.data[DOMAIN][DATA_CONFIG_ENTRIES].pop(entry.entry_id) - remove_init_dispatcher = data.get(DATA_REMOVE_INIT_DISPATCHER) - if remove_init_dispatcher is not None: - remove_init_dispatcher() - data[DATA_UNSUB_UPDATE_LISTENER]() - data[DATA_DEVICE].async_unload() - if entry.data[CONF_ID]: - # discovery - scanner = YeelightScanner.async_get(hass) - scanner.async_unregister_callback(entry.data[CONF_ID]) + data_config_entries = hass.data[DOMAIN][DATA_CONFIG_ENTRIES] + entry_data = data_config_entries[entry.entry_id] - return unload_ok + if entry_data[DATA_PLATFORMS_LOADED]: + if not await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + return False + + if entry.data.get(CONF_ID): + # discovery + scanner = YeelightScanner.async_get(hass) + scanner.async_unregister_callback(entry.data[CONF_ID]) + + data_config_entries.pop(entry.entry_id) + + return True @callback @@ -282,7 +316,7 @@ class YeelightScanner: cls._scanner = cls(hass) return cls._scanner - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize class.""" self._hass = hass self._seen = {} @@ -539,7 +573,7 @@ class YeelightDevice: class YeelightEntity(Entity): """Represents single Yeelight entity.""" - def __init__(self, device: YeelightDevice, entry: ConfigEntry): + def __init__(self, device: YeelightDevice, entry: ConfigEntry) -> None: """Initialize the entity.""" self._device = device self._unique_id = entry.entry_id @@ -553,7 +587,7 @@ class YeelightEntity(Entity): return self._unique_id @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return the device info.""" return { "identifiers": {(DOMAIN, self._unique_id)}, @@ -582,16 +616,12 @@ async def _async_get_device( hass: HomeAssistant, host: str, entry: ConfigEntry, - capabilities: dict | None, ) -> YeelightDevice: # Get model from config and capabilities model = entry.options.get(CONF_MODEL) - if not model and capabilities is not None: - model = capabilities.get("model") # Set up device bulb = Bulb(host, model=model or None) - if capabilities is None: - capabilities = await hass.async_add_executor_job(bulb.get_capabilities) + capabilities = await hass.async_add_executor_job(bulb.get_capabilities) return YeelightDevice(hass, host, entry.options, bulb, capabilities) diff --git a/homeassistant/components/yeelight/config_flow.py b/homeassistant/components/yeelight/config_flow.py index 0473cc1042c..0b0fe0d96c1 100644 --- a/homeassistant/components/yeelight/config_flow.py +++ b/homeassistant/components/yeelight/config_flow.py @@ -5,6 +5,7 @@ import voluptuous as vol import yeelight from homeassistant import config_entries, exceptions +from homeassistant.components.dhcp import IP_ADDRESS from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import callback import homeassistant.helpers.config_validation as cv @@ -21,6 +22,8 @@ from . import ( _async_unique_name, ) +MODEL_UNKNOWN = "unknown" + _LOGGER = logging.getLogger(__name__) @@ -28,7 +31,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Yeelight.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL @staticmethod @callback @@ -39,24 +41,73 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self): """Initialize the config flow.""" self._discovered_devices = {} + self._discovered_model = None + self._discovered_ip = None + + async def async_step_homekit(self, discovery_info): + """Handle discovery from homekit.""" + self._discovered_ip = discovery_info["host"] + return await self._async_handle_discovery() + + async def async_step_dhcp(self, discovery_info): + """Handle discovery from dhcp.""" + self._discovered_ip = discovery_info[IP_ADDRESS] + return await self._async_handle_discovery() + + async def _async_handle_discovery(self): + """Handle any discovery.""" + self.context[CONF_HOST] = self._discovered_ip + for progress in self._async_in_progress(): + if progress.get("context", {}).get(CONF_HOST) == self._discovered_ip: + return self.async_abort(reason="already_in_progress") + + try: + self._discovered_model = await self._async_try_connect(self._discovered_ip) + except CannotConnect: + return self.async_abort(reason="cannot_connect") + + if not self.unique_id: + return self.async_abort(reason="cannot_connect") + + self._abort_if_unique_id_configured( + updates={CONF_HOST: self._discovered_ip}, reload_on_update=False + ) + return await self.async_step_discovery_confirm() + + async def async_step_discovery_confirm(self, user_input=None): + """Confirm discovery.""" + if user_input is not None: + return self.async_create_entry( + title=f"{self._discovered_model} {self.unique_id}", + data={CONF_ID: self.unique_id, CONF_HOST: self._discovered_ip}, + ) + + self._set_confirm_only() + placeholders = { + "model": self._discovered_model, + "host": self._discovered_ip, + } + self.context["title_placeholders"] = placeholders + return self.async_show_form( + step_id="discovery_confirm", description_placeholders=placeholders + ) async def async_step_user(self, user_input=None): """Handle the initial step.""" errors = {} if user_input is not None: - if user_input.get(CONF_HOST): - try: - await self._async_try_connect(user_input[CONF_HOST]) - return self.async_create_entry( - title=user_input[CONF_HOST], - data=user_input, - ) - except CannotConnect: - errors["base"] = "cannot_connect" - except AlreadyConfigured: - return self.async_abort(reason="already_configured") - else: + if not user_input.get(CONF_HOST): return await self.async_step_pick_device() + try: + model = await self._async_try_connect(user_input[CONF_HOST]) + except CannotConnect: + errors["base"] = "cannot_connect" + else: + self._abort_if_unique_id_configured() + return self.async_create_entry( + title=f"{model} {self.unique_id}", + data=user_input, + ) user_input = user_input or {} return self.async_show_form( @@ -115,20 +166,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except CannotConnect: _LOGGER.error("Failed to import %s: cannot connect", host) return self.async_abort(reason="cannot_connect") - except AlreadyConfigured: - return self.async_abort(reason="already_configured") if CONF_NIGHTLIGHT_SWITCH_TYPE in user_input: user_input[CONF_NIGHTLIGHT_SWITCH] = ( user_input.pop(CONF_NIGHTLIGHT_SWITCH_TYPE) == NIGHTLIGHT_SWITCH_TYPE_LIGHT ) + self._abort_if_unique_id_configured() return self.async_create_entry(title=user_input[CONF_NAME], data=user_input) async def _async_try_connect(self, host): """Set up with options.""" - for entry in self._async_current_entries(): - if entry.data.get(CONF_HOST) == host: - raise AlreadyConfigured + self._async_abort_entries_match({CONF_HOST: host}) bulb = yeelight.Bulb(host) try: @@ -138,8 +186,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): else: _LOGGER.debug("Get capabilities: %s", capabilities) await self.async_set_unique_id(capabilities["id"]) - self._abort_if_unique_id_configured() - return + return capabilities["model"] except OSError as err: _LOGGER.debug("Failed to get capabilities from %s: %s", host, err) # Ignore the error since get_capabilities uses UDP discovery packet @@ -152,6 +199,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.error("Failed to get properties from %s: %s", host, err) raise CannotConnect from err _LOGGER.debug("Get properties: %s", bulb.last_properties) + return MODEL_UNKNOWN class OptionsFlowHandler(config_entries.OptionsFlow): @@ -196,7 +244,3 @@ class OptionsFlowHandler(config_entries.OptionsFlow): class CannotConnect(exceptions.HomeAssistantError): """Error to indicate we cannot connect.""" - - -class AlreadyConfigured(exceptions.HomeAssistantError): - """Indicate the ip address is already configured.""" diff --git a/homeassistant/components/yeelight/light.py b/homeassistant/components/yeelight/light.py index 218bcbbdb27..0782ab94c61 100644 --- a/homeassistant/components/yeelight/light.py +++ b/homeassistant/components/yeelight/light.py @@ -375,7 +375,7 @@ def _async_setup_services(hass: HomeAssistant): ) ) - platform = entity_platform.current_platform.get() + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( SERVICE_SET_MODE, diff --git a/homeassistant/components/yeelight/manifest.json b/homeassistant/components/yeelight/manifest.json index 8e5288efb81..0bf6249b647 100644 --- a/homeassistant/components/yeelight/manifest.json +++ b/homeassistant/components/yeelight/manifest.json @@ -2,8 +2,14 @@ "domain": "yeelight", "name": "Yeelight", "documentation": "https://www.home-assistant.io/integrations/yeelight", - "requirements": ["yeelight==0.6.2"], + "requirements": ["yeelight==0.6.3"], "codeowners": ["@rytilahti", "@zewelor", "@shenxn"], "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_polling", + "dhcp": [{ + "hostname": "yeelink-*" + }], + "homekit": { + "models": ["YLDP*"] + } } diff --git a/homeassistant/components/yeelight/services.yaml b/homeassistant/components/yeelight/services.yaml index b519d0c91d9..b365f273e31 100644 --- a/homeassistant/components/yeelight/services.yaml +++ b/homeassistant/components/yeelight/services.yaml @@ -1,96 +1,193 @@ set_mode: + name: Set mode description: Set a operation mode. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" mode: - description: Operation mode. Valid values are 'last', 'normal', 'rgb', 'hsv', 'color_flow', 'moonlight'. - example: "moonlight" + name: Mode + description: Operation mode. + required: true + selector: + select: + options: + - 'color_flow' + - 'hsv' + - 'last' + - 'moonlight' + - 'normal' + - 'rgb' + set_color_scene: + name: Set color scene description: Changes the light to the specified RGB color and brightness. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" rgb_color: + name: RGB color description: Color for the light in RGB-format. example: "[255, 100, 100]" + selector: + object: brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" set_hsv_scene: + name: Set HSV scene description: Changes the light to the specified HSV color and brightness. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" hs_color: + name: Hue/sat color description: Color for the light in hue/sat format. Hue is 0-359 and Sat is 0-100. example: "[300, 70]" + selector: + object: brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" set_color_temp_scene: + name: Set color temperature scene description: Changes the light to the specified color temperature. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" kelvin: + name: Kelvin description: Color temperature for the light in Kelvin. - example: 4000 + selector: + number: + min: 1700 + max: 6500 + step: 100 + unit_of_measurement: K brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" set_color_flow_scene: + name: Set color flow scene description: starts a color flow. If the light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" count: + name: Count description: The number of times to run this flow (0 to run forever). - example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 action: - description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') - example: "stay" + name: Action + description: The action to take after the flow stops. + default: 'recover' + selector: + select: + options: + - 'off' + - 'recover' + - 'stay' transitions: + name: Transitions description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' + selector: + object: set_auto_delay_off_scene: + name: Set auto delay off scene description: Turns the light on to the specified brightness and sets a timer to turn it back off after the given number of minutes. If the light is off, Set a color scene, if light is off, it will be turned on. + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" minutes: - description: The minutes to wait before automatically turning the light off. - example: 5 + name: Minutes + description: The time to wait before automatically turning the light off. + selector: + number: + min: 1 + max: 60 + unit_of_measurement: minutes brightness: - description: The brightness value to set (1-100). - example: 50 + name: Brightness + description: The brightness value to set. + selector: + number: + min: 0 + max: 100 + unit_of_measurement: "%" start_flow: + name: Start flow description: Start a custom flow, using transitions from https://yeelight.readthedocs.io/en/stable/yeelight.html#flow-objects + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" count: + name: Count description: The number of times to run this flow (0 to run forever). - example: 0 + default: 0 + selector: + number: + min: 0 + max: 100 action: - description: The action to take after the flow stops. Can be 'recover', 'stay', 'off'. (default 'recover') - example: "stay" + name: Action + description: The action to take after the flow stops. + default: 'recover' + selector: + select: + options: + - 'off' + - 'recover' + - 'stay' transitions: + name: Transitions description: Array of transitions, for desired effect. Examples https://yeelight.readthedocs.io/en/stable/flow.html example: '[{ "TemperatureTransition": [1900, 1000, 80] }, { "TemperatureTransition": [1900, 1000, 10] }]' + selector: + object: set_music_mode: + name: Set music mode description: Enable or disable music_mode + target: + entity: + integration: yeelight + domain: light fields: - entity_id: - description: Name of the light entity. - example: "light.yeelight" music_mode: + name: Music mode description: Use true or false to enable / disable music_mode - example: true + required: true + selector: + boolean: diff --git a/homeassistant/components/yeelight/strings.json b/homeassistant/components/yeelight/strings.json index 52a684bc26f..807fae1ca64 100644 --- a/homeassistant/components/yeelight/strings.json +++ b/homeassistant/components/yeelight/strings.json @@ -1,5 +1,6 @@ { "config": { + "flow_title": "{model} {host}", "step": { "user": { "description": "If you leave the host empty, discovery will be used to find devices.", @@ -11,6 +12,9 @@ "data": { "device": "Device" } + }, + "discovery_confirm": { + "description": "Do you want to setup {model} ({host})?" } }, "error": { diff --git a/homeassistant/components/yeelight/translations/ca.json b/homeassistant/components/yeelight/translations/ca.json index 77fe2b49c71..9bdbd01bfca 100644 --- a/homeassistant/components/yeelight/translations/ca.json +++ b/homeassistant/components/yeelight/translations/ca.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Vols configurar {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositiu" diff --git a/homeassistant/components/yeelight/translations/de.json b/homeassistant/components/yeelight/translations/de.json index 6eaff2e87a3..1a8f9a463b7 100644 --- a/homeassistant/components/yeelight/translations/de.json +++ b/homeassistant/components/yeelight/translations/de.json @@ -8,6 +8,9 @@ "cannot_connect": "Verbindung fehlgeschlagen" }, "step": { + "discovery_confirm": { + "description": "M\u00f6chten Sie {model} ({host}) einrichten?" + }, "pick_device": { "data": { "device": "Ger\u00e4te" @@ -25,10 +28,13 @@ "step": { "init": { "data": { + "model": "Modell (optional)", + "nightlight_switch": "Nachtlichtschalter verwenden", "save_on_change": "Status bei \u00c4nderung speichern", "transition": "\u00dcbergangszeit (ms)", "use_music_mode": "Musik-Modus aktivieren" - } + }, + "description": "Wenn Sie das Modell leer lassen, wird es automatisch erkannt." } } } diff --git a/homeassistant/components/yeelight/translations/en.json b/homeassistant/components/yeelight/translations/en.json index 218f82f86b7..06431e7bc2b 100644 --- a/homeassistant/components/yeelight/translations/en.json +++ b/homeassistant/components/yeelight/translations/en.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Failed to connect" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Do you want to setup {model} ({host})?" + }, "pick_device": { "data": { "device": "Device" diff --git a/homeassistant/components/yeelight/translations/es.json b/homeassistant/components/yeelight/translations/es.json index d633a885fda..044a10c695d 100644 --- a/homeassistant/components/yeelight/translations/es.json +++ b/homeassistant/components/yeelight/translations/es.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "No se pudo conectar" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "\u00bfQuieres configurar {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositivo" diff --git a/homeassistant/components/yeelight/translations/et.json b/homeassistant/components/yeelight/translations/et.json index 55cdf2e0b28..450b85b03cd 100644 --- a/homeassistant/components/yeelight/translations/et.json +++ b/homeassistant/components/yeelight/translations/et.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Kas seadistada {model} ({host})?" + }, "pick_device": { "data": { "device": "Seade" diff --git a/homeassistant/components/yeelight/translations/it.json b/homeassistant/components/yeelight/translations/it.json index a8129f4359d..1a139dcd8b4 100644 --- a/homeassistant/components/yeelight/translations/it.json +++ b/homeassistant/components/yeelight/translations/it.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Impossibile connettersi" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Vuoi configurare {model} ({host})?" + }, "pick_device": { "data": { "device": "Dispositivo" diff --git a/homeassistant/components/yeelight/translations/nl.json b/homeassistant/components/yeelight/translations/nl.json index 7dd509cca06..6aecb4f0bd9 100644 --- a/homeassistant/components/yeelight/translations/nl.json +++ b/homeassistant/components/yeelight/translations/nl.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Kon niet verbinden" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Wilt u {model} ({host}) instellen?" + }, "pick_device": { "data": { "device": "Apparaat" diff --git a/homeassistant/components/yeelight/translations/no.json b/homeassistant/components/yeelight/translations/no.json index 5d3107b779f..bbfe545e919 100644 --- a/homeassistant/components/yeelight/translations/no.json +++ b/homeassistant/components/yeelight/translations/no.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Vil du sette opp {model} ( {host} )?" + }, "pick_device": { "data": { "device": "Enhet" diff --git a/homeassistant/components/yeelight/translations/pl.json b/homeassistant/components/yeelight/translations/pl.json index 574f8303a4c..9ea693fffcc 100644 --- a/homeassistant/components/yeelight/translations/pl.json +++ b/homeassistant/components/yeelight/translations/pl.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "Czy chcesz skonfigurowa\u0107 {model} ({host})?" + }, "pick_device": { "data": { "device": "Urz\u0105dzenie" diff --git a/homeassistant/components/yeelight/translations/ru.json b/homeassistant/components/yeelight/translations/ru.json index 221fa3b4738..cbeaad534b4 100644 --- a/homeassistant/components/yeelight/translations/ru.json +++ b/homeassistant/components/yeelight/translations/ru.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "\u0425\u043e\u0442\u0438\u0442\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0438\u0442\u044c {model} ({host})?" + }, "pick_device": { "data": { "device": "\u0423\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e" diff --git a/homeassistant/components/yeelight/translations/zh-Hant.json b/homeassistant/components/yeelight/translations/zh-Hant.json index fe21b9e535b..b5df81beafd 100644 --- a/homeassistant/components/yeelight/translations/zh-Hant.json +++ b/homeassistant/components/yeelight/translations/zh-Hant.json @@ -7,7 +7,11 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, + "flow_title": "{model} {host}", "step": { + "discovery_confirm": { + "description": "\u662f\u5426\u8981\u8a2d\u5b9a {model} ({host})\uff1f" + }, "pick_device": { "data": { "device": "\u88dd\u7f6e" diff --git a/homeassistant/components/zamg/sensor.py b/homeassistant/components/zamg/sensor.py index 2e2d07cea62..9731e033972 100644 --- a/homeassistant/components/zamg/sensor.py +++ b/homeassistant/components/zamg/sensor.py @@ -7,7 +7,6 @@ import logging import os from aiohttp.hdrs import USER_AGENT -import pytz import requests import voluptuous as vol @@ -28,7 +27,7 @@ from homeassistant.const import ( __version__, ) import homeassistant.helpers.config_validation as cv -from homeassistant.util import Throttle +from homeassistant.util import Throttle, dt as dt_util _LOGGER = logging.getLogger(__name__) @@ -41,6 +40,7 @@ CONF_STATION_ID = "station_id" DEFAULT_NAME = "zamg" MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=10) +VIENNA_TIME_ZONE = dt_util.get_time_zone("Europe/Vienna") SENSOR_TYPES = { "pressure": ("Pressure", PRESSURE_HPA, "LDstat hPa", float), @@ -108,7 +108,7 @@ def setup_platform(hass, config, add_entities, discovery_info=None): station_id = config.get(CONF_STATION_ID) or closest_station( latitude, longitude, hass.config.config_dir ) - if station_id not in zamg_stations(hass.config.config_dir): + if station_id not in _get_ogd_stations(): _LOGGER.error( "Configured ZAMG %s (%s) is not a known station", CONF_STATION_ID, @@ -187,7 +187,7 @@ class ZamgData: date, time = self.data.get("update_date"), self.data.get("update_time") if date is not None and time is not None: return datetime.strptime(date + time, "%d-%m-%Y%H:%M").replace( - tzinfo=pytz.timezone("Europe/Vienna") + tzinfo=VIENNA_TIME_ZONE ) @classmethod @@ -208,7 +208,7 @@ class ZamgData: """Get the latest data from ZAMG.""" if self.last_update and ( self.last_update + timedelta(hours=1) - > datetime.utcnow().replace(tzinfo=pytz.utc) + > datetime.utcnow().replace(tzinfo=dt_util.UTC) ): return # Not time to update yet; data is only hourly @@ -239,9 +239,14 @@ class ZamgData: return self.data.get(variable) +def _get_ogd_stations(): + """Return all stations in the OGD dataset.""" + return {r["Station"] for r in ZamgData.current_observations()} + + def _get_zamg_stations(): """Return {CONF_STATION: (lat, lon)} for all stations, for auto-config.""" - capital_stations = {r["Station"] for r in ZamgData.current_observations()} + capital_stations = _get_ogd_stations() req = requests.get( "https://www.zamg.ac.at/cms/en/documents/climate/" "doc_metnetwork/zamg-observation-points", diff --git a/homeassistant/components/zeroconf/__init__.py b/homeassistant/components/zeroconf/__init__.py index 58d8ad21094..64d631e2850 100644 --- a/homeassistant/components/zeroconf/__init__.py +++ b/homeassistant/components/zeroconf/__init__.py @@ -1,20 +1,17 @@ """Support for exposing Home Assistant via Zeroconf.""" from __future__ import annotations -from collections.abc import Iterable +import asyncio +from collections.abc import Coroutine from contextlib import suppress import fnmatch -from functools import partial import ipaddress -from ipaddress import ip_address import logging import socket from typing import Any, TypedDict, cast -from pyroute2 import IPRoute import voluptuous as vol from zeroconf import ( - Error as ZeroconfError, InterfaceChoice, IPVersion, NonUniqueNameException, @@ -24,20 +21,21 @@ from zeroconf import ( ) from homeassistant import config_entries, util +from homeassistant.components import network +from homeassistant.components.network.models import Adapter from homeassistant.const import ( EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP, __version__, ) -from homeassistant.core import Event, HomeAssistant +from homeassistant.core import Event, HomeAssistant, callback +from homeassistant.data_entry_flow import FlowResult import homeassistant.helpers.config_validation as cv from homeassistant.helpers.network import NoURLAvailableError, get_url -from homeassistant.helpers.singleton import singleton -from homeassistant.loader import async_get_homekit, async_get_zeroconf -from homeassistant.util.network import is_loopback +from homeassistant.loader import async_get_homekit, async_get_zeroconf, bind_hass -from .models import HaServiceBrowser, HaZeroconf +from .models import HaAsyncZeroconf, HaServiceBrowser, HaZeroconf from .usage import install_multiple_zeroconf_catcher _LOGGER = logging.getLogger(__name__) @@ -70,11 +68,14 @@ MAX_NAME_LEN = 63 CONFIG_SCHEMA = vol.Schema( { - DOMAIN: vol.Schema( - { - vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean, - vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, - } + DOMAIN: vol.All( + cv.deprecated(CONF_DEFAULT_INTERFACE), + vol.Schema( + { + vol.Optional(CONF_DEFAULT_INTERFACE): cv.boolean, + vol.Optional(CONF_IPV6, default=DEFAULT_IPV6): cv.boolean, + } + ), ) }, extra=vol.ALLOW_EXTRA, @@ -92,16 +93,34 @@ class HaServiceInfo(TypedDict): properties: dict[str, Any] -@singleton(DOMAIN) +class ZeroconfFlow(TypedDict): + """A queued zeroconf discovery flow.""" + + domain: str + context: dict[str, Any] + data: HaServiceInfo + + +@bind_hass async def async_get_instance(hass: HomeAssistant) -> HaZeroconf: + """Zeroconf instance to be shared with other integrations that use it.""" + return cast(HaZeroconf, (await _async_get_instance(hass)).zeroconf) + + +@bind_hass +async def async_get_async_instance(hass: HomeAssistant) -> HaAsyncZeroconf: """Zeroconf instance to be shared with other integrations that use it.""" return await _async_get_instance(hass) -async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: +async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaAsyncZeroconf: + if DOMAIN in hass.data: + return cast(HaAsyncZeroconf, hass.data[DOMAIN]) + logging.getLogger("zeroconf").setLevel(logging.NOTSET) - zeroconf = await hass.async_add_executor_job(partial(HaZeroconf, **zcargs)) + aio_zc = HaAsyncZeroconf(**zcargs) + zeroconf = cast(HaZeroconf, aio_zc.zeroconf) install_multiple_zeroconf_catcher(zeroconf) @@ -110,53 +129,16 @@ async def _async_get_instance(hass: HomeAssistant, **zcargs: Any) -> HaZeroconf: zeroconf.ha_close() hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _stop_zeroconf) + hass.data[DOMAIN] = aio_zc - return zeroconf + return aio_zc -def _get_ip_route(dst_ip: str) -> Any: - """Get ip next hop.""" - return IPRoute().route("get", dst=dst_ip) - - -def _first_ip_nexthop_from_route(routes: Iterable) -> None | str: - """Find the first RTA_PREFSRC in the routes.""" - _LOGGER.debug("Routes: %s", routes) - for route in routes: - for key, value in route["attrs"]: - if key == "RTA_PREFSRC": - return cast(str, value) - return None - - -async def async_detect_interfaces_setting(hass: HomeAssistant) -> InterfaceChoice: - """Auto detect the interfaces setting when unset.""" - routes = [] - try: - routes = await hass.async_add_executor_job(_get_ip_route, MDNS_TARGET_IP) - except Exception as ex: # pylint: disable=broad-except - _LOGGER.debug( - "The system could not auto detect routing data on your operating system; Zeroconf will broadcast on all interfaces", - exc_info=ex, - ) - return InterfaceChoice.All - - if not (first_ip := _first_ip_nexthop_from_route(routes)): - _LOGGER.debug( - "The system could not auto detect the nexthop for %s on your operating system; Zeroconf will broadcast on all interfaces", - MDNS_TARGET_IP, - ) - return InterfaceChoice.All - - if is_loopback(ip_address(first_ip)): - _LOGGER.debug( - "The next hop for %s is %s; Zeroconf will broadcast on all interfaces", - MDNS_TARGET_IP, - first_ip, - ) - return InterfaceChoice.All - - return InterfaceChoice.Default +def _async_use_default_interface(adapters: list[Adapter]) -> bool: + for adapter in adapters: + if adapter["enabled"] and not adapter["default"]: + return False + return True async def async_setup(hass: HomeAssistant, config: dict) -> bool: @@ -164,14 +146,29 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: zc_config = config.get(DOMAIN, {}) zc_args: dict = {} - if CONF_DEFAULT_INTERFACE not in zc_config: - zc_args["interfaces"] = await async_detect_interfaces_setting(hass) - elif zc_config[CONF_DEFAULT_INTERFACE]: + adapters = await network.async_get_adapters(hass) + if _async_use_default_interface(adapters): zc_args["interfaces"] = InterfaceChoice.Default + else: + interfaces = zc_args["interfaces"] = [] + for adapter in adapters: + if not adapter["enabled"]: + continue + if ipv4s := adapter["ipv4"]: + interfaces.append(ipv4s[0]["address"]) + elif ipv6s := adapter["ipv6"]: + interfaces.append(ipv6s[0]["scope_id"]) if not zc_config.get(CONF_IPV6, DEFAULT_IPV6): zc_args["ip_version"] = IPVersion.V4Only - zeroconf = hass.data[DOMAIN] = await _async_get_instance(hass, **zc_args) + aio_zc = await _async_get_instance(hass, **zc_args) + zeroconf = aio_zc.zeroconf + + zeroconf_types, homekit_models = await asyncio.gather( + async_get_zeroconf(hass), async_get_homekit(hass) + ) + discovery = ZeroconfDiscovery(hass, zeroconf, zeroconf_types, homekit_models) + await discovery.async_setup() async def _async_zeroconf_hass_start(_event: Event) -> None: """Expose Home Assistant on zeroconf when it starts. @@ -179,25 +176,25 @@ async def async_setup(hass: HomeAssistant, config: dict) -> bool: Wait till started or otherwise HTTP is not up and running. """ uuid = await hass.helpers.instance_id.async_get() - await hass.async_add_executor_job( - _register_hass_zc_service, hass, zeroconf, uuid - ) + await _async_register_hass_zc_service(hass, aio_zc, uuid) - async def _async_zeroconf_hass_started(_event: Event) -> None: - """Start the service browser.""" + @callback + def _async_start_discovery(_event: Event) -> None: + """Start processing flows.""" + discovery.async_start() - await _async_start_zeroconf_browser(hass, zeroconf) + async def _async_zeroconf_hass_stop(_event: Event) -> None: + await discovery.async_stop() + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_zeroconf_hass_stop) hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _async_zeroconf_hass_start) - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STARTED, _async_zeroconf_hass_started - ) + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STARTED, _async_start_discovery) return True -def _register_hass_zc_service( - hass: HomeAssistant, zeroconf: HaZeroconf, uuid: str +async def _async_register_hass_zc_service( + hass: HomeAssistant, aio_zc: HaAsyncZeroconf, uuid: str ) -> None: # Get instance UUID valid_location_name = _truncate_location_name_to_valid(hass.config.location_name) @@ -244,51 +241,105 @@ def _register_hass_zc_service( _LOGGER.info("Starting Zeroconf broadcast") try: - zeroconf.register_service(info) + await aio_zc.async_register_service(info) except NonUniqueNameException: _LOGGER.error( "Home Assistant instance with identical name present in the local network" ) -async def _async_start_zeroconf_browser( - hass: HomeAssistant, zeroconf: HaZeroconf -) -> None: - """Start the zeroconf browser.""" +class FlowDispatcher: + """Dispatch discovery flows.""" - zeroconf_types = await async_get_zeroconf(hass) - homekit_models = await async_get_homekit(hass) + def __init__(self, hass: HomeAssistant) -> None: + """Init the discovery dispatcher.""" + self.hass = hass + self.pending_flows: list[ZeroconfFlow] = [] + self.started = False - types = list(zeroconf_types) + @callback + def async_start(self) -> None: + """Start processing pending flows.""" + self.started = True + self.hass.loop.call_soon(self._async_process_pending_flows) - for hk_type in HOMEKIT_TYPES: - if hk_type not in zeroconf_types: - types.append(hk_type) + def _async_process_pending_flows(self) -> None: + for flow in self.pending_flows: + self.hass.async_create_task(self._init_flow(flow)) + self.pending_flows = [] + + def create(self, flow: ZeroconfFlow) -> None: + """Create and add or queue a flow.""" + if self.started: + self.hass.create_task(self._init_flow(flow)) + else: + self.pending_flows.append(flow) + + def _init_flow(self, flow: ZeroconfFlow) -> Coroutine[None, None, FlowResult]: + """Create a flow.""" + return self.hass.config_entries.flow.async_init( + flow["domain"], context=flow["context"], data=flow["data"] + ) + + +class ZeroconfDiscovery: + """Discovery via zeroconf.""" + + def __init__( + self, + hass: HomeAssistant, + zeroconf: Zeroconf, + zeroconf_types: dict[str, list[dict[str, str]]], + homekit_models: dict[str, str], + ) -> None: + """Init discovery.""" + self.hass = hass + self.zeroconf = zeroconf + self.zeroconf_types = zeroconf_types + self.homekit_models = homekit_models + + self.flow_dispatcher: FlowDispatcher | None = None + self.service_browser: HaServiceBrowser | None = None + + async def async_setup(self) -> None: + """Start discovery.""" + self.flow_dispatcher = FlowDispatcher(self.hass) + types = list(self.zeroconf_types) + # We want to make sure we know about other HomeAssistant + # instances as soon as possible to avoid name conflicts + # so we always browse for ZEROCONF_TYPE + for hk_type in (ZEROCONF_TYPE, *HOMEKIT_TYPES): + if hk_type not in self.zeroconf_types: + types.append(hk_type) + _LOGGER.debug("Starting Zeroconf browser") + self.service_browser = HaServiceBrowser( + self.zeroconf, types, handlers=[self.service_update] + ) + + async def async_stop(self) -> None: + """Cancel the service browser and stop processing the queue.""" + if self.service_browser: + await self.hass.async_add_executor_job(self.service_browser.cancel) + + @callback + def async_start(self) -> None: + """Start processing discovery flows.""" + assert self.flow_dispatcher is not None + self.flow_dispatcher.async_start() def service_update( + self, zeroconf: Zeroconf, service_type: str, name: str, state_change: ServiceStateChange, ) -> None: """Service state changed.""" - nonlocal zeroconf_types - nonlocal homekit_models - if state_change == ServiceStateChange.Removed: return - try: - service_info = zeroconf.get_service_info(service_type, name) - except ZeroconfError: - _LOGGER.exception("Failed to get info for device %s", name) - return - - if not service_info: - # Prevent the browser thread from collapsing as - # service_info can be None - _LOGGER.debug("Failed to get info for device %s", name) - return + service_info = ServiceInfo(service_type, name) + service_info.load_from_cache(zeroconf) info = info_from_service(service_info) if not info: @@ -297,10 +348,12 @@ async def _async_start_zeroconf_browser( return _LOGGER.debug("Discovered new device %s %s", name, info) + assert self.flow_dispatcher is not None # If we can handle it as a HomeKit discovery, we do that here. if service_type in HOMEKIT_TYPES: - discovery_was_forwarded = handle_homekit(hass, homekit_models, info) + if pending_flow := handle_homekit(self.hass, self.homekit_models, info): + self.flow_dispatcher.create(pending_flow) # Continue on here as homekit_controller # still needs to get updates on devices # so it can see when the 'c#' field is updated. @@ -308,10 +361,7 @@ async def _async_start_zeroconf_browser( # We only send updates to homekit_controller # if the device is already paired in order to avoid # offering a second discovery for the same device - if ( - discovery_was_forwarded - and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"] - ): + if pending_flow and HOMEKIT_PAIRED_STATUS_FLAG in info["properties"]: try: # 0 means paired and not discoverable by iOS clients) if int(info["properties"][HOMEKIT_PAIRED_STATUS_FLAG]): @@ -340,42 +390,37 @@ async def _async_start_zeroconf_browser( # Not all homekit types are currently used for discovery # so not all service type exist in zeroconf_types - for entry in zeroconf_types.get(service_type, []): - if len(entry) > 1: - if ( - uppercase_mac is not None - and "macaddress" in entry - and not fnmatch.fnmatch(uppercase_mac, entry["macaddress"]) + for matcher in self.zeroconf_types.get(service_type, []): + if len(matcher) > 1: + if "macaddress" in matcher and ( + uppercase_mac is None + or not fnmatch.fnmatch(uppercase_mac, matcher["macaddress"]) ): continue - if ( - lowercase_name is not None - and "name" in entry - and not fnmatch.fnmatch(lowercase_name, entry["name"]) + if "name" in matcher and ( + lowercase_name is None + or not fnmatch.fnmatch(lowercase_name, matcher["name"]) ): continue - if ( - lowercase_manufacturer is not None - and "manufacturer" in entry - and not fnmatch.fnmatch( - lowercase_manufacturer, entry["manufacturer"] + if "manufacturer" in matcher and ( + lowercase_manufacturer is None + or not fnmatch.fnmatch( + lowercase_manufacturer, matcher["manufacturer"] ) ): continue - hass.add_job( - hass.config_entries.flow.async_init( - entry["domain"], context={"source": DOMAIN}, data=info - ) # type: ignore - ) - - _LOGGER.debug("Starting Zeroconf browser") - HaServiceBrowser(zeroconf, types, handlers=[service_update]) + flow: ZeroconfFlow = { + "domain": matcher["domain"], + "context": {"source": config_entries.SOURCE_ZEROCONF}, + "data": info, + } + self.flow_dispatcher.create(flow) def handle_homekit( hass: HomeAssistant, homekit_models: dict[str, str], info: HaServiceInfo -) -> bool: +) -> ZeroconfFlow | None: """Handle a HomeKit discovery. Return if discovery was forwarded. @@ -389,26 +434,23 @@ def handle_homekit( break if model is None: - return False + return None for test_model in homekit_models: if ( model != test_model - and not model.startswith(f"{test_model} ") - and not model.startswith(f"{test_model}-") + and not model.startswith((f"{test_model} ", f"{test_model}-")) + and not fnmatch.fnmatch(model, test_model) ): continue - hass.add_job( - hass.config_entries.flow.async_init( - homekit_models[test_model], - context={"source": config_entries.SOURCE_HOMEKIT}, - data=info, - ) # type: ignore - ) - return True + return { + "domain": homekit_models[test_model], + "context": {"source": config_entries.SOURCE_HOMEKIT}, + "data": info, + } - return False + return None def info_from_service(service: ServiceInfo) -> HaServiceInfo | None: diff --git a/homeassistant/components/zeroconf/manifest.json b/homeassistant/components/zeroconf/manifest.json index 6e0c50e0683..030a970d77d 100644 --- a/homeassistant/components/zeroconf/manifest.json +++ b/homeassistant/components/zeroconf/manifest.json @@ -2,8 +2,8 @@ "domain": "zeroconf", "name": "Zero-configuration networking (zeroconf)", "documentation": "https://www.home-assistant.io/integrations/zeroconf", - "requirements": ["zeroconf==0.29.0","pyroute2==0.5.18"], - "dependencies": ["api"], + "requirements": ["zeroconf==0.31.0"], + "dependencies": ["network", "api"], "codeowners": ["@bdraco"], "quality_scale": "internal", "iot_class": "local_push" diff --git a/homeassistant/components/zeroconf/models.py b/homeassistant/components/zeroconf/models.py index 02a6fc7cdaa..c09e6428f2a 100644 --- a/homeassistant/components/zeroconf/models.py +++ b/homeassistant/components/zeroconf/models.py @@ -1,6 +1,10 @@ """Models for Zeroconf.""" +import asyncio +from typing import Any + from zeroconf import DNSPointer, DNSRecord, ServiceBrowser, Zeroconf +from zeroconf.asyncio import AsyncZeroconf class HaZeroconf(Zeroconf): @@ -12,6 +16,20 @@ class HaZeroconf(Zeroconf): ha_close = Zeroconf.close +class HaAsyncZeroconf(AsyncZeroconf): + """Home Assistant version of AsyncZeroconf.""" + + def __init__( # pylint: disable=super-init-not-called + self, *args: Any, **kwargs: Any + ) -> None: + """Wrap AsyncZeroconf.""" + self.zeroconf = HaZeroconf(*args, **kwargs) + self.loop = asyncio.get_running_loop() + + async def async_close(self) -> None: + """Fake method to avoid integrations closing it.""" + + class HaServiceBrowser(ServiceBrowser): """ServiceBrowser that only consumes DNSPointer records.""" diff --git a/homeassistant/components/zerproc/config_flow.py b/homeassistant/components/zerproc/config_flow.py index 6e3d70b0815..fdf17c14e5a 100644 --- a/homeassistant/components/zerproc/config_flow.py +++ b/homeassistant/components/zerproc/config_flow.py @@ -3,7 +3,6 @@ import logging import pyzerproc -from homeassistant import config_entries from homeassistant.helpers import config_entry_flow from .const import DOMAIN @@ -21,6 +20,4 @@ async def _async_has_devices(hass) -> bool: return False -config_entry_flow.register_discovery_flow( - DOMAIN, "Zerproc", _async_has_devices, config_entries.CONN_CLASS_LOCAL_POLL -) +config_entry_flow.register_discovery_flow(DOMAIN, "Zerproc", _async_has_devices) diff --git a/homeassistant/components/zerproc/light.py b/homeassistant/components/zerproc/light.py index d4bf6a98c70..ad14d9c506a 100644 --- a/homeassistant/components/zerproc/light.py +++ b/homeassistant/components/zerproc/light.py @@ -3,7 +3,6 @@ from __future__ import annotations from datetime import timedelta import logging -from typing import Callable import pyzerproc @@ -18,6 +17,7 @@ from homeassistant.config_entries import ConfigEntry from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import HomeAssistant from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.event import async_track_time_interval import homeassistant.util.color as color_util @@ -52,7 +52,7 @@ async def discover_entities(hass: HomeAssistant) -> list[Entity]: async def async_setup_entry( hass: HomeAssistant, config_entry: ConfigEntry, - async_add_entities: Callable[[list[Entity], bool], None], + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Zerproc light devices.""" warned = False diff --git a/homeassistant/components/zha/config_flow.py b/homeassistant/components/zha/config_flow.py index a1e161a8132..00aaf7c3625 100644 --- a/homeassistant/components/zha/config_flow.py +++ b/homeassistant/components/zha/config_flow.py @@ -31,7 +31,6 @@ class ZhaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 2 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize flow instance.""" diff --git a/homeassistant/components/zha/core/channels/__init__.py b/homeassistant/components/zha/core/channels/__init__.py index e6d2d722f61..3661d3b17d9 100644 --- a/homeassistant/components/zha/core/channels/__init__.py +++ b/homeassistant/components/zha/core/channels/__init__.py @@ -176,7 +176,7 @@ class Channels: class ChannelPool: """All channels of an endpoint.""" - def __init__(self, channels: Channels, ep_id: int): + def __init__(self, channels: Channels, ep_id: int) -> None: """Initialize instance.""" self._all_channels: ChannelsDict = {} self._channels: Channels = channels diff --git a/homeassistant/components/zha/core/const.py b/homeassistant/components/zha/core/const.py index c4c18c4304b..dcc2a080a76 100644 --- a/homeassistant/components/zha/core/const.py +++ b/homeassistant/components/zha/core/const.py @@ -168,6 +168,7 @@ DEBUG_COMP_BELLOWS = "bellows" DEBUG_COMP_ZHA = "homeassistant.components.zha" DEBUG_COMP_ZIGPY = "zigpy" DEBUG_COMP_ZIGPY_CC = "zigpy_cc" +DEBUG_COMP_ZIGPY_ZNP = "zigpy_znp" DEBUG_COMP_ZIGPY_DECONZ = "zigpy_deconz" DEBUG_COMP_ZIGPY_XBEE = "zigpy_xbee" DEBUG_COMP_ZIGPY_ZIGATE = "zigpy_zigate" @@ -178,6 +179,7 @@ DEBUG_LEVELS = { DEBUG_COMP_ZHA: logging.DEBUG, DEBUG_COMP_ZIGPY: logging.DEBUG, DEBUG_COMP_ZIGPY_CC: logging.DEBUG, + DEBUG_COMP_ZIGPY_ZNP: logging.DEBUG, DEBUG_COMP_ZIGPY_DECONZ: logging.DEBUG, DEBUG_COMP_ZIGPY_XBEE: logging.DEBUG, DEBUG_COMP_ZIGPY_ZIGATE: logging.DEBUG, @@ -256,7 +258,7 @@ class RadioType(enum.Enum): return radio.name raise ValueError - def __init__(self, description: str, controller_cls: CALLABLE_T): + def __init__(self, description: str, controller_cls: CALLABLE_T) -> None: """Init instance.""" self._desc = description self._ctrl_cls = controller_cls diff --git a/homeassistant/components/zha/core/device.py b/homeassistant/components/zha/core/device.py index 6497a85b8f9..37608287609 100644 --- a/homeassistant/components/zha/core/device.py +++ b/homeassistant/components/zha/core/device.py @@ -91,7 +91,7 @@ class ZHADevice(LogMixin): hass: HomeAssistant, zigpy_device: zha_typing.ZigpyDeviceType, zha_gateway: zha_typing.ZhaGatewayType, - ): + ) -> None: """Initialize the gateway.""" self.hass = hass self._zigpy_device = zigpy_device diff --git a/homeassistant/components/zha/core/group.py b/homeassistant/components/zha/core/group.py index 90dcb6fffc3..c8970b2d393 100644 --- a/homeassistant/components/zha/core/group.py +++ b/homeassistant/components/zha/core/group.py @@ -33,7 +33,7 @@ class ZHAGroupMember(LogMixin): def __init__( self, zha_group: ZhaGroupType, zha_device: ZhaDeviceType, endpoint_id: int - ): + ) -> None: """Initialize the group member.""" self._zha_group: ZhaGroupType = zha_group self._zha_device: ZhaDeviceType = zha_device @@ -116,7 +116,7 @@ class ZHAGroup(LogMixin): hass: HomeAssistant, zha_gateway: ZhaGatewayType, zigpy_group: ZigpyGroupType, - ): + ) -> None: """Initialize the group.""" self.hass: HomeAssistant = hass self._zigpy_group: ZigpyGroupType = zigpy_group diff --git a/homeassistant/components/zha/cover.py b/homeassistant/components/zha/cover.py index 5530cd3e3f5..35080c56921 100644 --- a/homeassistant/components/zha/cover.py +++ b/homeassistant/components/zha/cover.py @@ -183,7 +183,7 @@ class Shade(ZhaEntity, CoverEntity): zha_device: ZhaDeviceType, channels: list[ChannelType], **kwargs, - ): + ) -> None: """Initialize the ZHA light.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._on_off_channel = self.cluster_channels[CHANNEL_ON_OFF] diff --git a/homeassistant/components/zha/entity.py b/homeassistant/components/zha/entity.py index 2e5fe935435..a5259deea5d 100644 --- a/homeassistant/components/zha/entity.py +++ b/homeassistant/components/zha/entity.py @@ -40,7 +40,7 @@ UPDATE_GROUP_FROM_CHILD_DELAY = 0.2 class BaseZhaEntity(LogMixin, entity.Entity): """A base class for ZHA entities.""" - def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs): + def __init__(self, unique_id: str, zha_device: ZhaDeviceType, **kwargs) -> None: """Init ZHA entity.""" self._name: str = "" self._force_update: bool = False @@ -83,7 +83,7 @@ class BaseZhaEntity(LogMixin, entity.Entity): return self._should_poll @property - def device_info(self) -> dict[str, Any]: + def device_info(self) -> entity.DeviceInfo: """Return a device description for device registry.""" zha_device_info = self._zha_device.device_info ieee = zha_device_info["ieee"] @@ -147,7 +147,7 @@ class ZhaEntity(BaseZhaEntity, RestoreEntity): zha_device: ZhaDeviceType, channels: list[ChannelType], **kwargs, - ): + ) -> None: """Init ZHA entity.""" super().__init__(unique_id, zha_device, **kwargs) ieeetail = "".join([f"{o:02x}" for o in zha_device.ieee[:4]]) diff --git a/homeassistant/components/zha/lock.py b/homeassistant/components/zha/lock.py index 5684b22db6a..99f6230de0b 100644 --- a/homeassistant/components/zha/lock.py +++ b/homeassistant/components/zha/lock.py @@ -50,8 +50,7 @@ async def async_setup_entry(hass, config_entry, async_add_entities): ) hass.data[DATA_ZHA][DATA_ZHA_DISPATCHERS].append(unsub) - platform = entity_platform.current_platform.get() - assert platform + platform = entity_platform.async_get_current_platform() platform.async_register_entity_service( # type: ignore SERVICE_SET_LOCK_USER_CODE, diff --git a/homeassistant/components/zha/manifest.json b/homeassistant/components/zha/manifest.json index 42859f301b7..ec231ccc0e4 100644 --- a/homeassistant/components/zha/manifest.json +++ b/homeassistant/components/zha/manifest.json @@ -13,7 +13,7 @@ "zigpy==0.33.0", "zigpy-xbee==0.13.0", "zigpy-zigate==0.7.3", - "zigpy-znp==0.4.0" + "zigpy-znp==0.5.1" ], "codeowners": ["@dmulcahey", "@adminiuga"], "zeroconf": [ diff --git a/homeassistant/components/zha/sensor.py b/homeassistant/components/zha/sensor.py index d40638ecd71..714df80eebb 100644 --- a/homeassistant/components/zha/sensor.py +++ b/homeassistant/components/zha/sensor.py @@ -3,7 +3,7 @@ from __future__ import annotations import functools import numbers -from typing import Any, Callable +from typing import Any from homeassistant.components.sensor import ( DEVICE_CLASS_BATTERY, @@ -15,6 +15,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_PRESSURE, DEVICE_CLASS_TEMPERATURE, DOMAIN, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -28,6 +29,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.typing import StateType from .core import discovery @@ -72,7 +74,9 @@ STRICT_MATCH = functools.partial(ZHA_ENTITIES.strict_match, DOMAIN) async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up the Zigbee Home Automation sensor from config entry.""" entities_to_create = hass.data[DATA_ZHA][DOMAIN] @@ -98,6 +102,7 @@ class Sensor(ZhaEntity, SensorEntity): _device_class: str | None = None _divisor: int = 1 _multiplier: int = 1 + _state_class: str | None = None _unit: str | None = None def __init__( @@ -106,7 +111,7 @@ class Sensor(ZhaEntity, SensorEntity): zha_device: ZhaDeviceType, channels: list[ChannelType], **kwargs, - ): + ) -> None: """Init this sensor.""" super().__init__(unique_id, zha_device, channels, **kwargs) self._channel: ChannelType = channels[0] @@ -123,6 +128,11 @@ class Sensor(ZhaEntity, SensorEntity): """Return device class from component DEVICE_CLASSES.""" return self._device_class + @property + def state_class(self) -> str | None: + """Return the state class of this entity, from STATE_CLASSES, if any.""" + return self._state_class + @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity.""" @@ -169,6 +179,7 @@ class Battery(Sensor): SENSOR_ATTR = "battery_percentage_remaining" _device_class = DEVICE_CLASS_BATTERY + _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @staticmethod @@ -231,6 +242,7 @@ class Humidity(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_HUMIDITY _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT _unit = PERCENTAGE @@ -272,6 +284,7 @@ class Pressure(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_PRESSURE _decimals = 0 + _state_class = STATE_CLASS_MEASUREMENT _unit = PRESSURE_HPA @@ -282,6 +295,7 @@ class Temperature(Sensor): SENSOR_ATTR = "measured_value" _device_class = DEVICE_CLASS_TEMPERATURE _divisor = 100 + _state_class = STATE_CLASS_MEASUREMENT _unit = TEMP_CELSIUS diff --git a/homeassistant/components/zha/services.yaml b/homeassistant/components/zha/services.yaml index 63f30c2e3f1..0e645da365e 100644 --- a/homeassistant/components/zha/services.yaml +++ b/homeassistant/components/zha/services.yaml @@ -7,7 +7,6 @@ permit: duration: name: Duration description: Time to permit joins, in seconds - example: 60 default: 60 selector: number: @@ -77,24 +76,28 @@ set_zigbee_cluster_attribute: description: IEEE address for the device required: true example: "00:0d:6f:00:05:7d:2d:34" + selector: + text: endpoint_id: name: Endpoint ID description: Endpoint id for the cluster required: true - example: 1 + selector: + number: + min: 1 + max: 65535 + mode: box cluster_id: name: Cluster ID description: ZCL cluster to retrieve attributes for required: true - example: 6 selector: number: min: 1 max: 65535 cluster_type: name: Cluster Type - description: type of the cluster (in or out) - example: "out" + description: type of the cluster default: "in" selector: select: @@ -140,7 +143,6 @@ issue_zigbee_cluster_command: name: Endpoint ID description: Endpoint id for the cluster required: true - example: 1 selector: number: min: 1 @@ -149,15 +151,13 @@ issue_zigbee_cluster_command: name: Cluster ID description: ZCL cluster to retrieve attributes for required: true - example: 6 selector: number: min: 1 max: 65535 cluster_type: name: Cluster Type - description: type of the cluster (in or out) - example: "out" + description: type of the cluster default: "in" selector: select: @@ -168,16 +168,14 @@ issue_zigbee_cluster_command: name: Command description: id of the command to execute required: true - example: 0 selector: number: min: 1 max: 65535 command_type: name: Command Type - description: type of the command to execute (client or server) + description: type of the command to execute required: true - example: "server" selector: select: options: @@ -212,15 +210,13 @@ issue_zigbee_group_command: name: Cluster ID description: ZCL cluster to send command to required: true - example: 6 selector: number: min: 1 max: 65535 cluster_type: name: Cluster Type - description: type of the cluster (in or out) - example: "out" + description: type of the cluster default: "in" selector: select: @@ -231,7 +227,6 @@ issue_zigbee_group_command: name: Command description: id of the command to execute required: true - example: 0 selector: number: min: 1 @@ -265,7 +260,6 @@ warning_device_squawk: name: Mode description: >- The Squawk Mode field is used as a 4-bit enumeration, and can have one of the values shown in Table 8-24 of the ZCL spec - Squawk Mode Field. The exact operation of each mode (how the WD “squawks”) is implementation specific. - example: 1 default: 0 selector: number: @@ -276,7 +270,6 @@ warning_device_squawk: name: Strobe description: >- The strobe field is used as a Boolean, and determines if the visual indication is also required in addition to the audible squawk, as shown in Table 8-25 of the ZCL spec - Strobe Bit. - example: 1 default: 1 selector: number: @@ -287,7 +280,6 @@ warning_device_squawk: name: Level description: >- The squawk level field is used as a 2-bit enumeration, and determines the intensity of audible squawk sound as shown in Table 8-26 of the ZCL spec - Squawk Level Field Values. - example: 2 default: 2 selector: number: @@ -311,7 +303,6 @@ warning_device_warn: name: Mode description: >- The Warning Mode field is used as an 4-bit enumeration, can have one of the values 0-6 defined below in table 8-20 of the ZCL spec. The exact behavior of the WD device in each mode is according to the relevant security standards. - example: 1 default: 3 selector: number: @@ -322,7 +313,6 @@ warning_device_warn: name: Strobe description: >- The Strobe field is used as a 2-bit enumeration, and determines if the visual indication is required in addition to the audible siren, as indicated in Table 8-21 of the ZCL spec. "0" means no strobe, "1" means strobe. If the strobe field is “1” and the Warning Mode is “0” (“Stop”) then only the strobe is activated. - example: 1 default: 1 selector: number: @@ -333,7 +323,6 @@ warning_device_warn: name: Level description: >- The Siren Level field is used as a 2-bit enumeration, and indicates the intensity of audible squawk sound as shown in Table 8-22 of the ZCL spec. - example: 2 default: 2 selector: number: @@ -344,7 +333,6 @@ warning_device_warn: name: Duration description: >- Requested duration of warning, in seconds (16 bit). If both Strobe and Warning Mode are "0" this field SHALL be ignored. - example: 2 default: 5 selector: number: @@ -355,7 +343,6 @@ warning_device_warn: name: Duty cycle description: >- Indicates the length of the flash cycle. This provides a means of varying the flash duration for different alarm types (e.g., fire, police, burglar). Valid range is 0-100 in increments of 10. All other values SHALL be rounded to the nearest valid value. Strobe SHALL calculate duty cycle over a duration of one second. The ON state SHALL precede the OFF state. For example, if Strobe Duty Cycle Field specifies “40,” then the strobe SHALL flash ON for 4/10ths of a second and then turn OFF for 6/10ths of a second. - example: 50 default: 0 selector: number: @@ -366,7 +353,6 @@ warning_device_warn: name: Intensity description: >- Indicates the intensity of the strobe as shown in Table 8-23 of the ZCL spec. This attribute is designed to vary the output of the strobe (i.e., brightness) and not its frequency, which is detailed in section 8.4.2.3.1.6 of the ZCL spec. - example: 2 default: 2 selector: number: diff --git a/homeassistant/components/zha/strings.json b/homeassistant/components/zha/strings.json index 2c2a93aa4d6..9ca6a9821b3 100644 --- a/homeassistant/components/zha/strings.json +++ b/homeassistant/components/zha/strings.json @@ -1,6 +1,6 @@ { "config": { - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "user": { "title": "ZHA", diff --git a/homeassistant/components/zha/translations/ca.json b/homeassistant/components/zha/translations/ca.json index 20eee443960..0320ea34f2c 100644 --- a/homeassistant/components/zha/translations/ca.json +++ b/homeassistant/components/zha/translations/ca.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Ha fallat la connexi\u00f3" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Codi necessari per activar accions", + "alarm_failed_tries": "Nombre d'intents de codi erronis consecutius per disparar una alarma", + "alarm_master_code": "Codi mestre per als panells de control d'alarma", + "title": "Opcions del panell de control d'alarma" + }, + "zha_options": { + "default_light_transition": "Temps de transici\u00f3 predeterminat (segons)", + "enable_identify_on_join": "Activa l'efecte d'identificaci\u00f3 quan els dispositius s'uneixin a la xarxa", + "title": "Opcions globals" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/homeassistant/components/zha/translations/de.json b/homeassistant/components/zha/translations/de.json index f1dea3341d7..bea46792003 100644 --- a/homeassistant/components/zha/translations/de.json +++ b/homeassistant/components/zha/translations/de.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Verbindung fehlgeschlagen" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Code f\u00fcr Scharfschaltaktionen erforderlich", + "alarm_failed_tries": "Die Anzahl aufeinanderfolgender fehlgeschlagener Codeeintr\u00e4ge, um einen Alarm auszul\u00f6sen", + "alarm_master_code": "Mastercode f\u00fcr die Alarmzentrale(n)", + "title": "Optionen f\u00fcr die Alarmsteuerung" + }, + "zha_options": { + "default_light_transition": "Standardlicht\u00fcbergangszeit (Sekunden)", + "enable_identify_on_join": "Aktivieren Sie den Identifikationseffekt, wenn Ger\u00e4te dem Netzwerk beitreten", + "title": "Globale Optionen" + } + }, "device_automation": { "action_type": { "squawk": "Kreischen", diff --git a/homeassistant/components/zha/translations/en.json b/homeassistant/components/zha/translations/en.json index 2a6aa34d886..83dc56c75fc 100644 --- a/homeassistant/components/zha/translations/en.json +++ b/homeassistant/components/zha/translations/en.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Failed to connect" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { diff --git a/homeassistant/components/zha/translations/es.json b/homeassistant/components/zha/translations/es.json index 4fc089b26e9..5655d67fd34 100644 --- a/homeassistant/components/zha/translations/es.json +++ b/homeassistant/components/zha/translations/es.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "C\u00f3digo requerido para las acciones de armado", + "alarm_failed_tries": "El n\u00famero de entradas de c\u00f3digo fallidas consecutivas para activar una alarma", + "alarm_master_code": "C\u00f3digo maestro de la(s) central(es) de alarma", + "title": "Opciones del panel de control de la alarma" + }, + "zha_options": { + "default_light_transition": "Tiempo de transici\u00f3n de la luz por defecto (segundos)", + "enable_identify_on_join": "Activar el efecto de identificaci\u00f3n cuando los dispositivos se unen a la red", + "title": "Opciones globales" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/homeassistant/components/zha/translations/et.json b/homeassistant/components/zha/translations/et.json index d03300f9971..afbf2180ef6 100644 --- a/homeassistant/components/zha/translations/et.json +++ b/homeassistant/components/zha/translations/et.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u00dchendamine nurjus" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Valvestamise kood", + "alarm_failed_tries": "Mitu j\u00e4rjestikust eba\u00f5nnestunud koodi sisestamist h\u00e4ire k\u00e4ivitamiseks", + "alarm_master_code": "Valvekeskuse juhtpaneel(ide) \u00fclemkood", + "title": "Valvekeskuse juhtpaneeli s\u00e4tted" + }, + "zha_options": { + "default_light_transition": "Heleduse vaike\u00fclemineku aeg (sekundites)", + "enable_identify_on_join": "Luba tuvastamine kui seadmed liituvad v\u00f5rguga", + "title": "\u00dcldised valikud" + } + }, "device_automation": { "action_type": { "squawk": "Pr\u00e4\u00e4ksata", diff --git a/homeassistant/components/zha/translations/fr.json b/homeassistant/components/zha/translations/fr.json index 9e35ef9a541..75ba26ca809 100644 --- a/homeassistant/components/zha/translations/fr.json +++ b/homeassistant/components/zha/translations/fr.json @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Code requis pour les actions d'armement", + "alarm_failed_tries": "Le nombre de codes erron\u00e9s cons\u00e9cutifs pour d\u00e9clencher l'alarme", + "alarm_master_code": "Code principal pour le(s) panneau(x) de contr\u00f4le d'alarme", + "title": "Options du panneau de contr\u00f4le d'alarme" + }, + "zha_options": { + "default_light_transition": "Temps de transition de la lumi\u00e8re par d\u00e9faut (en secondes)", + "enable_identify_on_join": "Activer l'effet d'identification quand les appareils rejoignent le r\u00e9seau", + "title": "Options g\u00e9n\u00e9rales" + } + }, "device_automation": { "action_type": { "squawk": "Hurlement", diff --git a/homeassistant/components/zha/translations/it.json b/homeassistant/components/zha/translations/it.json index e97828d8e2a..247f6b4027e 100644 --- a/homeassistant/components/zha/translations/it.json +++ b/homeassistant/components/zha/translations/it.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Impossibile connettersi" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Codice necessario per le azioni di armamento", + "alarm_failed_tries": "Il numero di inserimenti consecutivi di codici falliti per attivare un allarme", + "alarm_master_code": "Codice principale per i pannelli di controllo degli allarmi", + "title": "Opzioni del pannello di controllo degli allarmi" + }, + "zha_options": { + "default_light_transition": "Tempo di transizione della luce predefinito (secondi)", + "enable_identify_on_join": "Abilita l'effetto di identificazione quando i dispositivi si uniscono alla rete", + "title": "Opzioni globali" + } + }, "device_automation": { "action_type": { "squawk": "Strillare", diff --git a/homeassistant/components/zha/translations/nl.json b/homeassistant/components/zha/translations/nl.json index 83e3426dcbb..86a3f7a7f69 100644 --- a/homeassistant/components/zha/translations/nl.json +++ b/homeassistant/components/zha/translations/nl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Kan geen verbinding maken" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Code vereist voor inschakelacties", + "alarm_failed_tries": "Het aantal opeenvolgende foute codes om het alarm te activeren", + "alarm_master_code": "Mastercode voor het alarm bedieningspaneel", + "title": "Alarm bedieningspaneel Opties" + }, + "zha_options": { + "default_light_transition": "Standaard licht transitietijd (seconden)", + "enable_identify_on_join": "Schakel het identificatie-effect in wanneer apparaten in het netwerk komen", + "title": "Globale opties" + } + }, "device_automation": { "action_type": { "squawk": "Schreeuw", diff --git a/homeassistant/components/zha/translations/no.json b/homeassistant/components/zha/translations/no.json index 3917ebd103b..087b4adb2c3 100644 --- a/homeassistant/components/zha/translations/no.json +++ b/homeassistant/components/zha/translations/no.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Tilkobling mislyktes" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kode kreves for tilkobling", + "alarm_failed_tries": "Antall p\u00e5f\u00f8lgende mislykkede kodeoppf\u00f8ringer for \u00e5 utl\u00f8se en alarm", + "alarm_master_code": "Hovedkode for alarmens kontrollpanel(er)", + "title": "Alternativer for alarmkontrollpanel" + }, + "zha_options": { + "default_light_transition": "Standard lysovergangstid (sekunder)", + "enable_identify_on_join": "Aktiver identifiseringseffekt n\u00e5r enheter blir med i nettverket", + "title": "Globale alternativer" + } + }, "device_automation": { "action_type": { "squawk": "Squawk", diff --git a/homeassistant/components/zha/translations/pl.json b/homeassistant/components/zha/translations/pl.json index f9b34d1be82..dce671771f3 100644 --- a/homeassistant/components/zha/translations/pl.json +++ b/homeassistant/components/zha/translations/pl.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "Nie mo\u017cna nawi\u0105za\u0107 po\u0142\u0105czenia" }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "Kod wymagany do akcji uzbrajania", + "alarm_failed_tries": "Liczba kolejnych nieudanych wpis\u00f3w kodu uruchamiaj\u0105ca alarm", + "alarm_master_code": "Kod g\u0142\u00f3wny panelu (paneli) alarmowego", + "title": "Opcje panelu alarmowego" + }, + "zha_options": { + "default_light_transition": "Domy\u015blny czas efektu przej\u015bcia dla \u015bwiat\u0142a (w sekundach)", + "enable_identify_on_join": "W\u0142\u0105cz efekt identyfikacji, gdy urz\u0105dzenia do\u0142\u0105czaj\u0105 do sieci", + "title": "Opcje og\u00f3lne" + } + }, "device_automation": { "action_type": { "squawk": "squawk", diff --git a/homeassistant/components/zha/translations/ru.json b/homeassistant/components/zha/translations/ru.json index e4084d0b2f6..291f5c0eea1 100644 --- a/homeassistant/components/zha/translations/ru.json +++ b/homeassistant/components/zha/translations/ru.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u0434\u043a\u043b\u044e\u0447\u0438\u0442\u044c\u0441\u044f." }, - "flow_title": "ZHA: {name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "\u041a\u043e\u0434, \u043d\u0435\u043e\u0431\u0445\u043e\u0434\u0438\u043c\u044b\u0439 \u0434\u043b\u044f \u043f\u043e\u0441\u0442\u0430\u043d\u043e\u0432\u043a\u0438 \u043d\u0430 \u043e\u0445\u0440\u0430\u043d\u0443", + "alarm_failed_tries": "\u041a\u043e\u043b\u0438\u0447\u0435\u0441\u0442\u0432\u043e \u043f\u043e\u0441\u043b\u0435\u0434\u043e\u0432\u0430\u0442\u0435\u043b\u044c\u043d\u044b\u0445 \u043d\u0435\u0443\u0434\u0430\u0447\u043d\u044b\u0445 \u0432\u0432\u043e\u0434\u043e\u0432 \u043a\u043e\u0434\u0430, \u0434\u043b\u044f \u0441\u0440\u0430\u0431\u0430\u0442\u044b\u0432\u0430\u043d\u0438\u044f \u0442\u0440\u0435\u0432\u043e\u0433\u0438", + "alarm_master_code": "\u041c\u0430\u0441\u0442\u0435\u0440-\u043a\u043e\u0434 \u0434\u043b\u044f \u043a\u043e\u043d\u0442\u0440\u043e\u043b\u044c\u043d\u044b\u0445 \u043f\u0430\u043d\u0435\u043b\u0435\u0439", + "title": "\u041e\u043f\u0446\u0438\u0438 \u043f\u0430\u043d\u0435\u043b\u0438 \u0443\u043f\u0440\u0430\u0432\u043b\u0435\u043d\u0438\u044f \u0441\u0438\u0433\u043d\u0430\u043b\u0438\u0437\u0430\u0446\u0438\u0435\u0439" + }, + "zha_options": { + "default_light_transition": "\u0412\u0440\u0435\u043c\u044f \u043f\u043b\u0430\u0432\u043d\u043e\u0433\u043e \u043f\u0435\u0440\u0435\u0445\u043e\u0434\u0430 \u0441\u0432\u0435\u0442\u0430 \u043f\u043e \u0443\u043c\u043e\u043b\u0447\u0430\u043d\u0438\u044e (\u0432 \u0441\u0435\u043a\u0443\u043d\u0434\u0430\u0445)", + "enable_identify_on_join": "\u042d\u0444\u0444\u0435\u043a\u0442 \u0434\u043b\u044f \u0438\u0434\u0435\u043d\u0442\u0438\u0444\u0438\u043a\u0430\u0446\u0438\u0438 \u043f\u0440\u0438\u0441\u043e\u0435\u0434\u0438\u043d\u0435\u043d\u0438\u044f \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0430 \u043a \u0441\u0435\u0442\u0438", + "title": "\u0413\u043b\u043e\u0431\u0430\u043b\u044c\u043d\u044b\u0435 \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0438" + } + }, "device_automation": { "action_type": { "squawk": "\u0422\u0440\u0430\u043d\u0441\u043f\u043e\u043d\u0434\u0435\u0440", diff --git a/homeassistant/components/zha/translations/zh-Hant.json b/homeassistant/components/zha/translations/zh-Hant.json index c1ad9b82262..43cc366ca35 100644 --- a/homeassistant/components/zha/translations/zh-Hant.json +++ b/homeassistant/components/zha/translations/zh-Hant.json @@ -6,7 +6,7 @@ "error": { "cannot_connect": "\u9023\u7dda\u5931\u6557" }, - "flow_title": "ZHA\uff1a{name}", + "flow_title": "{name}", "step": { "pick_radio": { "data": { @@ -33,6 +33,19 @@ } } }, + "config_panel": { + "zha_alarm_options": { + "alarm_arm_requires_code": "\u8b66\u6212\u52d5\u4f5c\u9700\u8981\u4ee3\u78bc", + "alarm_failed_tries": "\u9023\u7e8c\u8f38\u5165\u4ee3\u78bc\u89f8\u767c\u8b66\u5831\u932f\u8aa4\u6b21\u6578", + "alarm_master_code": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u63a7\u5236\u78bc", + "title": "\u8b66\u6212\u63a7\u5236\u9762\u677f\u9078\u9805" + }, + "zha_options": { + "default_light_transition": "\u9810\u8a2d\u71c8\u5149\u8f49\u63db\u6642\u9593\uff08\u79d2\uff09", + "enable_identify_on_join": "\u7576\u88dd\u7f6e\u52a0\u5165\u7db2\u8def\u6642\u3001\u958b\u555f\u8b58\u5225\u6548\u679c", + "title": "Global \u9078\u9805" + } + }, "device_automation": { "action_type": { "squawk": "\u61c9\u7b54", diff --git a/homeassistant/components/zone/__init__.py b/homeassistant/components/zone/__init__.py index b224c2a47d7..8ab0e9b2703 100644 --- a/homeassistant/components/zone/__init__.py +++ b/homeassistant/components/zone/__init__.py @@ -279,7 +279,7 @@ async def async_unload_entry( class Zone(entity.Entity): """Representation of a Zone.""" - def __init__(self, config: dict): + def __init__(self, config: dict) -> None: """Initialize the zone.""" self._config = config self.editable = True diff --git a/homeassistant/components/zone/services.yaml b/homeassistant/components/zone/services.yaml index 550eee24fab..2ce77132a53 100644 --- a/homeassistant/components/zone/services.yaml +++ b/homeassistant/components/zone/services.yaml @@ -1,2 +1,3 @@ reload: + name: Reload description: Reload the YAML-based zone configuration. diff --git a/homeassistant/components/zoneminder/services.yaml b/homeassistant/components/zoneminder/services.yaml index a6fb85b641d..74ab0cf5945 100644 --- a/homeassistant/components/zoneminder/services.yaml +++ b/homeassistant/components/zoneminder/services.yaml @@ -1,6 +1,11 @@ set_run_state: + name: Set run state description: Set the ZoneMinder run state fields: name: + name: Name description: The string name of the ZoneMinder run state to set as active. + required: true example: "Home" + selector: + text: diff --git a/homeassistant/components/zwave/config_flow.py b/homeassistant/components/zwave/config_flow.py index 18c5d2f6f4e..ce7aebd801a 100644 --- a/homeassistant/components/zwave/config_flow.py +++ b/homeassistant/components/zwave/config_flow.py @@ -14,12 +14,10 @@ from .const import ( ) -@config_entries.HANDLERS.register(DOMAIN) -class ZwaveFlowHandler(config_entries.ConfigFlow): +class ZwaveFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a Z-Wave config flow.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self): """Initialize the Z-Wave config flow.""" diff --git a/homeassistant/components/zwave/services.yaml b/homeassistant/components/zwave/services.yaml index b4a5db58986..d3063ef5d43 100644 --- a/homeassistant/components/zwave/services.yaml +++ b/homeassistant/components/zwave/services.yaml @@ -1,196 +1,411 @@ # Describes the format for available Z-Wave services change_association: + name: Change association description: Change an association in the Z-Wave network. fields: association: + name: Association description: Specify add or remove association + required: true example: add + selector: + text: node_id: + name: Node ID description: Node id of the node to set association for. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 target_node_id: + name: Target node ID description: Node id of the node to associate to. - example: 42 + required: true + selector: + number: + min: 1 + max: 255 group: + name: Group description: Group number to set association for. + required: true + selector: + number: + min: 1 + max: 5 instance: - description: (Optional) Instance of multichannel association. Defaults to 0. + name: Instance + description: Instance of multichannel association. + default: 0 + selector: + number: + min: 0 + max: 255 add_node: + name: Add node description: Add a new (unsecure) node to the Z-Wave network. Refer to OZW_Log.txt for progress. add_node_secure: + name: Add node secure description: Add a new node to the Z-Wave network 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. Refer to OZW_Log.txt for progress. cancel_command: + name: Cancel command description: Cancel a running Z-Wave controller command. Use this to exit add_node, if you weren't going to use it but activated it. heal_network: + name: Heal network description: Start a Z-Wave network heal. This might take a while and will slow down the Z-Wave network greatly while it is being processed. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the nodes to the controller. Defaults to False. - example: true + name: Return routes + description: Whether or not to update the return routes from the nodes to the controller. + default: false + selector: + boolean: heal_node: + name: Heal node description: Start a Z-Wave node heal. Refer to OZW_Log.txt for progress. fields: return_routes: - description: Whether or not to update the return routes from the node to the controller. Defaults to False. - example: true + name: Return routes + description: Whether or not to update the return routes from the node to the controller. + default: false + selector: + boolean: remove_node: + name: Remove node description: Remove a node from the Z-Wave network. Refer to OZW_Log.txt for progress. remove_failed_node: + name: Remove failed node description: This command will remove a failed node from the network. The node should be on the controller's failed nodes list, otherwise this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: - description: Node id of the device to remove (integer). - example: 10 + name: Node ID + description: Node id of the device to remove. + required: true + selector: + number: + min: 1 + max: 255 replace_failed_node: + name: Replace failed node description: Replace a failed node with another. If the node is not in the controller's failed nodes list, or the node responds, this command will fail. Refer to OZW_Log.txt for progress. fields: node_id: - description: Node id of the device to replace (integer). - example: 10 + name: Node ID + description: Node id of the device to replace. + required: true + selector: + number: + min: 1 + max: 255 set_config_parameter: + name: Set config parameter description: Set a config parameter to a node on the Z-Wave network. fields: node_id: - description: Node id of the device to set config parameter to (integer). + name: Node ID + description: Node id of the device to set config parameter to. + required: true + selector: + number: + min: 1 + max: 255 parameter: - description: Parameter number to set (integer). + name: Parameter + description: Parameter number to set. + required: true + selector: + number: + min: 1 + max: 255 value: + name: Value description: Value to set for parameter. (String value for list and bool parameters, integer for others). + required: true + selector: + text: size: - description: (Optional) Set the size of the parameter value. Only needed if no parameters are available. + name: Size + description: Set the size of the parameter value. Only needed if no parameters are available. + default: 2 + selector: + number: + min: 1 + max: 255 set_node_value: + name: Set node value description: Set the value for a given value_id on a Z-Wave device. fields: node_id: - description: Node id of the device to set the value on (integer). + name: Node ID + description: Node id of the device to set the value on. + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: Value id of the value to set (integer or string). + required: true + selector: + text: value: + name: Value description: Value to set (integer or string). + required: true + selector: + text: refresh_node_value: + name: Refresh node value description: Refresh the value for a given value_id on a Z-Wave device. fields: node_id: - description: Node id of the device to refresh value from (integer). + name: Node ID + description: Node id of the device to refresh value from. + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: Value id of the value to refresh. + required: true + selector: + text: set_poll_intensity: + name: Set poll intensity description: Set the polling interval to a nodes value fields: node_id: + name: Node ID description: ID of the node to set polling to. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: ID of the value to set polling to. example: 72037594255792737 + required: true + selector: + text: poll_intensity: + name: Poll intensity description: The intensity to poll, 0 = disabled, 1 = Every time through list, 2 = Every second time through list... - example: 2 + required: true + selector: + number: + min: 0 + max: 100 print_config_parameter: + name: Print configuration parameter description: Prints a Z-Wave node config parameter value to log. fields: node_id: - description: Node id of the device to print the parameter from (integer). + name: Node ID + description: Node id of the device to print the parameter from. + required: true + selector: + number: + min: 1 + max: 255 parameter: - description: Parameter number to print (integer). + name: Parameter + description: Parameter number to print. + required: true + selector: + number: + min: 1 + max: 255 print_node: + name: Print node description: Print all information about z-wave node. fields: node_id: + name: Node ID description: Node id of the device to print. + required: true + selector: + number: + min: 1 + max: 255 refresh_entity: + name: Refresh entity description: Refresh zwave entity. fields: entity_id: + name: Entity description: Name of the entity to refresh. - example: "light.leviton_vrmx11lz_multilevel_scene_switch_level_40" + required: true + selector: + entity: + integration: zwave refresh_node: + name: Refresh node description: Refresh zwave node. fields: node_id: + name: Node ID description: ID of the node to refresh. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 set_wakeup: + name: Set wakeup description: Sets wake-up interval of a node. fields: node_id: - description: Node id of the device to set the wake-up interval for. (integer) + name: Node ID + description: Node id of the device to set the wake-up interval for. + required: true + selector: + number: + min: 1 + max: 255 value: - description: Value of the interval to set. (integer) + name: Value + description: Value of the interval to set. + required: true + selector: + text: start_network: + name: Start network description: Start the Z-Wave network. This might take a while, depending on how big your Z-Wave network is. stop_network: + name: Stop network description: Stop the Z-Wave network, all updates into Home Assistant will stop. soft_reset: + name: Soft reset description: This will reset the controller without removing its data. Use carefully because not all controllers support this. Refer to your controller's manual. test_network: + name: Test network description: This will send test to nodes in the Z-Wave network. This will greatly slow down the Z-Wave network while it is being processed. Refer to OZW_Log.txt for progress. test_node: + name: Test node description: This will send test messages to a node in the Z-Wave network. This could bring back dead nodes. fields: node_id: + name: Node ID description: ID of the node to send test messages to. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 messages: - description: Optional. Amount of test messages to send. - example: 3 + name: Messages + description: Amount of test messages to send. + default: 1 + selector: + number: + min: 1 + max: 100 rename_node: + name: Rename node description: Set the name of a node. This will also affect the IDs of all entities in the node. fields: node_id: + name: Node ID description: ID of the node to rename. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 update_ids: - description: (optional) Rename the entity IDs for entities of this node. - example: true + name: Update IDs + description: Rename the entity IDs for entities of this node. + default: false + selector: + boolean: name: + name: Name description: New Name + required: true example: "kitchen" + selector: + text: rename_value: + name: Rename value description: Set the name of a node value. This will affect the ID of the value entity. Value IDs can be queried from /api/zwave/values/{node_id} fields: node_id: + name: Node ID description: ID of the node to rename. - example: 10 + required: true + selector: + number: + min: 1 + max: 255 value_id: + name: Value ID description: ID of the value to rename. example: 72037594255792737 + required: true + selector: + text: update_ids: - description: (optional) Update the entity ID for this value's entity. - example: true + name: Update IDs + description: Update the entity ID for this value's entity. + default: false + selector: + boolean: name: + name: Name description: New Name example: "Luminosity" + required: true + selector: + text: reset_node_meters: + name: Reset node meters description: Resets the meter counters of a node. fields: node_id: - description: Node id of the device to reset meters for. (integer) + name: Node ID + description: Node id of the device to reset meters for. + required: true + selector: + number: + min: 1 + max: 255 instance: - description: (Optional) Instance of association. Defaults to instance 1. + name: Instance + description: Instance of association. + default: 1 + selector: + number: + min: 1 + max: 100 diff --git a/homeassistant/components/zwave/translations/bg.json b/homeassistant/components/zwave/translations/bg.json index 191640ad0a0..ae82e98d705 100644 --- a/homeassistant/components/zwave/translations/bg.json +++ b/homeassistant/components/zwave/translations/bg.json @@ -12,8 +12,7 @@ "network_key": "\u041c\u0440\u0435\u0436\u043e\u0432 \u043a\u043b\u044e\u0447 (\u043e\u0441\u0442\u0430\u0432\u0435\u0442\u0435 \u043f\u0440\u0430\u0437\u043d\u043e \u0437\u0430 \u0430\u0432\u0442\u043e\u043c\u0430\u0442\u0438\u0447\u043d\u043e \u0433\u0435\u043d\u0435\u0440\u0438\u0440\u0430\u043d\u0435)", "usb_path": "USB \u043f\u044a\u0442" }, - "description": "\u0412\u0438\u0436\u0442\u0435 https://www.home-assistant.io/docs/z-wave/installation/ \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442\u043d\u043e\u0441\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0438", - "title": "\u041d\u0430\u0441\u0442\u0440\u043e\u0439\u0432\u0430\u043d\u0435 \u043d\u0430 Z-Wave" + "description": "\u0412\u0438\u0436\u0442\u0435 https://www.home-assistant.io/docs/z-wave/installation/ \u0437\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e\u0442\u043d\u043e\u0441\u043d\u043e \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u043e\u043d\u043d\u0438\u0442\u0435 \u043f\u0440\u043e\u043c\u0435\u043d\u043b\u0438\u0432\u0438" } } }, diff --git a/homeassistant/components/zwave/translations/ca.json b/homeassistant/components/zwave/translations/ca.json index 13805a2d1ed..9a5ef2b5010 100644 --- a/homeassistant/components/zwave/translations/ca.json +++ b/homeassistant/components/zwave/translations/ca.json @@ -13,8 +13,7 @@ "network_key": "Clau de xarxa (deixa-ho en blanc per generar-la autom\u00e0ticament)", "usb_path": "Ruta del port USB del dispositiu" }, - "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3", - "title": "Configuraci\u00f3 de Z-Wave" + "description": "Aquesta integraci\u00f3 ja no s'actualitzar\u00e0. Utilitza Z-Wave JS per a instal\u00b7lacions noves.\n\nConsulta https://www.home-assistant.io/docs/z-wave/installation/ per a m\u00e9s informaci\u00f3 sobre les variables de configuraci\u00f3" } } }, diff --git a/homeassistant/components/zwave/translations/cs.json b/homeassistant/components/zwave/translations/cs.json index 40488adfe52..320feafe08a 100644 --- a/homeassistant/components/zwave/translations/cs.json +++ b/homeassistant/components/zwave/translations/cs.json @@ -13,8 +13,7 @@ "network_key": "S\u00ed\u0165ov\u00fd kl\u00ed\u010d (ponechte pr\u00e1zdn\u00e9 pro automatick\u00e9 generov\u00e1n\u00ed)", "usb_path": "Cesta k USB za\u0159\u00edzen\u00ed" }, - "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch", - "title": "Nastavit Z-Wave" + "description": "Viz https://www.home-assistant.io/docs/z-wave/installation/ pro informace o konfigura\u010dn\u00edch prom\u011bnn\u00fdch" } } }, diff --git a/homeassistant/components/zwave/translations/da.json b/homeassistant/components/zwave/translations/da.json index effd2d5a98a..7bad51c4f8e 100644 --- a/homeassistant/components/zwave/translations/da.json +++ b/homeassistant/components/zwave/translations/da.json @@ -12,8 +12,7 @@ "network_key": "Netv\u00e6rksn\u00f8gle (efterlad blank for autogenerering)", "usb_path": "Sti til USB-enhed" }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler", - "title": "Ops\u00e6t Z-Wave" + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ for oplysninger om konfigurationsvariabler" } } }, diff --git a/homeassistant/components/zwave/translations/de.json b/homeassistant/components/zwave/translations/de.json index f592c2243ac..488580cb18a 100644 --- a/homeassistant/components/zwave/translations/de.json +++ b/homeassistant/components/zwave/translations/de.json @@ -13,8 +13,7 @@ "network_key": "Netzwerkschl\u00fcssel (leer lassen, um automatisch zu generieren)", "usb_path": "USB-Ger\u00e4t Pfad" }, - "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/", - "title": "Z-Wave einrichten" + "description": "Informationen zu den Konfigurationsvariablen findest du unter https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/en.json b/homeassistant/components/zwave/translations/en.json index 2fe3e15646a..5c7442fea05 100644 --- a/homeassistant/components/zwave/translations/en.json +++ b/homeassistant/components/zwave/translations/en.json @@ -13,8 +13,7 @@ "network_key": "Network Key (leave blank to auto-generate)", "usb_path": "USB Device Path" }, - "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables", - "title": "Set up Z-Wave" + "description": "This integration is no longer maintained. For new installations, use Z-Wave JS instead.\n\nSee https://www.home-assistant.io/docs/z-wave/installation/ for information on the configuration variables" } } }, diff --git a/homeassistant/components/zwave/translations/es-419.json b/homeassistant/components/zwave/translations/es-419.json index 0376714dd84..abcf85fffa1 100644 --- a/homeassistant/components/zwave/translations/es-419.json +++ b/homeassistant/components/zwave/translations/es-419.json @@ -12,8 +12,7 @@ "network_key": "Clave de red (dejar en blanco para auto-generar)", "usb_path": "Ruta USB" }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", - "title": "Configurar Z-Wave" + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" } } }, diff --git a/homeassistant/components/zwave/translations/es.json b/homeassistant/components/zwave/translations/es.json index 08408bf9d92..0d8b9a3020c 100644 --- a/homeassistant/components/zwave/translations/es.json +++ b/homeassistant/components/zwave/translations/es.json @@ -13,8 +13,7 @@ "network_key": "Clave de red (d\u00e9jelo en blanco para generar autom\u00e1ticamente)", "usb_path": "Ruta del dispositivo USB" }, - "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n", - "title": "Configurar Z-Wave" + "description": "Consulta https://www.home-assistant.io/docs/z-wave/installation/ para obtener informaci\u00f3n sobre las variables de configuraci\u00f3n" } } }, diff --git a/homeassistant/components/zwave/translations/et.json b/homeassistant/components/zwave/translations/et.json index b1fa6127076..922126e0d86 100644 --- a/homeassistant/components/zwave/translations/et.json +++ b/homeassistant/components/zwave/translations/et.json @@ -13,8 +13,7 @@ "network_key": "V\u00f5rguv\u00f5ti (j\u00e4ta automaatse genereerimise jaoks t\u00fchjaks)", "usb_path": "USB seadme rada" }, - "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/", - "title": "Seadista Z-Wave" + "description": "Seda sidumist enam ei hallata. Uueks sidumiseks kasuta Z-Wave JS.\n\nKonfiguratsioonimuutujate kohta leiad teavet https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/fi.json b/homeassistant/components/zwave/translations/fi.json index 5cddea71d32..90fb77b49e1 100644 --- a/homeassistant/components/zwave/translations/fi.json +++ b/homeassistant/components/zwave/translations/fi.json @@ -4,8 +4,7 @@ "user": { "data": { "usb_path": "USB-polku" - }, - "title": "Z-Waven m\u00e4\u00e4ritt\u00e4minen" + } } } }, diff --git a/homeassistant/components/zwave/translations/fr.json b/homeassistant/components/zwave/translations/fr.json index 6c23d35ac4e..03f6f9823ad 100644 --- a/homeassistant/components/zwave/translations/fr.json +++ b/homeassistant/components/zwave/translations/fr.json @@ -13,8 +13,7 @@ "network_key": "Cl\u00e9 r\u00e9seau (laisser vide pour g\u00e9n\u00e9rer automatiquement)", "usb_path": "Chemin du p\u00e9riph\u00e9rique USB" }, - "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration.", - "title": "Configurer Z-Wave" + "description": "Voir https://www.home-assistant.io/docs/z-wave/installation/ pour plus d'informations sur les variables de configuration." } } }, diff --git a/homeassistant/components/zwave/translations/hu.json b/homeassistant/components/zwave/translations/hu.json index 68a19863b53..7269ee32daf 100644 --- a/homeassistant/components/zwave/translations/hu.json +++ b/homeassistant/components/zwave/translations/hu.json @@ -13,8 +13,7 @@ "network_key": "H\u00e1l\u00f3zati kulcs (hagyja \u00fcresen az automatikus gener\u00e1l\u00e1shoz)", "usb_path": "USB eszk\u00f6z el\u00e9r\u00e9si \u00fat" }, - "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon.", - "title": "Z-Wave be\u00e1ll\u00edt\u00e1sa" + "description": "A konfigur\u00e1ci\u00f3s v\u00e1ltoz\u00f3kr\u00f3l az inform\u00e1ci\u00f3kat l\u00e1sd a https://www.home-assistant.io/docs/z-wave/installation/ oldalon." } } }, diff --git a/homeassistant/components/zwave/translations/id.json b/homeassistant/components/zwave/translations/id.json index 99bd6270326..91301f6e00e 100644 --- a/homeassistant/components/zwave/translations/id.json +++ b/homeassistant/components/zwave/translations/id.json @@ -13,8 +13,7 @@ "network_key": "Kunci Jaringan (biarkan kosong untuk dibuat secara otomatis)", "usb_path": "Jalur Perangkat USB" }, - "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi", - "title": "Siapkan Z-Wave" + "description": "Integrasi ini tidak lagi dipertahankan. Untuk instalasi baru, gunakan Z-Wave JS sebagai gantinya.\n\nBaca https://www.home-assistant.io/docs/z-wave/installation/ untuk informasi tentang variabel konfigurasi" } } }, diff --git a/homeassistant/components/zwave/translations/it.json b/homeassistant/components/zwave/translations/it.json index d3522cf0889..a99cc241633 100644 --- a/homeassistant/components/zwave/translations/it.json +++ b/homeassistant/components/zwave/translations/it.json @@ -13,8 +13,7 @@ "network_key": "Chiave di rete (lascia vuoto per generare automaticamente)", "usb_path": "Percorso del dispositivo USB" }, - "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione", - "title": "Configura Z-Wave" + "description": "Questa integrazione non viene pi\u00f9 mantenuta. Per le nuove installazioni, usa invece Z-Wave JS. \n\nVedere https://www.home-assistant.io/docs/z-wave/installation/ per informazioni sulle variabili di configurazione" } } }, diff --git a/homeassistant/components/zwave/translations/ko.json b/homeassistant/components/zwave/translations/ko.json index 674476ac759..613b4108d22 100644 --- a/homeassistant/components/zwave/translations/ko.json +++ b/homeassistant/components/zwave/translations/ko.json @@ -13,8 +13,7 @@ "network_key": "\ub124\ud2b8\uc6cc\ud06c \ud0a4 (\uacf5\ub780\uc73c\ub85c \ube44\uc6cc\ub450\uba74 \uc790\ub3d9 \uc0dd\uc131\ud569\ub2c8\ub2e4)", "usb_path": "USB \uc7a5\uce58 \uacbd\ub85c" }, - "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694", - "title": "Z-Wave \uc124\uc815" + "description": "\uc774 \ud1b5\ud569 \uad6c\uc131\uc694\uc18c\ub294 \ub354 \uc774\uc0c1 \uc9c0\uc6d0\ub418\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \uc0c8\ub85c\uc6b4 \uc124\uce58\uc758 \uacbd\uc6b0 Z-Wave JS \ub97c \uc0ac\uc6a9\ud574\uc8fc\uc138\uc694.\n\n\uad6c\uc131 \ubcc0\uc218\uc5d0 \ub300\ud55c \uc815\ubcf4\ub294 https://www.home-assistant.io/docs/z-wave/installation/ \uc744 \ucc38\uc870\ud574\uc8fc\uc138\uc694" } } }, diff --git a/homeassistant/components/zwave/translations/lb.json b/homeassistant/components/zwave/translations/lb.json index d2359ff4c61..e41d37b2025 100644 --- a/homeassistant/components/zwave/translations/lb.json +++ b/homeassistant/components/zwave/translations/lb.json @@ -13,8 +13,7 @@ "network_key": "Netzwierk Schl\u00ebssel (eidel loossen fir een automatesch z'erstellen)", "usb_path": "Pad zum USB Apparat" }, - "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen", - "title": "Z-Wave konfigur\u00e9ieren" + "description": "Lies op https://www.home-assistant.io/docs/z-wave/installation/ fir weider Informatiounen iwwert d'Konfiguratioun vun den Variabelen" } } }, diff --git a/homeassistant/components/zwave/translations/nl.json b/homeassistant/components/zwave/translations/nl.json index a366d1d50df..d8c58fe784c 100644 --- a/homeassistant/components/zwave/translations/nl.json +++ b/homeassistant/components/zwave/translations/nl.json @@ -13,8 +13,7 @@ "network_key": "Netwerksleutel (laat leeg om automatisch te genereren)", "usb_path": "USB-apparaatpad" }, - "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen", - "title": "Stel Z-Wave in" + "description": "Deze integratie wordt niet langer onderhouden. Voor nieuwe installaties, gebruik Z-Wave JS in plaats daarvan.\n\nZie https://www.home-assistant.io/docs/z-wave/installation/ voor informatie over de configuratievariabelen" } } }, diff --git a/homeassistant/components/zwave/translations/no.json b/homeassistant/components/zwave/translations/no.json index ab5a405f975..8582f906b57 100644 --- a/homeassistant/components/zwave/translations/no.json +++ b/homeassistant/components/zwave/translations/no.json @@ -13,8 +13,7 @@ "network_key": "Nettverksn\u00f8kkel (la v\u00e6re tom for automatisk oppretting)", "usb_path": "USB enhetsbane" }, - "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene", - "title": "Sett opp Z-Wave" + "description": "Denne integrasjonen opprettholdes ikke lenger. For nye installasjoner, bruk Z-Wave JS i stedet. \n\n Se https://www.home-assistant.io/docs/z-wave/installation/ for informasjon om konfigurasjonsvariablene" } } }, diff --git a/homeassistant/components/zwave/translations/pl.json b/homeassistant/components/zwave/translations/pl.json index 0a4b6a4828c..9706fb1721f 100644 --- a/homeassistant/components/zwave/translations/pl.json +++ b/homeassistant/components/zwave/translations/pl.json @@ -13,8 +13,7 @@ "network_key": "Klucz sieciowy (pozostaw pusty, by generowa\u0107 automatycznie)", "usb_path": "\u015acie\u017cka urz\u0105dzenia USB" }, - "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych", - "title": "Konfiguracja Z-Wave" + "description": "Ta integracja nie jest ju\u017c wspierana. Dla nowych instalacji, u\u017cyj Z-Wave JS.\n\nPrzejd\u017a na https://www.home-assistant.io/docs/z-wave/installation/, aby uzyska\u0107 informacje na temat zmiennych konfiguracyjnych" } } }, diff --git a/homeassistant/components/zwave/translations/pt-BR.json b/homeassistant/components/zwave/translations/pt-BR.json index e46a1bb14cb..8c20db13830 100644 --- a/homeassistant/components/zwave/translations/pt-BR.json +++ b/homeassistant/components/zwave/translations/pt-BR.json @@ -12,8 +12,7 @@ "network_key": "Chave de rede (deixe em branco para gerar automaticamente)", "usb_path": "Caminho do USB" }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o", - "title": "Configurar o Z-Wave" + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/zwave/translations/pt.json b/homeassistant/components/zwave/translations/pt.json index 74942081884..27fc303f08b 100644 --- a/homeassistant/components/zwave/translations/pt.json +++ b/homeassistant/components/zwave/translations/pt.json @@ -13,8 +13,7 @@ "network_key": "Network Key (deixe em branco para auto-gera\u00e7\u00e3o)", "usb_path": "Endere\u00e7o USB" }, - "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o", - "title": "Configurar o Z-Wave" + "description": "Consulte https://www.home-assistant.io/docs/z-wave/installation/ para obter informa\u00e7\u00f5es sobre as vari\u00e1veis de configura\u00e7\u00e3o" } } }, diff --git a/homeassistant/components/zwave/translations/ro.json b/homeassistant/components/zwave/translations/ro.json index aa644cde3fa..de199bea03b 100644 --- a/homeassistant/components/zwave/translations/ro.json +++ b/homeassistant/components/zwave/translations/ro.json @@ -12,8 +12,7 @@ "network_key": "Cheie de re\u021bea (l\u0103sa\u021bi necompletat pentru a genera automat)", "usb_path": "Cale USB" }, - "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare", - "title": "Configura\u021bi Z-Wave" + "description": "Vede\u021bi https://www.home-assistant.io/docs/z-wave/installation/ pentru informa\u021bii despre variabilele de configurare" } } }, diff --git a/homeassistant/components/zwave/translations/ru.json b/homeassistant/components/zwave/translations/ru.json index 5188bb8330e..7b7f0c6733d 100644 --- a/homeassistant/components/zwave/translations/ru.json +++ b/homeassistant/components/zwave/translations/ru.json @@ -13,8 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u0441\u0435\u0442\u0438 (\u043e\u0441\u0442\u0430\u0432\u044c\u0442\u0435 \u043f\u0443\u0441\u0442\u044b\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u041f\u0443\u0442\u044c \u043a USB-\u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u0443" }, - "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Z-Wave" + "description": "\u042d\u0442\u0430 \u0438\u043d\u0442\u0435\u0433\u0440\u0430\u0446\u0438\u044f \u0431\u043e\u043b\u044c\u0448\u0435 \u043d\u0435 \u043f\u043e\u0434\u0434\u0435\u0440\u0436\u0438\u0432\u0430\u0435\u0442\u0441\u044f. \u0420\u0435\u043a\u043e\u043c\u0435\u043d\u0434\u0443\u0435\u0442\u0441\u044f \u0438\u0441\u043f\u043e\u043b\u044c\u0437\u043e\u0432\u0430\u0442\u044c \u0432\u043c\u0435\u0441\u0442\u043e \u043d\u0435\u0451 Z-Wave JS.\n\n\u041e\u0437\u043d\u0430\u043a\u043e\u043c\u044c\u0442\u0435\u0441\u044c \u0441 [\u0438\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0438\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043f\u043e\u043b\u0443\u0447\u0435\u043d\u0438\u044f \u0431\u043e\u043b\u0435\u0435 \u043f\u043e\u0434\u0440\u043e\u0431\u043d\u043e\u0439 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u0438 \u043e \u043d\u0430\u0441\u0442\u0440\u043e\u0439\u043a\u0435 \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." } } }, diff --git a/homeassistant/components/zwave/translations/sl.json b/homeassistant/components/zwave/translations/sl.json index f84f4c926ee..38e70c59652 100644 --- a/homeassistant/components/zwave/translations/sl.json +++ b/homeassistant/components/zwave/translations/sl.json @@ -12,8 +12,7 @@ "network_key": "Omre\u017eni klju\u010d (pustite prazno za samodejno generiranje)", "usb_path": "USB Pot" }, - "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/", - "title": "Nastavite Z-Wave" + "description": "Za informacije o konfiguracijskih spremenljivka si oglejte https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/sv.json b/homeassistant/components/zwave/translations/sv.json index 5d5e5adc210..6d3af30a057 100644 --- a/homeassistant/components/zwave/translations/sv.json +++ b/homeassistant/components/zwave/translations/sv.json @@ -12,8 +12,7 @@ "network_key": "N\u00e4tverksnyckel (l\u00e4mna blank f\u00f6r automatisk generering)", "usb_path": "USB-s\u00f6kv\u00e4g" }, - "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler", - "title": "St\u00e4lla in Z-Wave" + "description": "Se https://www.home-assistant.io/docs/z-wave/installation/ f\u00f6r information om konfigurationsvariabler" } } }, diff --git a/homeassistant/components/zwave/translations/uk.json b/homeassistant/components/zwave/translations/uk.json index 5cdd6060cc4..696c0caccd2 100644 --- a/homeassistant/components/zwave/translations/uk.json +++ b/homeassistant/components/zwave/translations/uk.json @@ -13,8 +13,7 @@ "network_key": "\u041a\u043b\u044e\u0447 \u043c\u0435\u0440\u0435\u0436\u0456 (\u0437\u0430\u043b\u0438\u0448\u0442\u0435 \u043f\u043e\u0440\u043e\u0436\u043d\u0456\u043c \u0434\u043b\u044f \u0430\u0432\u0442\u043e\u0433\u0435\u043d\u0435\u0440\u0430\u0446\u0438\u0438)", "usb_path": "\u0428\u043b\u044f\u0445 \u0434\u043e USB-\u043f\u0440\u0438\u0441\u0442\u0440\u043e\u044e" }, - "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430.", - "title": "Z-Wave" + "description": "\u041e\u0437\u043d\u0430\u0439\u043e\u043c\u0442\u0435\u0441\u044f \u0437 [\u0456\u043d\u0441\u0442\u0440\u0443\u043a\u0446\u0456\u044f\u043c\u0438](https://www.home-assistant.io/docs/z-wave/installation/) \u0434\u043b\u044f \u043e\u0442\u0440\u0438\u043c\u0430\u043d\u043d\u044f \u0431\u0456\u043b\u044c\u0448 \u0434\u043e\u043a\u043b\u0430\u0434\u043d\u043e\u0457 \u0456\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0456\u0457 \u043f\u0440\u043e \u043d\u0430\u043b\u0430\u0448\u0442\u0443\u0432\u0430\u043d\u043d\u044f \u043a\u043e\u043c\u043f\u043e\u043d\u0435\u043d\u0442\u0430." } } }, diff --git a/homeassistant/components/zwave/translations/zh-Hans.json b/homeassistant/components/zwave/translations/zh-Hans.json index c6a220617c1..64af99e21ff 100644 --- a/homeassistant/components/zwave/translations/zh-Hans.json +++ b/homeassistant/components/zwave/translations/zh-Hans.json @@ -12,8 +12,7 @@ "network_key": "\u7f51\u7edc\u5bc6\u94a5\uff08\u7559\u7a7a\u5c06\u81ea\u52a8\u751f\u6210\uff09", "usb_path": "USB \u8def\u5f84" }, - "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/", - "title": "\u8bbe\u7f6e Z-Wave" + "description": "\u6709\u5173\u914d\u7f6e\u7684\u4fe1\u606f\uff0c\u8bf7\u53c2\u9605 https://www.home-assistant.io/docs/z-wave/installation/" } } }, diff --git a/homeassistant/components/zwave/translations/zh-Hant.json b/homeassistant/components/zwave/translations/zh-Hant.json index d786a6b881e..4be9b77a8c6 100644 --- a/homeassistant/components/zwave/translations/zh-Hant.json +++ b/homeassistant/components/zwave/translations/zh-Hant.json @@ -13,8 +13,7 @@ "network_key": "\u7db2\u8def\u5bc6\u9470\uff08\u4fdd\u7559\u7a7a\u767d\u5c07\u6703\u81ea\u52d5\u7522\u751f\uff09", "usb_path": "USB \u88dd\u7f6e\u8def\u5f91" }, - "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a", - "title": "\u8a2d\u5b9a Z-Wave" + "description": "\u6b64\u6574\u5408\u5df2\u7d93\u4e0d\u518d\u9032\u884c\u7dad\u8b77\uff0c\u8acb\u4f7f\u7528 Z-Wave JS \u53d6\u4ee3\u70ba\u65b0\u5b89\u88dd\u65b9\u5f0f\u3002\n\n\u8acb\u53c3\u95b1 https://www.home-assistant.io/docs/z-wave/installation/ \u4ee5\n\u7372\u5f97\u8a2d\u5b9a\u8b8a\u6578\u8cc7\u8a0a" } } }, diff --git a/homeassistant/components/zwave_js/__init__.py b/homeassistant/components/zwave_js/__init__.py index 8e980e19765..520495d5071 100644 --- a/homeassistant/components/zwave_js/__init__.py +++ b/homeassistant/components/zwave_js/__init__.py @@ -29,7 +29,7 @@ from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send -from .addon import AddonError, AddonManager, get_addon_manager +from .addon import AddonError, AddonManager, AddonState, get_addon_manager from .api import async_register_api from .const import ( ATTR_COMMAND_CLASS, @@ -51,6 +51,8 @@ from .const import ( ATTR_TYPE, ATTR_VALUE, ATTR_VALUE_RAW, + CONF_ADDON_DEVICE, + CONF_ADDON_NETWORK_KEY, CONF_DATA_COLLECTION_OPTED_IN, CONF_INTEGRATION_CREATED_ADDON, CONF_NETWORK_KEY, @@ -559,27 +561,38 @@ async def async_ensure_addon_running(hass: HomeAssistant, entry: ConfigEntry) -> if addon_manager.task_in_progress(): raise ConfigEntryNotReady try: - addon_is_installed = await addon_manager.async_is_addon_installed() - addon_is_running = await addon_manager.async_is_addon_running() + addon_info = await addon_manager.async_get_addon_info() except AddonError as err: - LOGGER.error("Failed to get the Z-Wave JS add-on info") + LOGGER.error(err) raise ConfigEntryNotReady from err usb_path: str = entry.data[CONF_USB_PATH] network_key: str = entry.data[CONF_NETWORK_KEY] + addon_state = addon_info.state - if not addon_is_installed: + if addon_state == AddonState.NOT_INSTALLED: addon_manager.async_schedule_install_setup_addon( usb_path, network_key, catch_error=True ) raise ConfigEntryNotReady - if not addon_is_running: + if addon_state == AddonState.NOT_RUNNING: addon_manager.async_schedule_setup_addon( usb_path, network_key, catch_error=True ) raise ConfigEntryNotReady + addon_options = addon_info.options + addon_device = addon_options[CONF_ADDON_DEVICE] + addon_network_key = addon_options[CONF_ADDON_NETWORK_KEY] + updates = {} + if usb_path != addon_device: + updates[CONF_USB_PATH] = addon_device + if network_key != addon_network_key: + updates[CONF_NETWORK_KEY] = addon_network_key + if updates: + hass.config_entries.async_update_entry(entry, data={**entry.data, **updates}) + @callback def async_ensure_addon_updated(hass: HomeAssistant) -> None: diff --git a/homeassistant/components/zwave_js/addon.py b/homeassistant/components/zwave_js/addon.py index 1413ea06de1..ff74b5d5a44 100644 --- a/homeassistant/components/zwave_js/addon.py +++ b/homeassistant/components/zwave_js/addon.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from dataclasses import dataclass +from enum import Enum from functools import partial from typing import Any, Callable, TypeVar, cast @@ -55,6 +57,26 @@ def api_error(error_message: str) -> Callable[[F], F]: return handle_hassio_api_error +@dataclass +class AddonInfo: + """Represent the current add-on info state.""" + + options: dict[str, Any] + state: AddonState + update_available: bool + version: str | None + + +class AddonState(Enum): + """Represent the current state of the add-on.""" + + NOT_INSTALLED = "not_installed" + INSTALLING = "installing" + UPDATING = "updating" + NOT_RUNNING = "not_running" + RUNNING = "running" + + class AddonManager: """Manage the add-on. @@ -93,25 +115,32 @@ class AddonManager: return discovery_info_config @api_error("Failed to get the Z-Wave JS add-on info") - async def async_get_addon_info(self) -> dict: + async def async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" addon_info: dict = await async_get_addon_info(self._hass, ADDON_SLUG) - return addon_info + addon_state = self.async_get_addon_state(addon_info) + return AddonInfo( + options=addon_info["options"], + state=addon_state, + update_available=addon_info["update_available"], + version=addon_info["version"], + ) - async def async_is_addon_running(self) -> bool: - """Return True if Z-Wave JS add-on is running.""" - addon_info = await self.async_get_addon_info() - return bool(addon_info["state"] == "started") + @callback + def async_get_addon_state(self, addon_info: dict[str, Any]) -> AddonState: + """Return the current state of the Z-Wave JS add-on.""" + addon_state = AddonState.NOT_INSTALLED - async def async_is_addon_installed(self) -> bool: - """Return True if Z-Wave JS add-on is installed.""" - addon_info = await self.async_get_addon_info() - return addon_info["version"] is not None + if addon_info["version"] is not None: + addon_state = AddonState.NOT_RUNNING + if addon_info["state"] == "started": + addon_state = AddonState.RUNNING + if self._install_task and not self._install_task.done(): + addon_state = AddonState.INSTALLING + if self._update_task and not self._update_task.done(): + addon_state = AddonState.UPDATING - async def async_get_addon_options(self) -> dict: - """Get Z-Wave JS add-on options.""" - addon_info = await self.async_get_addon_info() - return cast(dict, addon_info["options"]) + return addon_state @api_error("Failed to set the Z-Wave JS add-on options") async def async_set_addon_options(self, config: dict) -> None: @@ -164,13 +193,11 @@ class AddonManager: async def async_update_addon(self) -> None: """Update the Z-Wave JS add-on if needed.""" addon_info = await self.async_get_addon_info() - addon_version = addon_info["version"] - update_available = addon_info["update_available"] - if addon_version is None: + if addon_info.version is None: raise AddonError("Z-Wave JS add-on is not installed") - if not update_available: + if not addon_info.update_available: return await self.async_create_snapshot() @@ -215,14 +242,14 @@ class AddonManager: async def async_configure_addon(self, usb_path: str, network_key: str) -> None: """Configure and start Z-Wave JS add-on.""" - addon_options = await self.async_get_addon_options() + addon_info = await self.async_get_addon_info() new_addon_options = { CONF_ADDON_DEVICE: usb_path, CONF_ADDON_NETWORK_KEY: network_key, } - if new_addon_options != addon_options: + if new_addon_options != addon_info.options: await self.async_set_addon_options(new_addon_options) @callback @@ -246,8 +273,7 @@ class AddonManager: async def async_create_snapshot(self) -> None: """Create a partial snapshot of the Z-Wave JS add-on.""" addon_info = await self.async_get_addon_info() - addon_version = addon_info["version"] - name = f"addon_{ADDON_SLUG}_{addon_version}" + name = f"addon_{ADDON_SLUG}_{addon_info.version}" LOGGER.debug("Creating snapshot: %s", name) await async_create_snapshot( diff --git a/homeassistant/components/zwave_js/api.py b/homeassistant/components/zwave_js/api.py index 81600ec6c16..ffd00919941 100644 --- a/homeassistant/components/zwave_js/api.py +++ b/homeassistant/components/zwave_js/api.py @@ -2,16 +2,26 @@ from __future__ import annotations import dataclasses -from functools import wraps +from functools import partial, wraps import json from typing import Callable -from aiohttp import hdrs, web, web_exceptions +from aiohttp import hdrs, web, web_exceptions, web_request import voluptuous as vol from zwave_js_server import dump from zwave_js_server.client import Client from zwave_js_server.const import CommandClass, LogLevel -from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed +from zwave_js_server.exceptions import ( + BaseZwaveJSServerError, + InvalidNewValue, + NotFoundError, + SetValueFailed, +) +from zwave_js_server.firmware import begin_firmware_update +from zwave_js_server.model.firmware import ( + FirmwareUpdateFinished, + FirmwareUpdateProgress, +) from zwave_js_server.model.log_config import LogConfig from zwave_js_server.model.log_message import LogMessage from zwave_js_server.model.node import Node @@ -25,9 +35,10 @@ from homeassistant.components.websocket_api.const import ( ERR_NOT_SUPPORTED, ERR_UNKNOWN_ERROR, ) -from homeassistant.config_entries import ENTRY_STATE_LOADED, ConfigEntry +from homeassistant.config_entries import ConfigEntry, ConfigEntryState from homeassistant.const import CONF_URL from homeassistant.core import HomeAssistant, callback +from homeassistant.exceptions import Unauthorized from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.device_registry import DeviceEntry @@ -51,6 +62,7 @@ TYPE = "type" PROPERTY = "property" PROPERTY_KEY = "property_key" VALUE = "value" +SECURE = "secure" # constants for log config commands CONFIG = "config" @@ -85,7 +97,7 @@ def async_get_entry(orig_func: Callable) -> Callable: ) return - if entry.state != ENTRY_STATE_LOADED: + if entry.state is not ConfigEntryState.LOADED: connection.send_error( msg[ID], ERR_NOT_LOADED, f"Config entry {entry_id} not loaded" ) @@ -126,30 +138,47 @@ def async_register_api(hass: HomeAssistant) -> None: """Register all of our api endpoints.""" websocket_api.async_register_command(hass, websocket_network_status) websocket_api.async_register_command(hass, websocket_node_status) + websocket_api.async_register_command(hass, websocket_node_metadata) + websocket_api.async_register_command(hass, websocket_ping_node) websocket_api.async_register_command(hass, websocket_add_node) websocket_api.async_register_command(hass, websocket_stop_inclusion) - websocket_api.async_register_command(hass, websocket_remove_node) websocket_api.async_register_command(hass, websocket_stop_exclusion) + websocket_api.async_register_command(hass, websocket_remove_node) + websocket_api.async_register_command(hass, websocket_remove_failed_node) + websocket_api.async_register_command(hass, websocket_replace_failed_node) + websocket_api.async_register_command(hass, websocket_begin_healing_network) + websocket_api.async_register_command( + hass, websocket_subscribe_heal_network_progress + ) + websocket_api.async_register_command(hass, websocket_stop_healing_network) websocket_api.async_register_command(hass, websocket_refresh_node_info) websocket_api.async_register_command(hass, websocket_refresh_node_values) websocket_api.async_register_command(hass, websocket_refresh_node_cc_values) + websocket_api.async_register_command(hass, websocket_heal_node) + websocket_api.async_register_command(hass, websocket_set_config_parameter) + websocket_api.async_register_command(hass, websocket_get_config_parameters) websocket_api.async_register_command(hass, websocket_subscribe_logs) websocket_api.async_register_command(hass, websocket_update_log_config) websocket_api.async_register_command(hass, websocket_get_log_config) - websocket_api.async_register_command(hass, websocket_get_config_parameters) - websocket_api.async_register_command(hass, websocket_set_config_parameter) websocket_api.async_register_command( hass, websocket_update_data_collection_preference ) websocket_api.async_register_command(hass, websocket_data_collection_status) - hass.http.register_view(DumpView) # type: ignore + websocket_api.async_register_command(hass, websocket_abort_firmware_update) + websocket_api.async_register_command( + hass, websocket_subscribe_firmware_update_status + ) + websocket_api.async_register_command(hass, websocket_check_for_config_updates) + websocket_api.async_register_command(hass, websocket_install_config_update) + hass.http.register_view(DumpView()) + hass.http.register_view(FirmwareUploadView()) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( {vol.Required(TYPE): "zwave_js/network_status", vol.Required(ENTRY_ID): str} ) +@websocket_api.async_response @async_get_entry async def websocket_network_status( hass: HomeAssistant, @@ -159,6 +188,7 @@ async def websocket_network_status( client: Client, ) -> None: """Get the status of the Z-Wave JS network.""" + controller = client.driver.controller data = { "client": { "ws_server_url": client.ws_server_url, @@ -167,7 +197,24 @@ async def websocket_network_status( "server_version": client.version.server_version, }, "controller": { - "home_id": client.driver.controller.data["homeId"], + "home_id": controller.home_id, + "library_version": controller.library_version, + "type": controller.controller_type, + "own_node_id": controller.own_node_id, + "is_secondary": controller.is_secondary, + "is_using_home_id_from_other_network": controller.is_using_home_id_from_other_network, + "is_sis_present": controller.is_SIS_present, + "was_real_primary": controller.was_real_primary, + "is_static_update_controller": controller.is_static_update_controller, + "is_slave": controller.is_slave, + "serial_api_version": controller.serial_api_version, + "manufacturer_id": controller.manufacturer_id, + "product_id": controller.product_id, + "product_type": controller.product_type, + "supported_function_types": controller.supported_function_types, + "suc_node_id": controller.suc_node_id, + "supports_timers": controller.supports_timers, + "is_heal_network_active": controller.is_heal_network_active, "nodes": list(client.driver.controller.nodes), }, } @@ -177,7 +224,6 @@ async def websocket_network_status( ) -@websocket_api.async_response # type: ignore @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/node_status", @@ -185,6 +231,7 @@ async def websocket_network_status( vol.Required(NODE_ID): int, } ) +@websocket_api.async_response @async_get_node async def websocket_node_status( hass: HomeAssistant, @@ -206,15 +253,69 @@ async def websocket_node_status( ) -@websocket_api.require_admin # type: ignore +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/node_metadata", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) @websocket_api.async_response +@async_get_node +async def websocket_node_metadata( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Get the metadata of a Z-Wave JS node.""" + data = { + "node_id": node.node_id, + "exclusion": node.device_config.metadata.exclusion, + "inclusion": node.device_config.metadata.inclusion, + "manual": node.device_config.metadata.manual, + "wakeup": node.device_config.metadata.wakeup, + "reset": node.device_config.metadata.reset, + "device_database_url": node.device_database_url, + } + connection.send_result( + msg[ID], + data, + ) + + +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/ping_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_ping_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Ping a Z-Wave JS node.""" + result = await node.async_ping() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/add_node", vol.Required(ENTRY_ID): str, - vol.Optional("secure", default=False): bool, + vol.Optional(SECURE, default=False): bool, } ) +@websocket_api.async_response @async_get_entry async def websocket_add_node( hass: HomeAssistant, @@ -225,7 +326,7 @@ async def websocket_add_node( ) -> None: """Add a node to the Z-Wave network.""" controller = client.driver.controller - include_non_secure = not msg["secure"] + include_non_secure = not msg[SECURE] @callback def async_cleanup() -> None: @@ -239,9 +340,24 @@ async def websocket_add_node( websocket_api.event_message(msg[ID], {"event": event["event"]}) ) + @callback + def forward_stage(event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "stage": event["stageName"]} + ) + ) + @callback def node_added(event: dict) -> None: node = event["node"] + interview_unsubs = [ + node.on("interview started", forward_event), + node.on("interview completed", forward_event), + node.on("interview stage completed", forward_stage), + node.on("interview failed", forward_event), + ] + unsubs.extend(interview_unsubs) node_details = { "node_id": node.node_id, "status": node.status, @@ -255,7 +371,12 @@ async def websocket_add_node( @callback def device_registered(device: DeviceEntry) -> None: - device_details = {"name": device.name, "id": device.id} + device_details = { + "name": device.name, + "id": device.id, + "manufacturer": device.manufacturer, + "model": device.model, + } connection.send_message( websocket_api.event_message( msg[ID], {"event": "device registered", "device": device_details} @@ -280,14 +401,14 @@ async def websocket_add_node( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/stop_inclusion", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_stop_inclusion( hass: HomeAssistant, @@ -305,14 +426,14 @@ async def websocket_stop_inclusion( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/stop_exclusion", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_stop_exclusion( hass: HomeAssistant, @@ -330,14 +451,14 @@ async def websocket_stop_exclusion( ) -@websocket_api.require_admin # type:ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/remove_node", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_remove_node( hass: HomeAssistant, @@ -389,8 +510,286 @@ async def websocket_remove_node( ) -@websocket_api.require_admin # type: ignore +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/replace_failed_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + vol.Optional(SECURE, default=False): bool, + } +) @websocket_api.async_response +@async_get_entry +async def websocket_replace_failed_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Replace a failed node with a new node.""" + controller = client.driver.controller + include_non_secure = not msg[SECURE] + node_id = msg[NODE_ID] + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(event: dict) -> None: + connection.send_message( + websocket_api.event_message(msg[ID], {"event": event["event"]}) + ) + + @callback + def forward_stage(event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "stage": event["stageName"]} + ) + ) + + @callback + def node_added(event: dict) -> None: + node = event["node"] + interview_unsubs = [ + node.on("interview started", forward_event), + node.on("interview completed", forward_event), + node.on("interview stage completed", forward_stage), + node.on("interview failed", forward_event), + ] + unsubs.extend(interview_unsubs) + node_details = { + "node_id": node.node_id, + "status": node.status, + "ready": node.ready, + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node added", "node": node_details} + ) + ) + + @callback + def node_removed(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node.node_id, + } + + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node removed", "node": node_details} + ) + ) + + @callback + def device_registered(device: DeviceEntry) -> None: + device_details = { + "name": device.name, + "id": device.id, + "manufacturer": device.manufacturer, + "model": device.model, + } + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "device registered", "device": device_details} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("inclusion started", forward_event), + controller.on("inclusion failed", forward_event), + controller.on("inclusion stopped", forward_event), + controller.on("node removed", node_removed), + controller.on("node added", node_added), + async_dispatcher_connect( + hass, EVENT_DEVICE_ADDED_TO_REGISTRY, device_registered + ), + ] + + result = await controller.async_replace_failed_node(node_id, include_non_secure) + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/remove_failed_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_remove_failed_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Remove a failed node from the Z-Wave network.""" + controller = client.driver.controller + node_id = msg[NODE_ID] + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + unsub() + + @callback + def node_removed(event: dict) -> None: + node = event["node"] + node_details = { + "node_id": node.node_id, + } + + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": "node removed", "node": node_details} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsub = controller.on("node removed", node_removed) + + result = await controller.async_remove_failed_node(node_id) + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/begin_healing_network", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_begin_healing_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Begin healing the Z-Wave network.""" + controller = client.driver.controller + + result = await controller.async_begin_healing_network() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_heal_network_progress", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_subscribe_heal_network_progress( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Subscribe to heal Z-Wave network status updates.""" + controller = client.driver.controller + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_event(key: str, event: dict) -> None: + connection.send_message( + websocket_api.event_message( + msg[ID], {"event": event["event"], "heal_node_status": event[key]} + ) + ) + + connection.subscriptions[msg["id"]] = async_cleanup + unsubs = [ + controller.on("heal network progress", partial(forward_event, "progress")), + controller.on("heal network done", partial(forward_event, "result")), + ] + + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/stop_healing_network", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_stop_healing_network( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Stop healing the Z-Wave network.""" + controller = client.driver.controller + result = await controller.async_stop_healing_network() + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/heal_node", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_heal_node( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Heal a node on the Z-Wave network.""" + controller = client.driver.controller + node_id = msg[NODE_ID] + result = await controller.async_heal_node(node_id) + connection.send_result( + msg[ID], + result, + ) + + +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_info", @@ -398,6 +797,7 @@ async def websocket_remove_node( vol.Required(NODE_ID): int, }, ) +@websocket_api.async_response @async_get_node async def websocket_refresh_node_info( hass: HomeAssistant, @@ -439,8 +839,7 @@ async def websocket_refresh_node_info( connection.send_result(msg[ID], result) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_values", @@ -448,6 +847,7 @@ async def websocket_refresh_node_info( vol.Required(NODE_ID): int, }, ) +@websocket_api.async_response @async_get_node async def websocket_refresh_node_values( hass: HomeAssistant, @@ -460,8 +860,7 @@ async def websocket_refresh_node_values( connection.send_result(msg[ID]) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/refresh_node_cc_values", @@ -470,6 +869,7 @@ async def websocket_refresh_node_values( vol.Required(COMMAND_CLASS_ID): int, }, ) +@websocket_api.async_response @async_get_node async def websocket_refresh_node_cc_values( hass: HomeAssistant, @@ -492,8 +892,7 @@ async def websocket_refresh_node_cc_values( connection.send_result(msg[ID]) -@websocket_api.require_admin # type:ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/set_config_parameter", @@ -504,6 +903,7 @@ async def websocket_refresh_node_cc_values( vol.Required(VALUE): int, } ) +@websocket_api.async_response @async_get_node async def websocket_set_config_parameter( hass: HomeAssistant, @@ -543,8 +943,7 @@ async def websocket_set_config_parameter( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_config_parameters", @@ -552,6 +951,7 @@ async def websocket_set_config_parameter( vol.Required(NODE_ID): int, } ) +@websocket_api.async_response @async_get_node async def websocket_get_config_parameters( hass: HomeAssistant, connection: ActiveConnection, msg: dict, node: Node @@ -593,14 +993,14 @@ def filename_is_present_if_logging_to_file(obj: dict) -> dict: return obj -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/subscribe_logs", vol.Required(ENTRY_ID): str, } ) +@websocket_api.async_response @async_get_entry async def websocket_subscribe_logs( hass: HomeAssistant, @@ -640,8 +1040,7 @@ async def websocket_subscribe_logs( connection.send_result(msg[ID]) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/update_log_config", @@ -668,6 +1067,7 @@ async def websocket_subscribe_logs( ), }, ) +@websocket_api.async_response @async_get_entry async def websocket_update_log_config( hass: HomeAssistant, @@ -683,14 +1083,14 @@ async def websocket_update_log_config( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/get_log_config", vol.Required(ENTRY_ID): str, }, ) +@websocket_api.async_response @async_get_entry async def websocket_get_log_config( hass: HomeAssistant, @@ -707,8 +1107,7 @@ async def websocket_get_log_config( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/update_data_collection_preference", @@ -716,6 +1115,7 @@ async def websocket_get_log_config( vol.Required(OPTED_IN): bool, }, ) +@websocket_api.async_response @async_get_entry async def websocket_update_data_collection_preference( hass: HomeAssistant, @@ -738,14 +1138,14 @@ async def websocket_update_data_collection_preference( ) -@websocket_api.require_admin # type: ignore -@websocket_api.async_response +@websocket_api.require_admin @websocket_api.websocket_command( { vol.Required(TYPE): "zwave_js/data_collection_status", vol.Required(ENTRY_ID): str, }, ) +@websocket_api.async_response @async_get_entry async def websocket_data_collection_status( hass: HomeAssistant, @@ -786,3 +1186,179 @@ class DumpView(HomeAssistantView): hdrs.CONTENT_DISPOSITION: 'attachment; filename="zwave_js_dump.json"', }, ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/abort_firmware_update", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_abort_firmware_update( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Abort a firmware update.""" + await node.async_abort_firmware_update() + connection.send_result(msg[ID]) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/subscribe_firmware_update_status", + vol.Required(ENTRY_ID): str, + vol.Required(NODE_ID): int, + } +) +@websocket_api.async_response +@async_get_node +async def websocket_subscribe_firmware_update_status( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + node: Node, +) -> None: + """Subsribe to the status of a firmware update.""" + + @callback + def async_cleanup() -> None: + """Remove signal listeners.""" + for unsub in unsubs: + unsub() + + @callback + def forward_progress(event: dict) -> None: + progress: FirmwareUpdateProgress = event["firmware_update_progress"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "sent_fragments": progress.sent_fragments, + "total_fragments": progress.total_fragments, + }, + ) + ) + + @callback + def forward_finished(event: dict) -> None: + finished: FirmwareUpdateFinished = event["firmware_update_finished"] + connection.send_message( + websocket_api.event_message( + msg[ID], + { + "event": event["event"], + "status": finished.status, + "wait_time": finished.wait_time, + }, + ) + ) + + unsubs = [ + node.on("firmware update progress", forward_progress), + node.on("firmware update finished", forward_finished), + ] + connection.subscriptions[msg["id"]] = async_cleanup + + connection.send_result(msg[ID]) + + +class FirmwareUploadView(HomeAssistantView): + """View to upload firmware.""" + + url = r"/api/zwave_js/firmware/upload/{config_entry_id}/{node_id:\d+}" + name = "api:zwave_js:firmware:upload" + + async def post( + self, request: web.Request, config_entry_id: str, node_id: str + ) -> web.Response: + """Handle upload.""" + if not request["hass_user"].is_admin: + raise Unauthorized() + hass = request.app["hass"] + if config_entry_id not in hass.data[DOMAIN]: + raise web_exceptions.HTTPBadRequest + + entry = hass.config_entries.async_get_entry(config_entry_id) + client = hass.data[DOMAIN][config_entry_id][DATA_CLIENT] + node = client.driver.controller.nodes.get(int(node_id)) + if not node: + raise web_exceptions.HTTPNotFound + + # Increase max payload + request._client_max_size = 1024 * 1024 * 10 # pylint: disable=protected-access + + data = await request.post() + + if "file" not in data or not isinstance(data["file"], web_request.FileField): + raise web_exceptions.HTTPBadRequest + + uploaded_file: web_request.FileField = data["file"] + + try: + await begin_firmware_update( + entry.data[CONF_URL], + node, + uploaded_file.filename, + await hass.async_add_executor_job(uploaded_file.file.read), + async_get_clientsession(hass), + ) + except BaseZwaveJSServerError as err: + raise web_exceptions.HTTPBadRequest from err + + return self.json(None) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/check_for_config_updates", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_check_for_config_updates( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Check for config updates.""" + config_update = await client.driver.async_check_for_config_updates() + connection.send_result( + msg[ID], + { + "update_available": config_update.update_available, + "new_version": config_update.new_version, + }, + ) + + +@websocket_api.require_admin +@websocket_api.websocket_command( + { + vol.Required(TYPE): "zwave_js/install_config_update", + vol.Required(ENTRY_ID): str, + } +) +@websocket_api.async_response +@async_get_entry +async def websocket_install_config_update( + hass: HomeAssistant, + connection: ActiveConnection, + msg: dict, + entry: ConfigEntry, + client: Client, +) -> None: + """Check for config updates.""" + success = await client.driver.async_install_config_update() + connection.send_result(msg[ID], success) diff --git a/homeassistant/components/zwave_js/binary_sensor.py b/homeassistant/components/zwave_js/binary_sensor.py index b97975b0507..ad186b69fe4 100644 --- a/homeassistant/components/zwave_js/binary_sensor.py +++ b/homeassistant/components/zwave_js/binary_sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Callable, TypedDict +from typing import TypedDict from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass @@ -25,6 +25,7 @@ from homeassistant.components.binary_sensor import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -219,7 +220,9 @@ PROPERTY_SENSOR_MAPPINGS: list[PropertySensorMapping] = [ async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave binary sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/climate.py b/homeassistant/components/zwave_js/climate.py index 0cad9de8065..4ef13276fbe 100644 --- a/homeassistant/components/zwave_js/climate.py +++ b/homeassistant/components/zwave_js/climate.py @@ -1,7 +1,7 @@ """Representation of Z-Wave thermostats.""" from __future__ import annotations -from typing import Any, Callable, cast +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ( @@ -44,6 +44,9 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) +from homeassistant.components.zwave_js.discovery_data_template import ( + DynamicCurrentTempClimateDataTemplate, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_TEMPERATURE, @@ -53,11 +56,13 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.temperature import convert_temperature from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo from .entity import ZWaveBaseEntity +from .helpers import get_value_of_zwave_value # Map Z-Wave HVAC Mode to Home Assistant value # Note: We treat "auto" as "heat_cool" as most Z-Wave devices @@ -98,7 +103,9 @@ ATTR_FAN_STATE = "fan_state" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave climate from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] @@ -107,7 +114,10 @@ async def async_setup_entry( def async_add_climate(info: ZwaveDiscoveryInfo) -> None: """Add Z-Wave Climate.""" entities: list[ZWaveBaseEntity] = [] - entities.append(ZWaveClimate(config_entry, client, info)) + if info.platform_hint == "dynamic_current_temp": + entities.append(DynamicCurrentTempClimate(config_entry, client, info)) + else: + entities.append(ZWaveClimate(config_entry, client, info)) async_add_entities(entities) @@ -126,7 +136,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): def __init__( self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo ) -> None: - """Initialize lock.""" + """Initialize thermostat.""" super().__init__(config_entry, client, info) self._hvac_modes: dict[str, int | None] = {} self._hvac_presets: dict[str, int | None] = {} @@ -282,12 +292,12 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): @property def current_humidity(self) -> int | None: """Return the current humidity level.""" - return self._current_humidity.value if self._current_humidity else None + return get_value_of_zwave_value(self._current_humidity) @property def current_temperature(self) -> float | None: """Return the current temperature.""" - return self._current_temp.value if self._current_temp else None + return get_value_of_zwave_value(self._current_temp) @property def target_temperature(self) -> float | None: @@ -299,7 +309,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): temp = self._setpoint_value(self._current_mode_setpoint_enums[0]) except (IndexError, ValueError): return None - return temp.value if temp else None + return get_value_of_zwave_value(temp) @property def target_temperature_high(self) -> float | None: @@ -311,7 +321,7 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): temp = self._setpoint_value(self._current_mode_setpoint_enums[1]) except (IndexError, ValueError): return None - return temp.value if temp else None + return get_value_of_zwave_value(temp) @property def target_temperature_low(self) -> float | None: @@ -479,3 +489,25 @@ class ZWaveClimate(ZWaveBaseEntity, ClimateEntity): if preset_mode_value is None: raise ValueError(f"Received an invalid preset mode: {preset_mode}") await self.info.node.async_set_value(self._current_mode, preset_mode_value) + + +class DynamicCurrentTempClimate(ZWaveClimate): + """Representation of a thermostat that can dynamically use a different Zwave Value for current temp.""" + + def __init__( + self, config_entry: ConfigEntry, client: ZwaveClient, info: ZwaveDiscoveryInfo + ) -> None: + """Initialize thermostat.""" + super().__init__(config_entry, client, info) + self.data_template = cast( + DynamicCurrentTempClimateDataTemplate, self.info.platform_data_template + ) + + @property + def current_temperature(self) -> float | None: + """Return the current temperature.""" + assert self.info.platform_data + val = get_value_of_zwave_value( + self.data_template.current_temperature_value(self.info.platform_data) + ) + return val if val is not None else super().current_temperature diff --git a/homeassistant/components/zwave_js/config_flow.py b/homeassistant/components/zwave_js/config_flow.py index 58cb37edcfc..ef39a043b0e 100644 --- a/homeassistant/components/zwave_js/config_flow.py +++ b/homeassistant/components/zwave_js/config_flow.py @@ -3,7 +3,7 @@ from __future__ import annotations import asyncio import logging -from typing import Any, cast +from typing import Any import aiohttp from async_timeout import timeout @@ -17,7 +17,7 @@ from homeassistant.core import HomeAssistant, callback from homeassistant.data_entry_flow import AbortFlow, FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession -from .addon import AddonError, AddonManager, get_addon_manager +from .addon import AddonError, AddonInfo, AddonManager, AddonState, get_addon_manager from .const import ( CONF_ADDON_DEVICE, CONF_ADDON_NETWORK_KEY, @@ -74,7 +74,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Z-Wave JS.""" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH def __init__(self) -> None: """Set up flow instance.""" @@ -188,13 +187,15 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.use_addon = True - if await self._async_is_addon_running(): - addon_config = await self._async_get_addon_config() + addon_info = await self._async_get_addon_info() + + if addon_info.state == AddonState.RUNNING: + addon_config = addon_info.options self.usb_path = addon_config[CONF_ADDON_DEVICE] self.network_key = addon_config.get(CONF_ADDON_NETWORK_KEY, "") return await self.async_step_finish_addon_setup() - if await self._async_is_addon_installed(): + if addon_info.state == AddonState.NOT_RUNNING: return await self.async_step_configure_addon() return await self.async_step_install_addon() @@ -229,7 +230,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self, user_input: dict[str, Any] | None = None ) -> FlowResult: """Ask for config for Z-Wave JS add-on.""" - addon_config = await self._async_get_addon_config() + addon_info = await self._async_get_addon_info() + addon_config = addon_info.options errors: dict[str, str] = {} @@ -346,32 +348,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self._async_create_entry_from_vars() - async def _async_get_addon_info(self) -> dict: + async def _async_get_addon_info(self) -> AddonInfo: """Return and cache Z-Wave JS add-on info.""" addon_manager: AddonManager = get_addon_manager(self.hass) try: - addon_info: dict = await addon_manager.async_get_addon_info() + addon_info: AddonInfo = await addon_manager.async_get_addon_info() except AddonError as err: _LOGGER.error(err) raise AbortFlow("addon_info_failed") from err return addon_info - async def _async_is_addon_running(self) -> bool: - """Return True if Z-Wave JS add-on is running.""" - addon_info = await self._async_get_addon_info() - return bool(addon_info["state"] == "started") - - async def _async_is_addon_installed(self) -> bool: - """Return True if Z-Wave JS add-on is installed.""" - addon_info = await self._async_get_addon_info() - return addon_info["version"] is not None - - async def _async_get_addon_config(self) -> dict: - """Get Z-Wave JS add-on config.""" - addon_info = await self._async_get_addon_info() - return cast(dict, addon_info["options"]) - async def _async_set_addon_config(self, config: dict) -> None: """Set Z-Wave JS add-on config.""" addon_manager: AddonManager = get_addon_manager(self.hass) diff --git a/homeassistant/components/zwave_js/cover.py b/homeassistant/components/zwave_js/cover.py index 25c69335ed1..302ccd9cd32 100644 --- a/homeassistant/components/zwave_js/cover.py +++ b/homeassistant/components/zwave_js/cover.py @@ -2,14 +2,17 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.components.cover import ( ATTR_POSITION, + DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, DOMAIN as COVER_DOMAIN, SUPPORT_CLOSE, SUPPORT_OPEN, @@ -18,6 +21,7 @@ from homeassistant.components.cover import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -36,7 +40,9 @@ BARRIER_STATE_OPEN = 255 async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Cover from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] @@ -73,6 +79,15 @@ def percent_to_zwave_position(value: int) -> int: class ZWaveCover(ZWaveBaseEntity, CoverEntity): """Representation of a Z-Wave Cover device.""" + @property + def device_class(self) -> str | None: + """Return the class of this device, from component DEVICE_CLASSES.""" + if self.info.platform_hint == "window_shutter": + return DEVICE_CLASS_SHUTTER + if self.info.platform_hint == "window_blind": + return DEVICE_CLASS_BLIND + return DEVICE_CLASS_WINDOW + @property def is_closed(self) -> bool | None: """Return true if cover is closed.""" diff --git a/homeassistant/components/zwave_js/discovery.py b/homeassistant/components/zwave_js/discovery.py index 21e75a8ea51..29976850480 100644 --- a/homeassistant/components/zwave_js/discovery.py +++ b/homeassistant/components/zwave_js/discovery.py @@ -2,16 +2,50 @@ from __future__ import annotations from collections.abc import Generator -from dataclasses import dataclass +from dataclasses import asdict, dataclass, field from typing import Any -from zwave_js_server.const import CommandClass +from awesomeversion import AwesomeVersion +from zwave_js_server.const import THERMOSTAT_CURRENT_TEMP_PROPERTY, CommandClass from zwave_js_server.model.device_class import DeviceClassItem from zwave_js_server.model.node import Node as ZwaveNode from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.core import callback +from .discovery_data_template import ( + BaseDiscoverySchemaDataTemplate, + DynamicCurrentTempClimateDataTemplate, + ZwaveValueID, +) + + +class DataclassMustHaveAtLeastOne: + """A dataclass that must have at least one input parameter that is not None.""" + + def __post_init__(self) -> None: + """Post dataclass initialization.""" + if all(val is None for val in asdict(self).values()): + raise ValueError("At least one input parameter must not be None") + + +@dataclass +class FirmwareVersionRange(DataclassMustHaveAtLeastOne): + """Firmware version range dictionary.""" + + min: str | None = None + max: str | None = None + min_ver: AwesomeVersion | None = field(default=None, init=False) + max_ver: AwesomeVersion | None = field(default=None, init=False) + + def __post_init__(self) -> None: + """Post dataclass initialization.""" + super().__post_init__() + if self.min: + self.min_ver = AwesomeVersion(self.min) + if self.max: + self.max_ver = AwesomeVersion(self.max) + @dataclass class ZwaveDiscoveryInfo: @@ -27,10 +61,16 @@ class ZwaveDiscoveryInfo: platform: str # hint for the platform about this discovered entity platform_hint: str | None = "" + # data template to use in platform logic + platform_data_template: BaseDiscoverySchemaDataTemplate | None = None + # helper data to use in platform setup + platform_data: dict[str, Any] | None = None + # additional values that need to be watched by entity + additional_value_ids_to_watch: set[str] | None = None @dataclass -class ZWaveValueDiscoverySchema: +class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne): """Z-Wave Value discovery schema. The Z-Wave Value must match these conditions. @@ -69,12 +109,16 @@ class ZWaveDiscoverySchema: primary_value: ZWaveValueDiscoverySchema # [optional] hint for platform hint: str | None = None + # [optional] template to generate platform specific data to use in setup + data_template: BaseDiscoverySchemaDataTemplate | None = None # [optional] the node's manufacturer_id must match ANY of these values manufacturer_id: set[int] | None = None # [optional] the node's product_id must match ANY of these values product_id: set[int] | None = None # [optional] the node's product_type must match ANY of these values product_type: set[int] | None = None + # [optional] the node's firmware_version must be within this range + firmware_version_range: FirmwareVersionRange | None = None # [optional] the node's firmware_version must match ANY of these values firmware_version: set[str] | None = None # [optional] the node's basic device class must match ANY of these values @@ -176,6 +220,7 @@ DISCOVERY_SCHEMAS = [ # Fibaro Shutter Fibaro FGS222 ZWaveDiscoverySchema( platform="cover", + hint="window_shutter", manufacturer_id={0x010F}, product_id={0x1000}, product_type={0x0302}, @@ -184,14 +229,16 @@ DISCOVERY_SCHEMAS = [ # Qubino flush shutter ZWaveDiscoverySchema( platform="cover", + hint="window_shutter", manufacturer_id={0x0159}, - product_id={0x0052}, + product_id={0x0052, 0x0053}, product_type={0x0003}, primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA, ), # Graber/Bali/Spring Fashion Covers ZWaveDiscoverySchema( platform="cover", + hint="window_blind", manufacturer_id={0x026E}, product_id={0x5A31}, product_type={0x4353}, @@ -200,6 +247,7 @@ DISCOVERY_SCHEMAS = [ # iBlinds v2 window blind motor ZWaveDiscoverySchema( platform="cover", + hint="window_blind", manufacturer_id={0x0287}, product_id={0x000D}, product_type={0x0003}, @@ -214,6 +262,88 @@ DISCOVERY_SCHEMAS = [ primary_value=SWITCH_BINARY_CURRENT_VALUE_SCHEMA, assumed_state=True, ), + # Heatit Z-TRM3 + ZWaveDiscoverySchema( + platform="climate", + hint="dynamic_current_temp", + manufacturer_id={0x019B}, + product_id={0x0203}, + product_type={0x0003}, + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), + data_template=DynamicCurrentTempClimateDataTemplate( + { + # Internal Sensor + "A": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=2, + ), + "AF": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=2, + ), + # External Sensor + "A2": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=3, + ), + "A2F": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=3, + ), + # Floor sensor + "F": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=4, + ), + }, + ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + ), + ), + # Heatit Z-TRM2fx + ZWaveDiscoverySchema( + platform="climate", + hint="dynamic_current_temp", + manufacturer_id={0x019B}, + product_id={0x0202}, + product_type={0x0003}, + firmware_version_range=FirmwareVersionRange(min="3.0"), + primary_value=ZWaveValueDiscoverySchema( + command_class={CommandClass.THERMOSTAT_MODE}, + property={"mode"}, + type={"number"}, + ), + data_template=DynamicCurrentTempClimateDataTemplate( + { + # External Sensor + "A2": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=2, + ), + "A2F": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=2, + ), + # Floor sensor + "F": ZwaveValueID( + THERMOSTAT_CURRENT_TEMP_PROPERTY, + CommandClass.SENSOR_MULTILEVEL, + endpoint=3, + ), + }, + ZwaveValueID(2, CommandClass.CONFIGURATION, endpoint=0), + ), + ), # ====== START OF CONFIG PARAMETER SPECIFIC MAPPING SCHEMAS ======= # Door lock mode config parameter. Functionality equivalent to Notification CC # list sensors. @@ -481,6 +611,21 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None ): continue + # check firmware_version_range + if schema.firmware_version_range is not None and ( + ( + schema.firmware_version_range.min is not None + and schema.firmware_version_range.min_ver + > AwesomeVersion(value.node.firmware_version) + ) + or ( + schema.firmware_version_range.max is not None + and schema.firmware_version_range.max_ver + < AwesomeVersion(value.node.firmware_version) + ) + ): + continue + # check firmware_version if ( schema.firmware_version is not None @@ -524,6 +669,15 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None ): continue + # resolve helper data from template + resolved_data = None + additional_value_ids_to_watch = None + if schema.data_template: + resolved_data = schema.data_template.resolve_data(value) + additional_value_ids_to_watch = schema.data_template.value_ids_to_watch( + resolved_data + ) + # all checks passed, this value belongs to an entity yield ZwaveDiscoveryInfo( node=value.node, @@ -531,6 +685,9 @@ def async_discover_values(node: ZwaveNode) -> Generator[ZwaveDiscoveryInfo, None assumed_state=schema.assumed_state, platform=schema.platform, platform_hint=schema.hint, + platform_data_template=schema.data_template, + platform_data=resolved_data, + additional_value_ids_to_watch=additional_value_ids_to_watch, ) if not schema.allow_multi: diff --git a/homeassistant/components/zwave_js/discovery_data_template.py b/homeassistant/components/zwave_js/discovery_data_template.py new file mode 100644 index 00000000000..4a2a8d2da94 --- /dev/null +++ b/homeassistant/components/zwave_js/discovery_data_template.py @@ -0,0 +1,109 @@ +"""Data template classes for discovery used to generate device specific data for setup.""" +from __future__ import annotations + +from collections.abc import Iterable +from dataclasses import dataclass +from typing import Any + +from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue, get_value_id + + +@dataclass +class ZwaveValueID: + """Class to represent a value ID.""" + + property_: str | int + command_class: int + endpoint: int | None = None + property_key: str | int | None = None + + +@dataclass +class BaseDiscoverySchemaDataTemplate: + """Base class for discovery schema data templates.""" + + def resolve_data(self, value: ZwaveValue) -> dict[str, Any]: + """ + Resolve helper class data for a discovered value. + + Can optionally be implemented by subclasses if input data needs to be + transformed once discovered Value is available. + """ + # pylint: disable=no-self-use + return {} + + def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + """ + Return list of all ZwaveValues resolved by helper that should be watched. + + Should be implemented by subclasses only if there are values to watch. + """ + # pylint: disable=no-self-use + return [] + + def value_ids_to_watch(self, resolved_data: dict[str, Any]) -> set[str]: + """ + Return list of all Value IDs resolved by helper that should be watched. + + Not to be overwritten by subclasses. + """ + return {val.value_id for val in self.values_to_watch(resolved_data) if val} + + @staticmethod + def _get_value_from_id( + node: ZwaveNode, value_id_obj: ZwaveValueID + ) -> ZwaveValue | None: + """Get a ZwaveValue from a node using a ZwaveValueDict.""" + value_id = get_value_id( + node, + value_id_obj.command_class, + value_id_obj.property_, + endpoint=value_id_obj.endpoint, + property_key=value_id_obj.property_key, + ) + return node.values.get(value_id) + + +@dataclass +class DynamicCurrentTempClimateDataTemplate(BaseDiscoverySchemaDataTemplate): + """Data template class for Z-Wave JS Climate entities with dynamic current temps.""" + + lookup_table: dict[str | int, ZwaveValueID] + dependent_value: ZwaveValueID + + def resolve_data(self, value: ZwaveValue) -> dict[str, Any]: + """Resolve helper class data for a discovered value.""" + data: dict[str, Any] = { + "lookup_table": {}, + "dependent_value": self._get_value_from_id( + value.node, self.dependent_value + ), + } + for key in self.lookup_table: + data["lookup_table"][key] = self._get_value_from_id( + value.node, self.lookup_table[key] + ) + + return data + + def values_to_watch(self, resolved_data: dict[str, Any]) -> Iterable[ZwaveValue]: + """Return list of all ZwaveValues resolved by helper that should be watched.""" + return [ + *resolved_data["lookup_table"].values(), + resolved_data["dependent_value"], + ] + + @staticmethod + def current_temperature_value(resolved_data: dict[str, Any]) -> ZwaveValue | None: + """Get current temperature ZwaveValue from resolved data.""" + lookup_table: dict[str | int, ZwaveValue | None] = resolved_data["lookup_table"] + dependent_value: ZwaveValue | None = resolved_data["dependent_value"] + + if dependent_value: + lookup_key = dependent_value.metadata.states[ + str(dependent_value.value) + ].split("-")[0] + return lookup_table.get(lookup_key) + + return None diff --git a/homeassistant/components/zwave_js/entity.py b/homeassistant/components/zwave_js/entity.py index 20efa9ed7db..2d7dc961e68 100644 --- a/homeassistant/components/zwave_js/entity.py +++ b/homeassistant/components/zwave_js/entity.py @@ -9,7 +9,7 @@ from zwave_js_server.model.value import Value as ZwaveValue, get_value_id from homeassistant.config_entries import ConfigEntry from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -from homeassistant.helpers.entity import Entity +from homeassistant.helpers.entity import DeviceInfo, Entity from .const import DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -37,6 +37,11 @@ class ZWaveBaseEntity(Entity): # entities requiring additional values, can add extra ids to this list self.watched_value_ids = {self.info.primary_value.value_id} + if self.info.additional_value_ids_to_watch: + self.watched_value_ids = self.watched_value_ids.union( + self.info.additional_value_ids_to_watch + ) + @callback def on_value_update(self) -> None: """Call when one of the watched values change. @@ -87,7 +92,7 @@ class ZWaveBaseEntity(Entity): ) @property - def device_info(self) -> dict: + def device_info(self) -> DeviceInfo: """Return device information for the device registry.""" # device is precreated in main handler return { diff --git a/homeassistant/components/zwave_js/fan.py b/homeassistant/components/zwave_js/fan.py index 100e400f9f7..89b99e90110 100644 --- a/homeassistant/components/zwave_js/fan.py +++ b/homeassistant/components/zwave_js/fan.py @@ -2,7 +2,7 @@ from __future__ import annotations import math -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient @@ -14,6 +14,7 @@ from homeassistant.components.fan import ( from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.util.percentage import ( int_states_in_range, percentage_to_ranged_value, @@ -30,7 +31,9 @@ SPEED_RANGE = (1, 99) # off is not included async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Fan from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/helpers.py b/homeassistant/components/zwave_js/helpers.py index 98c308ea58c..beee7fefa30 100644 --- a/homeassistant/components/zwave_js/helpers.py +++ b/homeassistant/components/zwave_js/helpers.py @@ -1,10 +1,11 @@ """Helper functions for Z-Wave JS integration.""" from __future__ import annotations -from typing import cast +from typing import Any, cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.model.node import Node as ZwaveNode +from zwave_js_server.model.value import Value as ZwaveValue from homeassistant.config_entries import ConfigEntry from homeassistant.const import __version__ as HA_VERSION @@ -15,6 +16,12 @@ from homeassistant.helpers.entity_registry import async_get as async_get_ent_reg from .const import CONF_DATA_COLLECTION_OPTED_IN, DATA_CLIENT, DOMAIN +@callback +def get_value_of_zwave_value(value: ZwaveValue | None) -> Any | None: + """Return the value of a ZwaveValue.""" + return value.value if value else None + + async def async_enable_statistics(client: ZwaveClient) -> None: """Enable statistics on the driver.""" await client.driver.async_enable_statistics("Home Assistant", HA_VERSION) @@ -43,7 +50,7 @@ def get_device_id(client: ZwaveClient, node: ZwaveNode) -> tuple[str, str]: @callback -def get_home_and_node_id_from_device_id(device_id: tuple[str, str]) -> list[str]: +def get_home_and_node_id_from_device_id(device_id: tuple[str, ...]) -> list[str]: """ Get home ID and node ID for Z-Wave device registry entry. diff --git a/homeassistant/components/zwave_js/light.py b/homeassistant/components/zwave_js/light.py index 0b146d7d00b..a1ab78e6ee3 100644 --- a/homeassistant/components/zwave_js/light.py +++ b/homeassistant/components/zwave_js/light.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import ColorComponent, CommandClass @@ -11,19 +11,20 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, DOMAIN as LIGHT_DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback import homeassistant.util.color as color_util from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN @@ -45,7 +46,9 @@ MULTI_COLOR_MAP = { async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Light from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] @@ -85,14 +88,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Initialize the light.""" super().__init__(config_entry, client, info) self._supports_color = False - self._supports_white_value = False + self._supports_rgbw = False self._supports_color_temp = False self._hs_color: tuple[float, float] | None = None - self._white_value: int | None = None + self._rgbw_color: tuple[int, int, int, int] | None = None + self._color_mode: str | None = None self._color_temp: int | None = None self._min_mireds = 153 # 6500K as a safe default self._max_mireds = 370 # 2700K as a safe default - self._supported_features = SUPPORT_BRIGHTNESS self._warm_white = self.get_zwave_value( "targetColor", CommandClass.SWITCH_COLOR, @@ -103,6 +106,8 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): CommandClass.SWITCH_COLOR, value_property_key=ColorComponent.COLD_WHITE, ) + self._supported_color_modes = set() + self._supported_features = 0 # get additional (optional) values and set features self._target_value = self.get_zwave_value("targetValue") @@ -110,12 +115,14 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): if self._dimming_duration is not None: self._supported_features |= SUPPORT_TRANSITION self._calculate_color_values() - if self._supports_color: - self._supported_features |= SUPPORT_COLOR + if self._supports_rgbw: + self._supported_color_modes.add(COLOR_MODE_RGBW) + elif self._supports_color: + self._supported_color_modes.add(COLOR_MODE_HS) if self._supports_color_temp: - self._supported_features |= SUPPORT_COLOR_TEMP - if self._supports_white_value: - self._supported_features |= SUPPORT_WHITE_VALUE + self._supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + if not self._supported_color_modes: + self._supported_color_modes.add(COLOR_MODE_BRIGHTNESS) @callback def on_value_update(self) -> None: @@ -132,6 +139,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return round((self.info.primary_value.value / 99) * 255) return 0 + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def is_on(self) -> bool: """Return true if device is on (brightness above 0).""" @@ -143,9 +155,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): return self._hs_color @property - def white_value(self) -> int | None: - """Return the white value of this light between 0..255.""" - return self._white_value + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the hs color.""" + return self._rgbw_color @property def color_temp(self) -> int | None: @@ -162,6 +174,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): """Return the warmest color_temp that this light supports.""" return self._max_mireds + @property + def supported_color_modes(self) -> set | None: + """Flag supported features.""" + return self._supported_color_modes + @property def supported_features(self) -> int: """Flag supported features.""" @@ -211,20 +228,20 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): } ) - # White value - white_value = kwargs.get(ATTR_WHITE_VALUE) - if white_value is not None and self._supports_white_value: - # white led brightness is controlled by white level - # rgb leds (if any) can be on at the same time - white_channel = {} - + # RGBW + rgbw = kwargs.get(ATTR_RGBW_COLOR) + if rgbw is not None and self._supports_rgbw: + rgbw_channels = { + ColorComponent.RED: rgbw[0], + ColorComponent.GREEN: rgbw[1], + ColorComponent.BLUE: rgbw[2], + } if self._warm_white: - white_channel[ColorComponent.WARM_WHITE] = white_value + rgbw_channels[ColorComponent.WARM_WHITE] = rgbw[3] if self._cold_white: - white_channel[ColorComponent.COLD_WHITE] = white_value - - await self._async_set_colors(white_channel) + rgbw_channels[ColorComponent.COLD_WHITE] = rgbw[3] + await self._async_set_colors(rgbw_channels) # set brightness await self._async_set_brightness( @@ -361,6 +378,9 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): else: multi_color = {} + # Default: Brightness (no color) + self._color_mode = COLOR_MODE_BRIGHTNESS + # RGB support if red_val and green_val and blue_val: # prefer values from the multicolor property @@ -368,8 +388,11 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): green = multi_color.get("green", green_val.value) blue = multi_color.get("blue", blue_val.value) self._supports_color = True - # convert to HS - self._hs_color = color_util.color_RGB_to_hs(red, green, blue) + if None not in (red, green, blue): + # convert to HS + self._hs_color = color_util.color_RGB_to_hs(red, green, blue) + # Light supports color, set color mode to hs + self._color_mode = COLOR_MODE_HS # color temperature support if ww_val and cw_val: @@ -382,13 +405,21 @@ class ZwaveLight(ZWaveBaseEntity, LightEntity): self._max_mireds - ((cold_white / 255) * (self._max_mireds - self._min_mireds)) ) + # White channels turned on, set color mode to color_temp + self._color_mode = COLOR_MODE_COLOR_TEMP else: self._color_temp = None - # only one white channel (warm white) = white_level support - elif ww_val: - self._supports_white_value = True - self._white_value = multi_color.get("warmWhite", ww_val.value) - # only one white channel (cool white) = white_level support + # only one white channel (warm white) = rgbw support + elif red_val and green_val and blue_val and ww_val: + self._supports_rgbw = True + white = multi_color.get("warmWhite", ww_val.value) + self._rgbw_color = (red, green, blue, white) + # Light supports rgbw, set color mode to rgbw + self._color_mode = COLOR_MODE_RGBW + # only one white channel (cool white) = rgbw support elif cw_val: - self._supports_white_value = True - self._white_value = multi_color.get("coldWhite", cw_val.value) + self._supports_rgbw = True + white = multi_color.get("coldWhite", cw_val.value) + self._rgbw_color = (red, green, blue, white) + # Light supports rgbw, set color mode to rgbw + self._color_mode = COLOR_MODE_RGBW diff --git a/homeassistant/components/zwave_js/lock.py b/homeassistant/components/zwave_js/lock.py index 0647885345b..42230a9c267 100644 --- a/homeassistant/components/zwave_js/lock.py +++ b/homeassistant/components/zwave_js/lock.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any import voluptuous as vol from zwave_js_server.client import Client as ZwaveClient @@ -23,6 +23,7 @@ from homeassistant.const import STATE_LOCKED, STATE_UNLOCKED from homeassistant.core import HomeAssistant, callback from homeassistant.helpers import config_validation as cv, entity_platform from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -46,7 +47,9 @@ SERVICE_CLEAR_LOCK_USERCODE = "clear_lock_usercode" async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave lock from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] @@ -65,10 +68,9 @@ async def async_setup_entry( ) ) - platform = entity_platform.current_platform.get() - assert platform + platform = entity_platform.async_get_current_platform() - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_SET_LOCK_USERCODE, { vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), @@ -77,7 +79,7 @@ async def async_setup_entry( "async_set_lock_usercode", ) - platform.async_register_entity_service( # type: ignore + platform.async_register_entity_service( SERVICE_CLEAR_LOCK_USERCODE, { vol.Required(ATTR_CODE_SLOT): vol.Coerce(int), diff --git a/homeassistant/components/zwave_js/manifest.json b/homeassistant/components/zwave_js/manifest.json index e730b6ae9db..5ce65fcbb35 100644 --- a/homeassistant/components/zwave_js/manifest.json +++ b/homeassistant/components/zwave_js/manifest.json @@ -3,7 +3,7 @@ "name": "Z-Wave JS", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zwave_js", - "requirements": ["zwave-js-server-python==0.24.0"], + "requirements": ["zwave-js-server-python==0.26.0"], "codeowners": ["@home-assistant/z-wave"], "dependencies": ["http", "websocket_api"], "iot_class": "local_push" diff --git a/homeassistant/components/zwave_js/number.py b/homeassistant/components/zwave_js/number.py index f418ee3d35b..f427f7fac20 100644 --- a/homeassistant/components/zwave_js/number.py +++ b/homeassistant/components/zwave_js/number.py @@ -1,14 +1,13 @@ """Support for Z-Wave controls using the number platform.""" from __future__ import annotations -from typing import Callable - from zwave_js_server.client import Client as ZwaveClient from homeassistant.components.number import DOMAIN as NUMBER_DOMAIN, NumberEntity from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -16,7 +15,9 @@ from .entity import ZWaveBaseEntity async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave Number entity from Config Entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/sensor.py b/homeassistant/components/zwave_js/sensor.py index d1e18763b5b..40e28999a1a 100644 --- a/homeassistant/components/zwave_js/sensor.py +++ b/homeassistant/components/zwave_js/sensor.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Callable, cast +from typing import cast from zwave_js_server.client import Client as ZwaveClient from zwave_js_server.const import CommandClass, ConfigurationValueType @@ -14,6 +14,7 @@ from homeassistant.components.sensor import ( DEVICE_CLASS_ILLUMINANCE, DEVICE_CLASS_POWER, DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, SensorEntity, ) from homeassistant.config_entries import ConfigEntry @@ -25,6 +26,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -34,7 +36,9 @@ LOGGER = logging.getLogger(__name__) async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] @@ -84,6 +88,7 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): super().__init__(config_entry, client, info) self._name = self.generate_name(include_value_name=True) self._device_class = self._get_device_class() + self._state_class = self._get_state_class() def _get_device_class(self) -> str | None: """ @@ -110,11 +115,31 @@ class ZwaveSensorBase(ZWaveBaseEntity, SensorEntity): return DEVICE_CLASS_ILLUMINANCE return None + def _get_state_class(self) -> str | None: + """ + Get the state class of the sensor. + + This should be run once during initialization so we don't have to calculate + this value on every state update. + """ + if self.info.primary_value.command_class == CommandClass.BATTERY: + return STATE_CLASS_MEASUREMENT + if isinstance(self.info.primary_value.property_, str): + property_lower = self.info.primary_value.property_.lower() + if "humidity" in property_lower or "temperature" in property_lower: + return STATE_CLASS_MEASUREMENT + return None + @property def device_class(self) -> str | None: """Return the device class of the sensor.""" return self._device_class + @property + def state_class(self) -> str | None: + """Return the state class of the sensor.""" + return self._state_class + @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" diff --git a/homeassistant/components/zwave_js/services.py b/homeassistant/components/zwave_js/services.py index 16bf9c7eb94..c2ebe965fdd 100644 --- a/homeassistant/components/zwave_js/services.py +++ b/homeassistant/components/zwave_js/services.py @@ -55,7 +55,7 @@ BITMASK_SCHEMA = vol.All( class ZWaveServices: """Class that holds our services (Zwave Commands) that should be published to hass.""" - def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry): + def __init__(self, hass: HomeAssistant, ent_reg: EntityRegistry) -> None: """Initialize with hass object.""" self._hass = hass self._ent_reg = ent_reg @@ -67,22 +67,26 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_SET_CONFIG_PARAMETER, self.async_set_config_parameter, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( - vol.Coerce(int), cv.string - ), - vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( - vol.Coerce(int), BITMASK_SCHEMA - ), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), cv.string - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), - parameter_name_does_not_need_bitmask, + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Any( + vol.Coerce(int), cv.string + ), + vol.Optional(const.ATTR_CONFIG_PARAMETER_BITMASK): vol.Any( + vol.Coerce(int), BITMASK_SCHEMA + ), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), cv.string + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + parameter_name_does_not_need_bitmask, + ), ), ) @@ -90,21 +94,25 @@ class ZWaveServices: const.DOMAIN, const.SERVICE_BULK_SET_PARTIAL_CONFIG_PARAMETERS, self.async_bulk_set_partial_config_parameters, - schema=vol.All( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), - vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( - vol.Coerce(int), - { - vol.Any( - vol.Coerce(int), BITMASK_SCHEMA, cv.string - ): vol.Any(vol.Coerce(int), cv.string) - }, - ), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + schema=vol.Schema( + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_CONFIG_PARAMETER): vol.Coerce(int), + vol.Required(const.ATTR_CONFIG_VALUE): vol.Any( + vol.Coerce(int), + { + vol.Any( + vol.Coerce(int), BITMASK_SCHEMA, cv.string + ): vol.Any(vol.Coerce(int), cv.string) + }, + ), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) @@ -125,21 +133,27 @@ class ZWaveServices: const.SERVICE_SET_VALUE, self.async_set_value, schema=vol.Schema( - { - vol.Optional(ATTR_DEVICE_ID): vol.All(cv.ensure_list, [cv.string]), - vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, - vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), - vol.Required(const.ATTR_PROPERTY): vol.Any(vol.Coerce(int), str), - vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( - vol.Coerce(int), str - ), - vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), - vol.Required(const.ATTR_VALUE): vol.Any( - bool, vol.Coerce(int), vol.Coerce(float), cv.string - ), - vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), - }, - cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + vol.All( + { + vol.Optional(ATTR_DEVICE_ID): vol.All( + cv.ensure_list, [cv.string] + ), + vol.Optional(ATTR_ENTITY_ID): cv.entity_ids, + vol.Required(const.ATTR_COMMAND_CLASS): vol.Coerce(int), + vol.Required(const.ATTR_PROPERTY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_PROPERTY_KEY): vol.Any( + vol.Coerce(int), str + ), + vol.Optional(const.ATTR_ENDPOINT): vol.Coerce(int), + vol.Required(const.ATTR_VALUE): vol.Any( + bool, vol.Coerce(int), vol.Coerce(float), cv.string + ), + vol.Optional(const.ATTR_WAIT_FOR_RESULT): vol.Coerce(bool), + }, + cv.has_at_least_one_key(ATTR_DEVICE_ID, ATTR_ENTITY_ID), + ), ), ) diff --git a/homeassistant/components/zwave_js/services.yaml b/homeassistant/components/zwave_js/services.yaml index f9d90f94779..84877189298 100644 --- a/homeassistant/components/zwave_js/services.yaml +++ b/homeassistant/components/zwave_js/services.yaml @@ -108,8 +108,6 @@ refresh_value: refresh_all_values: name: Refresh all values? description: Whether to refresh all values (true) or just the primary value (false) - required: false - example: true default: false selector: boolean: @@ -159,7 +157,6 @@ set_value: wait_for_result: name: Wait for result? description: Whether or not to wait for a response from the node. If not included in the payload, the integration will decide whether to wait or not. If set to `true`, note that the service call can take a while if setting a value on an asleep battery device. - example: false required: false selector: boolean: diff --git a/homeassistant/components/zwave_js/switch.py b/homeassistant/components/zwave_js/switch.py index e64ea57703d..0be5d1d7f61 100644 --- a/homeassistant/components/zwave_js/switch.py +++ b/homeassistant/components/zwave_js/switch.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Any, Callable +from typing import Any from zwave_js_server.client import Client as ZwaveClient @@ -10,6 +10,7 @@ from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN, SwitchEntit from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant, callback from homeassistant.helpers.dispatcher import async_dispatcher_connect +from homeassistant.helpers.entity_platform import AddEntitiesCallback from .const import DATA_CLIENT, DATA_UNSUBSCRIBE, DOMAIN from .discovery import ZwaveDiscoveryInfo @@ -23,7 +24,9 @@ BARRIER_EVENT_SIGNALING_ON = 255 async def async_setup_entry( - hass: HomeAssistant, config_entry: ConfigEntry, async_add_entities: Callable + hass: HomeAssistant, + config_entry: ConfigEntry, + async_add_entities: AddEntitiesCallback, ) -> None: """Set up Z-Wave sensor from config entry.""" client: ZwaveClient = hass.data[DOMAIN][config_entry.entry_id][DATA_CLIENT] diff --git a/homeassistant/components/zwave_js/translations/bg.json b/homeassistant/components/zwave_js/translations/bg.json index abf89f00513..cc046c009d8 100644 --- a/homeassistant/components/zwave_js/translations/bg.json +++ b/homeassistant/components/zwave_js/translations/bg.json @@ -8,11 +8,6 @@ "data": { "url": "URL" } - }, - "user": { - "data": { - "url": "URL" - } } } } diff --git a/homeassistant/components/zwave_js/translations/ca.json b/homeassistant/components/zwave_js/translations/ca.json index 731c0bbcea8..af39281c06b 100644 --- a/homeassistant/components/zwave_js/translations/ca.json +++ b/homeassistant/components/zwave_js/translations/ca.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "No s'ha pogut obtenir la informaci\u00f3 de descobriment del complement Z-Wave JS.", "addon_info_failed": "No s'ha pogut obtenir la informaci\u00f3 del complement Z-Wave JS.", "addon_install_failed": "No s'ha pogut instal\u00b7lar el complement Z-Wave JS.", - "addon_missing_discovery_info": "Falta la informaci\u00f3 de descobriment del complement Z-Wave JS.", "addon_set_config_failed": "No s'ha pogut establir la configuraci\u00f3 de Z-Wave JS.", "addon_start_failed": "No s'ha pogut iniciar el complement Z-Wave JS.", "already_configured": "El dispositiu ja est\u00e0 configurat", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "El complement Z-Wave JS s'est\u00e0 iniciant." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/cs.json b/homeassistant/components/zwave_js/translations/cs.json index 57e7a6b74db..9f8af44c451 100644 --- a/homeassistant/components/zwave_js/translations/cs.json +++ b/homeassistant/components/zwave_js/translations/cs.json @@ -19,11 +19,6 @@ "data": { "url": "URL" } - }, - "user": { - "data": { - "url": "URL" - } } } } diff --git a/homeassistant/components/zwave_js/translations/de.json b/homeassistant/components/zwave_js/translations/de.json index ae9293cf926..c4672112fe5 100644 --- a/homeassistant/components/zwave_js/translations/de.json +++ b/homeassistant/components/zwave_js/translations/de.json @@ -1,6 +1,7 @@ { "config": { "abort": { + "addon_get_discovery_info_failed": "Z-Wave-JS-Add-on-Discovery-Informationen konnten nicht abgerufen werden.", "addon_info_failed": "Fehler beim Abrufen von Z-Wave JS Add-on Informationen.", "addon_install_failed": "Installation des Z-Wave JS Add-Ons fehlgeschlagen.", "addon_set_config_failed": "Setzen der Z-Wave JS Konfiguration fehlgeschlagen", @@ -47,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS Add-on wird gestartet." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/en.json b/homeassistant/components/zwave_js/translations/en.json index 5be980d52cb..101942dc717 100644 --- a/homeassistant/components/zwave_js/translations/en.json +++ b/homeassistant/components/zwave_js/translations/en.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Failed to get Z-Wave JS add-on discovery info.", "addon_info_failed": "Failed to get Z-Wave JS add-on info.", "addon_install_failed": "Failed to install the Z-Wave JS add-on.", - "addon_missing_discovery_info": "Missing Z-Wave JS add-on discovery info.", "addon_set_config_failed": "Failed to set Z-Wave JS configuration.", "addon_start_failed": "Failed to start the Z-Wave JS add-on.", "already_configured": "Device is already configured", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "The Z-Wave JS add-on is starting." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/es.json b/homeassistant/components/zwave_js/translations/es.json index 26fd155a0ad..1e5b07ec171 100644 --- a/homeassistant/components/zwave_js/translations/es.json +++ b/homeassistant/components/zwave_js/translations/es.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Fallo en la obtenci\u00f3n de la informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_info_failed": "No se pudo obtener la informaci\u00f3n del complemento Z-Wave JS.", "addon_install_failed": "No se ha podido instalar el complemento Z-Wave JS.", - "addon_missing_discovery_info": "Falta informaci\u00f3n de descubrimiento del complemento Z-Wave JS.", "addon_set_config_failed": "Fallo en la configuraci\u00f3n de Z-Wave JS.", "addon_start_failed": "No se ha podido iniciar el complemento Z-Wave JS.", "already_configured": "El dispositivo ya est\u00e1 configurado", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Se est\u00e1 iniciando el complemento Z-Wave JS." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/et.json b/homeassistant/components/zwave_js/translations/et.json index 4c68e63530f..2ae0a0f47c6 100644 --- a/homeassistant/components/zwave_js/translations/et.json +++ b/homeassistant/components/zwave_js/translations/et.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave JS lisandmooduli tuvastusteabe hankimine nurjus.", "addon_info_failed": "Z-Wave JS lisandmooduli teabe hankimine nurjus.", "addon_install_failed": "Z-Wave JS lisandmooduli paigaldamine nurjus.", - "addon_missing_discovery_info": "Z-Wave JS lisandmooduli tuvastusteave puudub.", "addon_set_config_failed": "Z-Wave JS konfiguratsiooni m\u00e4\u00e4ramine nurjus.", "addon_start_failed": "Z-Wave JS-i lisandmooduli k\u00e4ivitamine nurjus.", "already_configured": "Seade on juba h\u00e4\u00e4lestatud", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS lisandmoodul k\u00e4ivitub." - }, - "user": { - "data": { - "url": "" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/fr.json b/homeassistant/components/zwave_js/translations/fr.json index ce9f2f8b501..33571f12d60 100644 --- a/homeassistant/components/zwave_js/translations/fr.json +++ b/homeassistant/components/zwave_js/translations/fr.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Impossible d'obtenir les informations de d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", "addon_info_failed": "Impossible d'obtenir les informations sur le module compl\u00e9mentaire Z-Wave JS.", "addon_install_failed": "\u00c9chec de l'installation du module compl\u00e9mentaire Z-Wave JS.", - "addon_missing_discovery_info": "Informations manquantes sur la d\u00e9couverte du module compl\u00e9mentaire Z-Wave JS.", "addon_set_config_failed": "\u00c9chec de la d\u00e9finition de la configuration Z-Wave JS.", "addon_start_failed": "\u00c9chec du d\u00e9marrage du module compl\u00e9mentaire Z-Wave JS.", "already_configured": "Le p\u00e9riph\u00e9rique est d\u00e9j\u00e0 configur\u00e9", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Le module compl\u00e9mentaire Z-Wave JS est d\u00e9marr\u00e9." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/hu.json b/homeassistant/components/zwave_js/translations/hu.json index 6732251f3a0..87629666b09 100644 --- a/homeassistant/components/zwave_js/translations/hu.json +++ b/homeassistant/components/zwave_js/translations/hu.json @@ -37,11 +37,6 @@ }, "start_addon": { "title": "Indul a Z-Wave JS b\u0151v\u00edtm\u00e9ny." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/id.json b/homeassistant/components/zwave_js/translations/id.json index e8ea9381544..046cdd59485 100644 --- a/homeassistant/components/zwave_js/translations/id.json +++ b/homeassistant/components/zwave_js/translations/id.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Gagal mendapatkan info penemuan add-on Z-Wave JS.", "addon_info_failed": "Gagal mendapatkan info add-on Z-Wave JS.", "addon_install_failed": "Gagal menginstal add-on Z-Wave JS.", - "addon_missing_discovery_info": "Info penemuan add-on Z-Wave JS tidak ada.", "addon_set_config_failed": "Gagal menyetel konfigurasi Z-Wave JS.", "addon_start_failed": "Gagal memulai add-on Z-Wave JS.", "already_configured": "Perangkat sudah dikonfigurasi", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Add-on Z-Wave JS sedang dimulai." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/it.json b/homeassistant/components/zwave_js/translations/it.json index abe0ab066fb..f3005fc1651 100644 --- a/homeassistant/components/zwave_js/translations/it.json +++ b/homeassistant/components/zwave_js/translations/it.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Impossibile ottenere le informazioni sul rilevamento del componente aggiuntivo Z-Wave JS.", "addon_info_failed": "Impossibile ottenere le informazioni sul componente aggiuntivo Z-Wave JS.", "addon_install_failed": "Impossibile installare il componente aggiuntivo Z-Wave JS.", - "addon_missing_discovery_info": "Informazioni sul rilevamento del componente aggiuntivo Z-Wave JS mancanti.", "addon_set_config_failed": "Impossibile impostare la configurazione di Z-Wave JS.", "addon_start_failed": "Impossibile avviare il componente aggiuntivo Z-Wave JS.", "already_configured": "Il dispositivo \u00e8 gi\u00e0 configurato", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Il componente aggiuntivo Z-Wave JS si sta avviando." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/ko.json b/homeassistant/components/zwave_js/translations/ko.json index 22149af7496..08e3dc8c7d7 100644 --- a/homeassistant/components/zwave_js/translations/ko.json +++ b/homeassistant/components/zwave_js/translations/ko.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "addon_info_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uc815\ubcf4\ub97c \uac00\uc838\uc624\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "addon_install_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc124\uce58\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", - "addon_missing_discovery_info": "Z-Wave JS \uc560\ub4dc\uc628\uc758 \uac80\uc0c9 \uc815\ubcf4\uac00 \uc5c6\uc2b5\ub2c8\ub2e4.", "addon_set_config_failed": "Z-Wave JS \uad6c\uc131\uc744 \uc124\uc815\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "addon_start_failed": "Z-Wave JS \uc560\ub4dc\uc628\uc744 \uc2dc\uc791\ud558\uc9c0 \ubabb\ud588\uc2b5\ub2c8\ub2e4.", "already_configured": "\uae30\uae30\uac00 \uc774\ubbf8 \uad6c\uc131\ub418\uc5c8\uc2b5\ub2c8\ub2e4", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS \uc560\ub4dc\uc628\uc774 \uc2dc\uc791\ud558\ub294 \uc911\uc785\ub2c8\ub2e4." - }, - "user": { - "data": { - "url": "URL \uc8fc\uc18c" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/lb.json b/homeassistant/components/zwave_js/translations/lb.json index 302addbd7cf..d84c5323fb9 100644 --- a/homeassistant/components/zwave_js/translations/lb.json +++ b/homeassistant/components/zwave_js/translations/lb.json @@ -7,13 +7,6 @@ "cannot_connect": "Feeler beim verbannen", "invalid_ws_url": "Ong\u00eblteg Websocket URL", "unknown": "Onerwaarte Feeler" - }, - "step": { - "user": { - "data": { - "url": "URL" - } - } } }, "title": "Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/nl.json b/homeassistant/components/zwave_js/translations/nl.json index f50c9c8ceba..090733da15b 100644 --- a/homeassistant/components/zwave_js/translations/nl.json +++ b/homeassistant/components/zwave_js/translations/nl.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Ophalen van ontdekkingsinformatie voor Z-Wave JS-add-on is mislukt.", "addon_info_failed": "Ophalen van Z-Wave JS add-on-info is mislukt.", "addon_install_failed": "Kan de Z-Wave JS add-on niet installeren.", - "addon_missing_discovery_info": "De Z-Wave JS addon mist ontdekkings informatie", "addon_set_config_failed": "Instellen van de Z-Wave JS configuratie is mislukt.", "addon_start_failed": "Kan de Z-Wave JS add-on niet starten.", "already_configured": "Apparaat is al geconfigureerd", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "De add-on Z-Wave JS wordt gestart." - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/no.json b/homeassistant/components/zwave_js/translations/no.json index f893d2d7684..aa0fa2451aa 100644 --- a/homeassistant/components/zwave_js/translations/no.json +++ b/homeassistant/components/zwave_js/translations/no.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Kunne ikke hente oppdagelsesinformasjon om Z-Wave JS-tillegg", "addon_info_failed": "Kunne ikke hente informasjon om Z-Wave JS-tillegg", "addon_install_failed": "Kunne ikke installere Z-Wave JS-tillegg", - "addon_missing_discovery_info": "Manglende oppdagelsesinformasjon for Z-Wave JS-tillegg", "addon_set_config_failed": "Kunne ikke angi Z-Wave JS-konfigurasjon", "addon_start_failed": "Kunne ikke starte Z-Wave JS-tillegget.", "already_configured": "Enheten er allerede konfigurert", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS-tillegget starter" - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/pl.json b/homeassistant/components/zwave_js/translations/pl.json index 2bfd994132b..b729e8db3da 100644 --- a/homeassistant/components/zwave_js/translations/pl.json +++ b/homeassistant/components/zwave_js/translations/pl.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji wykrywania dodatku Z-Wave JS", "addon_info_failed": "Nie uda\u0142o si\u0119 uzyska\u0107 informacji o dodatku Z-Wave JS", "addon_install_failed": "Nie uda\u0142o si\u0119 zainstalowa\u0107 dodatku Z-Wave JS", - "addon_missing_discovery_info": "Brak informacji wykrywania dodatku Z-Wave JS", "addon_set_config_failed": "Nie uda\u0142o si\u0119 skonfigurowa\u0107 Z-Wave JS", "addon_start_failed": "Nie uda\u0142o si\u0119 uruchomi\u0107 dodatku Z-Wave JS.", "already_configured": "Urz\u0105dzenie jest ju\u017c skonfigurowane", @@ -48,12 +47,7 @@ "title": "Wybierz metod\u0119 po\u0142\u0105czenia" }, "start_addon": { - "title": "Dodatek Z-Wave JS uruchamia si\u0119." - }, - "user": { - "data": { - "url": "URL" - } + "title": "Dodatek Z-Wave JS uruchamia si\u0119..." } } }, diff --git a/homeassistant/components/zwave_js/translations/ru.json b/homeassistant/components/zwave_js/translations/ru.json index 1a65ce3ea71..b0b3745fac4 100644 --- a/homeassistant/components/zwave_js/translations/ru.json +++ b/homeassistant/components/zwave_js/translations/ru.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e\u0431 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0438\u0438 \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u044f Z-Wave JS.", "addon_info_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u043f\u043e\u043b\u0443\u0447\u0438\u0442\u044c \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044e \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", "addon_install_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", - "addon_missing_discovery_info": "\u041d\u0435 \u043e\u0431\u043d\u0430\u0440\u0443\u0436\u0435\u043d\u0430 \u0438\u043d\u0444\u043e\u0440\u043c\u0430\u0446\u0438\u044f \u043e \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0438 Z-Wave JS.", "addon_set_config_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0443\u0441\u0442\u0430\u043d\u043e\u0432\u0438\u0442\u044c \u043a\u043e\u043d\u0444\u0438\u0433\u0443\u0440\u0430\u0446\u0438\u044e Z-Wave JS.", "addon_start_failed": "\u041d\u0435 \u0443\u0434\u0430\u043b\u043e\u0441\u044c \u0437\u0430\u043f\u0443\u0441\u0442\u0438\u0442\u044c \u0434\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS.", "already_configured": "\u042d\u0442\u043e \u0443\u0441\u0442\u0440\u043e\u0439\u0441\u0442\u0432\u043e \u0443\u0436\u0435 \u0434\u043e\u0431\u0430\u0432\u043b\u0435\u043d\u043e \u0432 Home Assistant.", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "\u0414\u043e\u043f\u043e\u043b\u043d\u0435\u043d\u0438\u0435 Z-Wave JS \u0437\u0430\u043f\u0443\u0441\u043a\u0430\u0435\u0442\u0441\u044f" - }, - "user": { - "data": { - "url": "URL-\u0430\u0434\u0440\u0435\u0441" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/tr.json b/homeassistant/components/zwave_js/translations/tr.json index 04ddcc5252c..10c9c54a98b 100644 --- a/homeassistant/components/zwave_js/translations/tr.json +++ b/homeassistant/components/zwave_js/translations/tr.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "Z-Wave JS eklenti ke\u015fif bilgileri al\u0131namad\u0131.", "addon_info_failed": "Z-Wave JS eklenti bilgileri al\u0131namad\u0131.", "addon_install_failed": "Z-Wave JS eklentisi y\u00fcklenemedi.", - "addon_missing_discovery_info": "Eksik Z-Wave JS eklenti bulma bilgileri.", "addon_set_config_failed": "Z-Wave JS yap\u0131land\u0131rmas\u0131 ayarlanamad\u0131.", "already_configured": "Cihaz zaten yap\u0131land\u0131r\u0131lm\u0131\u015f", "already_in_progress": "Yap\u0131land\u0131rma ak\u0131\u015f\u0131 zaten devam ediyor", @@ -44,11 +43,6 @@ }, "description": "Z-Wave JS Supervisor eklentisini kullanmak istiyor musunuz?", "title": "Ba\u011flant\u0131 y\u00f6ntemini se\u00e7in" - }, - "user": { - "data": { - "url": "URL" - } } } }, diff --git a/homeassistant/components/zwave_js/translations/uk.json b/homeassistant/components/zwave_js/translations/uk.json index f5ff5224347..fe77655ac29 100644 --- a/homeassistant/components/zwave_js/translations/uk.json +++ b/homeassistant/components/zwave_js/translations/uk.json @@ -7,13 +7,6 @@ "cannot_connect": "\u041d\u0435 \u0432\u0434\u0430\u043b\u043e\u0441\u044f \u043f\u0456\u0434'\u0454\u0434\u043d\u0430\u0442\u0438\u0441\u044f", "invalid_ws_url": "\u041d\u0435\u0434\u0456\u0439\u0441\u043d\u0430 URL-\u0430\u0434\u0440\u0435\u0441\u0430 \u0432\u0435\u0431-\u0441\u043e\u043a\u0435\u0442\u0430", "unknown": "\u041d\u0435\u043e\u0447\u0456\u043a\u0443\u0432\u0430\u043d\u0430 \u043f\u043e\u043c\u0438\u043b\u043a\u0430" - }, - "step": { - "user": { - "data": { - "url": "URL-\u0430\u0434\u0440\u0435\u0441\u0430" - } - } } }, "title": "Z-Wave JS" diff --git a/homeassistant/components/zwave_js/translations/zh-Hant.json b/homeassistant/components/zwave_js/translations/zh-Hant.json index d35c9e8a260..827c0b54e90 100644 --- a/homeassistant/components/zwave_js/translations/zh-Hant.json +++ b/homeassistant/components/zwave_js/translations/zh-Hant.json @@ -4,7 +4,6 @@ "addon_get_discovery_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u5931\u6557\u3002", "addon_info_failed": "\u53d6\u5f97 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8cc7\u8a0a\u5931\u6557\u3002", "addon_install_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u5b89\u88dd\u5931\u6557\u3002", - "addon_missing_discovery_info": "\u7f3a\u5c11 Z-Wave JS \u9644\u52a0\u5143\u4ef6\u63a2\u7d22\u8cc7\u8a0a\u3002", "addon_set_config_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u8a2d\u5b9a\u5931\u6557\u3002", "addon_start_failed": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u5931\u6557\u3002", "already_configured": "\u88dd\u7f6e\u5df2\u7d93\u8a2d\u5b9a\u5b8c\u6210", @@ -49,11 +48,6 @@ }, "start_addon": { "title": "Z-Wave JS \u9644\u52a0\u5143\u4ef6\u555f\u59cb\u4e2d\u3002" - }, - "user": { - "data": { - "url": "\u7db2\u5740" - } } } }, diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 09325b118bf..49892937217 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -4,14 +4,13 @@ from __future__ import annotations import asyncio from collections.abc import Iterable, Mapping from contextvars import ContextVar +from enum import Enum import functools import logging from types import MappingProxyType, MethodType from typing import Any, Callable, Optional, cast import weakref -import attr - from homeassistant import data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED, EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback @@ -63,20 +62,37 @@ PATH_CONFIG = ".config_entries.json" SAVE_DELAY = 1 -# The config entry has been set up successfully -ENTRY_STATE_LOADED = "loaded" -# There was an error while trying to set up this config entry -ENTRY_STATE_SETUP_ERROR = "setup_error" -# There was an error while trying to migrate the config entry to a new version -ENTRY_STATE_MIGRATION_ERROR = "migration_error" -# The config entry was not ready to be set up yet, but might be later -ENTRY_STATE_SETUP_RETRY = "setup_retry" -# The config entry has not been loaded -ENTRY_STATE_NOT_LOADED = "not_loaded" -# An error occurred when trying to unload the entry -ENTRY_STATE_FAILED_UNLOAD = "failed_unload" -UNRECOVERABLE_STATES = (ENTRY_STATE_MIGRATION_ERROR, ENTRY_STATE_FAILED_UNLOAD) +class ConfigEntryState(Enum): + """Config entry state.""" + + LOADED = "loaded", True + """The config entry has been set up successfully""" + SETUP_ERROR = "setup_error", True + """There was an error while trying to set up this config entry""" + MIGRATION_ERROR = "migration_error", False + """There was an error while trying to migrate the config entry to a new version""" + SETUP_RETRY = "setup_retry", True + """The config entry was not ready to be set up yet, but might be later""" + NOT_LOADED = "not_loaded", True + """The config entry has not been loaded""" + FAILED_UNLOAD = "failed_unload", False + """An error occurred when trying to unload the entry""" + + _recoverable: bool + + def __new__(cls: type[object], value: str, recoverable: bool) -> ConfigEntryState: + """Create new ConfigEntryState.""" + obj = object.__new__(cls) + obj._value_ = value + obj._recoverable = recoverable + return cast("ConfigEntryState", obj) + + @property + def recoverable(self) -> bool: + """Get if the state is recoverable.""" + return self._recoverable + DEFAULT_DISCOVERY_UNIQUE_ID = "default_discovery_unique_id" DISCOVERY_NOTIFICATION_ID = "config_entry_discovery" @@ -92,6 +108,13 @@ RECONFIGURE_NOTIFICATION_ID = "config_entry_reconfigure" EVENT_FLOW_DISCOVERED = "config_entry_discovered" +DISABLED_USER = "user" + +RELOAD_AFTER_UPDATE_DELAY = 30 + +# Deprecated: Connection classes +# These aren't used anymore since 2021.6.0 +# Mainly here not to break custom integrations. CONN_CLASS_CLOUD_PUSH = "cloud_push" CONN_CLASS_CLOUD_POLL = "cloud_poll" CONN_CLASS_LOCAL_PUSH = "local_push" @@ -99,10 +122,6 @@ CONN_CLASS_LOCAL_POLL = "local_poll" CONN_CLASS_ASSUMED = "assumed" CONN_CLASS_UNKNOWN = "unknown" -DISABLED_USER = "user" - -RELOAD_AFTER_UPDATE_DELAY = 30 - class ConfigError(HomeAssistantError): """Error while configuring an account.""" @@ -131,9 +150,9 @@ class ConfigEntry: "options", "unique_id", "supports_unload", - "system_options", + "pref_disable_new_entities", + "pref_disable_polling", "source", - "connection_class", "state", "disabled_by", "_setup_lock", @@ -150,12 +169,12 @@ class ConfigEntry: title: str, data: Mapping[str, Any], source: str, - connection_class: str, - system_options: dict, - options: dict | None = None, + pref_disable_new_entities: bool | None = None, + pref_disable_polling: bool | None = None, + options: Mapping[str, Any] | None = None, unique_id: str | None = None, entry_id: str | None = None, - state: str = ENTRY_STATE_NOT_LOADED, + state: ConfigEntryState = ConfigEntryState.NOT_LOADED, disabled_by: str | None = None, ) -> None: """Initialize a config entry.""" @@ -178,14 +197,19 @@ class ConfigEntry: self.options = MappingProxyType(options or {}) # Entry system options - self.system_options = SystemOptions(**system_options) + if pref_disable_new_entities is None: + pref_disable_new_entities = False + + self.pref_disable_new_entities = pref_disable_new_entities + + if pref_disable_polling is None: + pref_disable_polling = False + + self.pref_disable_polling = pref_disable_polling # Source of the configuration (user, discovery, cloud) self.source = source - # Connection class - self.connection_class = connection_class - # State of the entry (LOADED, NOT_LOADED) self.state = state @@ -239,7 +263,7 @@ class ConfigEntry: err, ) if self.domain == integration.domain: - self.state = ENTRY_STATE_SETUP_ERROR + self.state = ConfigEntryState.SETUP_ERROR self.reason = "Import error" return @@ -253,13 +277,13 @@ class ConfigEntry: self.domain, err, ) - self.state = ENTRY_STATE_SETUP_ERROR + self.state = ConfigEntryState.SETUP_ERROR self.reason = "Import error" return # Perform migration if not await self.async_migrate(hass): - self.state = ENTRY_STATE_MIGRATION_ERROR + self.state = ConfigEntryState.MIGRATION_ERROR self.reason = None return @@ -290,7 +314,7 @@ class ConfigEntry: self.async_start_reauth(hass) result = False except ConfigEntryNotReady as ex: - self.state = ENTRY_STATE_SETUP_RETRY + self.state = ConfigEntryState.SETUP_RETRY self.reason = str(ex) or None wait_time = 2 ** min(tries, 4) * 5 tries += 1 @@ -339,10 +363,10 @@ class ConfigEntry: return if result: - self.state = ENTRY_STATE_LOADED + self.state = ConfigEntryState.LOADED self.reason = None else: - self.state = ENTRY_STATE_SETUP_ERROR + self.state = ConfigEntryState.SETUP_ERROR self.reason = error_reason async def async_shutdown(self) -> None: @@ -364,10 +388,13 @@ class ConfigEntry: Returns if unload is possible and was successful. """ if self.source == SOURCE_IGNORE: - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None return True + if self.state == ConfigEntryState.NOT_LOADED: + return True + if integration is None: try: integration = await loader.async_get_integration(hass, self.domain) @@ -376,20 +403,20 @@ class ConfigEntry: # that was uninstalled, or an integration # that has been renamed without removing the config # entry. - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None return True component = integration.get_component() if integration.domain == self.domain: - if self.state in UNRECOVERABLE_STATES: + if not self.state.recoverable: return False - if self.state != ENTRY_STATE_LOADED: + if self.state is not ConfigEntryState.LOADED: self.async_cancel_retry_setup() - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None return True @@ -397,7 +424,7 @@ class ConfigEntry: if not supports_unload: if integration.domain == self.domain: - self.state = ENTRY_STATE_FAILED_UNLOAD + self.state = ConfigEntryState.FAILED_UNLOAD self.reason = "Unload not supported" return False @@ -408,7 +435,7 @@ class ConfigEntry: # Only adjust state if we unloaded the component if result and integration.domain == self.domain: - self.state = ENTRY_STATE_NOT_LOADED + self.state = ConfigEntryState.NOT_LOADED self.reason = None self._async_process_on_unload() @@ -419,7 +446,7 @@ class ConfigEntry: "Error unloading entry %s for %s", self.title, integration.domain ) if integration.domain == self.domain: - self.state = ENTRY_STATE_FAILED_UNLOAD + self.state = ConfigEntryState.FAILED_UNLOAD self.reason = "Unknown error" return False @@ -519,9 +546,9 @@ class ConfigEntry: "title": self.title, "data": dict(self.data), "options": dict(self.options), - "system_options": self.system_options.as_dict(), + "pref_disable_new_entities": self.pref_disable_new_entities, + "pref_disable_polling": self.pref_disable_polling, "source": self.source, - "connection_class": self.connection_class, "unique_id": self.unique_id, "disabled_by": self.disabled_by, } @@ -572,7 +599,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): def __init__( self, hass: HomeAssistant, config_entries: ConfigEntries, hass_config: dict - ): + ) -> None: """Initialize the config entry flow manager.""" super().__init__(hass) self.config_entries = config_entries @@ -600,16 +627,21 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): # Check if config entry exists with unique ID. Unload it. existing_entry = None - if flow.unique_id is not None: - # Abort all flows in progress with same unique ID. - for progress_flow in self.async_progress(): - if ( - progress_flow["handler"] == flow.handler - and progress_flow["flow_id"] != flow.flow_id - and progress_flow["context"].get("unique_id") == flow.unique_id - ): - self.async_abort(progress_flow["flow_id"]) + # Abort all flows in progress with same unique ID + # or the default discovery ID + for progress_flow in self.async_progress(): + progress_unique_id = progress_flow["context"].get("unique_id") + if ( + progress_flow["handler"] == flow.handler + and progress_flow["flow_id"] != flow.flow_id + and ( + (flow.unique_id and progress_unique_id == flow.unique_id) + or progress_unique_id == DEFAULT_DISCOVERY_UNIQUE_ID + ) + ): + self.async_abort(progress_flow["flow_id"]) + if flow.unique_id is not None: # Reset unique ID when the default discovery ID has been used if flow.unique_id == DEFAULT_DISCOVERY_UNIQUE_ID: await flow.async_set_unique_id(None) @@ -623,10 +655,7 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): # Unload the entry before setting up the new one. # We will remove it only after the other one is set up, # so that device customizations are not getting lost. - if ( - existing_entry is not None - and existing_entry.state not in UNRECOVERABLE_STATES - ): + if existing_entry is not None and existing_entry.state.recoverable: await self.config_entries.async_unload(existing_entry.entry_id) entry = ConfigEntry( @@ -634,10 +663,8 @@ class ConfigEntriesFlowManager(data_entry_flow.FlowManager): domain=result["handler"], title=result["title"], data=result["data"], - options={}, - system_options={}, + options=result["options"], source=flow.context["source"], - connection_class=flow.CONNECTION_CLASS, unique_id=flow.unique_id, ) @@ -774,8 +801,8 @@ class ConfigEntries: if entry is None: raise UnknownEntry - if entry.state in UNRECOVERABLE_STATES: - unload_success = entry.state != ENTRY_STATE_FAILED_UNLOAD + if not entry.state.recoverable: + unload_success = entry.state is not ConfigEntryState.FAILED_UNLOAD else: unload_success = await self.async_unload(entry_id) @@ -829,27 +856,36 @@ class ConfigEntries: self._entries = {} return - self._entries = { - entry["entry_id"]: ConfigEntry( + entries = {} + + for entry in config["entries"]: + pref_disable_new_entities = entry.get("pref_disable_new_entities") + + # Between 0.98 and 2021.6 we stored 'disable_new_entities' in a system options dictionary + if pref_disable_new_entities is None and "system_options" in entry: + pref_disable_new_entities = entry.get("system_options", {}).get( + "disable_new_entities" + ) + + entries[entry["entry_id"]] = ConfigEntry( version=entry["version"], domain=entry["domain"], entry_id=entry["entry_id"], data=entry["data"], source=entry["source"], title=entry["title"], - # New in 0.79 - connection_class=entry.get("connection_class", CONN_CLASS_UNKNOWN), # New in 0.89 options=entry.get("options"), - # New in 0.98 - system_options=entry.get("system_options", {}), # New in 0.104 unique_id=entry.get("unique_id"), # New in 2021.3 disabled_by=entry.get("disabled_by"), + # New in 2021.6 + pref_disable_new_entities=pref_disable_new_entities, + pref_disable_polling=entry.get("pref_disable_polling"), ) - for entry in config["entries"] - } + + self._entries = entries async def async_setup(self, entry_id: str) -> bool: """Set up a config entry. @@ -861,7 +897,7 @@ class ConfigEntries: if entry is None: raise UnknownEntry - if entry.state != ENTRY_STATE_NOT_LOADED: + if entry.state is not ConfigEntryState.NOT_LOADED: raise OperationNotAllowed # Setup Component if not set up yet @@ -876,7 +912,7 @@ class ConfigEntries: if not result: return result - return entry.state == ENTRY_STATE_LOADED + return entry.state is ConfigEntryState.LOADED # type: ignore[comparison-overlap] # mypy bug? async def async_unload(self, entry_id: str) -> bool: """Unload a config entry.""" @@ -885,7 +921,7 @@ class ConfigEntries: if entry is None: raise UnknownEntry - if entry.state in UNRECOVERABLE_STATES: + if not entry.state.recoverable: raise OperationNotAllowed return await entry.async_unload(self.hass) @@ -948,11 +984,12 @@ class ConfigEntries: self, entry: ConfigEntry, *, - unique_id: str | dict | None | UndefinedType = UNDEFINED, - title: str | dict | UndefinedType = UNDEFINED, + unique_id: str | None | UndefinedType = UNDEFINED, + title: str | UndefinedType = UNDEFINED, data: dict | UndefinedType = UNDEFINED, options: Mapping[str, Any] | UndefinedType = UNDEFINED, - system_options: dict | UndefinedType = UNDEFINED, + pref_disable_new_entities: bool | UndefinedType = UNDEFINED, + pref_disable_polling: bool | UndefinedType = UNDEFINED, ) -> bool: """Update a config entry. @@ -964,13 +1001,17 @@ class ConfigEntries: """ changed = False - if unique_id is not UNDEFINED and entry.unique_id != unique_id: - changed = True - entry.unique_id = cast(Optional[str], unique_id) + for attr, value in ( + ("unique_id", unique_id), + ("title", title), + ("pref_disable_new_entities", pref_disable_new_entities), + ("pref_disable_polling", pref_disable_polling), + ): + if value == UNDEFINED or getattr(entry, attr) == value: + continue - if title is not UNDEFINED and entry.title != title: + setattr(entry, attr, value) changed = True - entry.title = cast(str, title) if data is not UNDEFINED and entry.data != data: # type: ignore changed = True @@ -980,13 +1021,6 @@ class ConfigEntries: changed = True entry.options = MappingProxyType(options) - if ( - system_options is not UNDEFINED - and entry.system_options.as_dict() != system_options - ): - changed = True - entry.system_options.update(**system_options) - if not changed: return False @@ -1077,8 +1111,6 @@ class ConfigFlow(data_entry_flow.FlowHandler): if domain is not None: HANDLERS.register(domain)(cls) - CONNECTION_CLASS = CONN_CLASS_UNKNOWN - @property def unique_id(self) -> str | None: """Return unique ID if available.""" @@ -1093,6 +1125,17 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Get the options flow for this handler.""" raise data_entry_flow.UnknownHandler + @callback + def _async_abort_entries_match( + self, match_dict: dict[str, Any] | None = None + ) -> None: + """Abort if current entries match all data.""" + if match_dict is None: + match_dict = {} # Match any entry + for entry in self._async_current_entries(include_ignore=False): + if all(item in entry.data.items() for item in match_dict.items()): + raise data_entry_flow.AbortFlow("already_configured") + @callback def _abort_if_unique_id_configured( self, @@ -1112,7 +1155,8 @@ class ConfigFlow(data_entry_flow.FlowHandler): if ( changed and reload_on_update - and entry.state in (ENTRY_STATE_LOADED, ENTRY_STATE_SETUP_RETRY) + and entry.state + in (ConfigEntryState.LOADED, ConfigEntryState.SETUP_RETRY) ): self.hass.async_create_task( self.hass.config_entries.async_reload(entry.entry_id) @@ -1305,6 +1349,28 @@ class ConfigFlow(data_entry_flow.FlowHandler): """Handle a flow initialized by DHCP discovery.""" return await self.async_step_discovery(discovery_info) + @callback + def async_create_entry( # pylint: disable=arguments-differ + self, + *, + title: str, + data: Mapping[str, Any], + description: str | None = None, + description_placeholders: dict | None = None, + options: Mapping[str, Any] | None = None, + ) -> data_entry_flow.FlowResult: + """Finish config flow and create a config entry.""" + result = super().async_create_entry( + title=title, + data=data, + description=description, + description_placeholders=description_placeholders, + ) + + result["options"] = options or {} + + return result + class OptionsFlowManager(data_entry_flow.FlowManager): """Flow to set options for a configuration entry.""" @@ -1357,21 +1423,6 @@ class OptionsFlow(data_entry_flow.FlowHandler): handler: str -@attr.s(slots=True) -class SystemOptions: - """Config entry system options.""" - - disable_new_entities: bool = attr.ib(default=False) - - def update(self, *, disable_new_entities: bool) -> None: - """Update properties.""" - self.disable_new_entities = disable_new_entities - - def as_dict(self) -> dict[str, Any]: - """Return dictionary version of this config entries system options.""" - return {"disable_new_entities": self.disable_new_entities} - - class EntityRegistryDisabledHandler: """Handler to handle when entities related to config entries updating disabled_by.""" diff --git a/homeassistant/const.py b/homeassistant/const.py index bd802526e0a..229646d74d1 100644 --- a/homeassistant/const.py +++ b/homeassistant/const.py @@ -1,655 +1,670 @@ """Constants used by Home Assistant components.""" -MAJOR_VERSION = 2021 -MINOR_VERSION = 5 -PATCH_VERSION = "5" -__short_version__ = f"{MAJOR_VERSION}.{MINOR_VERSION}" -__version__ = f"{__short_version__}.{PATCH_VERSION}" -REQUIRED_PYTHON_VER = (3, 8, 0) +from __future__ import annotations + +from typing import Final + +MAJOR_VERSION: Final = 2021 +MINOR_VERSION: Final = 6 +PATCH_VERSION: Final = "0" +__short_version__: Final = f"{MAJOR_VERSION}.{MINOR_VERSION}" +__version__: Final = f"{__short_version__}.{PATCH_VERSION}" +REQUIRED_PYTHON_VER: Final[tuple[int, int, int]] = (3, 8, 0) # Truthy date string triggers showing related deprecation warning messages. -REQUIRED_NEXT_PYTHON_VER = (3, 9, 0) -REQUIRED_NEXT_PYTHON_DATE = "" +REQUIRED_NEXT_PYTHON_VER: Final[tuple[int, int, int]] = (3, 9, 0) +REQUIRED_NEXT_PYTHON_DATE: Final = "" # Format for platform files -PLATFORM_FORMAT = "{platform}.{domain}" +PLATFORM_FORMAT: Final = "{platform}.{domain}" # Can be used to specify a catch all when registering state or event listeners. -MATCH_ALL = "*" +MATCH_ALL: Final = "*" # Entity target all constant -ENTITY_MATCH_NONE = "none" -ENTITY_MATCH_ALL = "all" +ENTITY_MATCH_NONE: Final = "none" +ENTITY_MATCH_ALL: Final = "all" # If no name is specified -DEVICE_DEFAULT_NAME = "Unnamed Device" +DEVICE_DEFAULT_NAME: Final = "Unnamed Device" -# Max characters for an event_type (changing this requires a recorder -# database migration) -MAX_LENGTH_EVENT_TYPE = 64 +# Max characters for data stored in the recorder (changes to these limits would require +# a database migration) +MAX_LENGTH_EVENT_EVENT_TYPE: Final = 64 +MAX_LENGTH_EVENT_ORIGIN: Final = 32 +MAX_LENGTH_EVENT_CONTEXT_ID: Final = 36 +MAX_LENGTH_STATE_DOMAIN: Final = 64 +MAX_LENGTH_STATE_ENTITY_ID: Final = 255 +MAX_LENGTH_STATE_STATE: Final = 255 # Sun events -SUN_EVENT_SUNSET = "sunset" -SUN_EVENT_SUNRISE = "sunrise" +SUN_EVENT_SUNSET: Final = "sunset" +SUN_EVENT_SUNRISE: Final = "sunrise" # #### CONFIG #### -CONF_ABOVE = "above" -CONF_ACCESS_TOKEN = "access_token" -CONF_ADDRESS = "address" -CONF_AFTER = "after" -CONF_ALIAS = "alias" -CONF_ALLOWLIST_EXTERNAL_URLS = "allowlist_external_urls" -CONF_API_KEY = "api_key" -CONF_API_TOKEN = "api_token" -CONF_API_VERSION = "api_version" -CONF_ARMING_TIME = "arming_time" -CONF_AT = "at" -CONF_ATTRIBUTE = "attribute" -CONF_AUTH_MFA_MODULES = "auth_mfa_modules" -CONF_AUTH_PROVIDERS = "auth_providers" -CONF_AUTHENTICATION = "authentication" -CONF_BASE = "base" -CONF_BEFORE = "before" -CONF_BELOW = "below" -CONF_BINARY_SENSORS = "binary_sensors" -CONF_BRIGHTNESS = "brightness" -CONF_BROADCAST_ADDRESS = "broadcast_address" -CONF_BROADCAST_PORT = "broadcast_port" -CONF_CHOOSE = "choose" -CONF_CLIENT_ID = "client_id" -CONF_CLIENT_SECRET = "client_secret" -CONF_CODE = "code" -CONF_COLOR_TEMP = "color_temp" -CONF_COMMAND = "command" -CONF_COMMAND_CLOSE = "command_close" -CONF_COMMAND_OFF = "command_off" -CONF_COMMAND_ON = "command_on" -CONF_COMMAND_OPEN = "command_open" -CONF_COMMAND_STATE = "command_state" -CONF_COMMAND_STOP = "command_stop" -CONF_CONDITION = "condition" -CONF_CONDITIONS = "conditions" -CONF_CONTINUE_ON_TIMEOUT = "continue_on_timeout" -CONF_COUNT = "count" -CONF_COVERS = "covers" -CONF_CURRENCY = "currency" -CONF_CUSTOMIZE = "customize" -CONF_CUSTOMIZE_DOMAIN = "customize_domain" -CONF_CUSTOMIZE_GLOB = "customize_glob" -CONF_DEFAULT = "default" -CONF_DELAY = "delay" -CONF_DELAY_TIME = "delay_time" -CONF_DESCRIPTION = "description" -CONF_DEVICE = "device" -CONF_DEVICES = "devices" -CONF_DEVICE_CLASS = "device_class" -CONF_DEVICE_ID = "device_id" -CONF_DISARM_AFTER_TRIGGER = "disarm_after_trigger" -CONF_DISCOVERY = "discovery" -CONF_DISKS = "disks" -CONF_DISPLAY_CURRENCY = "display_currency" -CONF_DISPLAY_OPTIONS = "display_options" -CONF_DOMAIN = "domain" -CONF_DOMAINS = "domains" -CONF_EFFECT = "effect" -CONF_ELEVATION = "elevation" -CONF_EMAIL = "email" -CONF_ENTITIES = "entities" -CONF_ENTITY_ID = "entity_id" -CONF_ENTITY_NAMESPACE = "entity_namespace" -CONF_ENTITY_PICTURE_TEMPLATE = "entity_picture_template" -CONF_EVENT = "event" -CONF_EVENT_DATA = "event_data" -CONF_EVENT_DATA_TEMPLATE = "event_data_template" -CONF_EXCLUDE = "exclude" -CONF_EXTERNAL_URL = "external_url" -CONF_FILENAME = "filename" -CONF_FILE_PATH = "file_path" -CONF_FOR = "for" -CONF_FORCE_UPDATE = "force_update" -CONF_FRIENDLY_NAME = "friendly_name" -CONF_FRIENDLY_NAME_TEMPLATE = "friendly_name_template" -CONF_HEADERS = "headers" -CONF_HOST = "host" -CONF_HOSTS = "hosts" -CONF_HS = "hs" -CONF_ICON = "icon" -CONF_ICON_TEMPLATE = "icon_template" -CONF_ID = "id" -CONF_INCLUDE = "include" -CONF_INTERNAL_URL = "internal_url" -CONF_IP_ADDRESS = "ip_address" -CONF_LATITUDE = "latitude" -CONF_LEGACY_TEMPLATES = "legacy_templates" -CONF_LIGHTS = "lights" -CONF_LONGITUDE = "longitude" -CONF_MAC = "mac" -CONF_MAXIMUM = "maximum" -CONF_MEDIA_DIRS = "media_dirs" -CONF_METHOD = "method" -CONF_MINIMUM = "minimum" -CONF_MODE = "mode" -CONF_MONITORED_CONDITIONS = "monitored_conditions" -CONF_MONITORED_VARIABLES = "monitored_variables" -CONF_NAME = "name" -CONF_OFFSET = "offset" -CONF_OPTIMISTIC = "optimistic" -CONF_PACKAGES = "packages" -CONF_PARAMS = "params" -CONF_PASSWORD = "password" -CONF_PATH = "path" -CONF_PAYLOAD = "payload" -CONF_PAYLOAD_OFF = "payload_off" -CONF_PAYLOAD_ON = "payload_on" -CONF_PENDING_TIME = "pending_time" -CONF_PIN = "pin" -CONF_PLATFORM = "platform" -CONF_PORT = "port" -CONF_PREFIX = "prefix" -CONF_PROFILE_NAME = "profile_name" -CONF_PROTOCOL = "protocol" -CONF_PROXY_SSL = "proxy_ssl" -CONF_QUOTE = "quote" -CONF_RADIUS = "radius" -CONF_RECIPIENT = "recipient" -CONF_REGION = "region" -CONF_REPEAT = "repeat" -CONF_RESOURCE = "resource" -CONF_RESOURCES = "resources" -CONF_RESOURCE_TEMPLATE = "resource_template" -CONF_RGB = "rgb" -CONF_ROOM = "room" -CONF_SCAN_INTERVAL = "scan_interval" -CONF_SCENE = "scene" -CONF_SELECTOR = "selector" -CONF_SENDER = "sender" -CONF_SENSORS = "sensors" -CONF_SENSOR_TYPE = "sensor_type" -CONF_SEQUENCE = "sequence" -CONF_SERVICE = "service" -CONF_SERVICE_DATA = "data" -CONF_SERVICE_TEMPLATE = "service_template" -CONF_SHOW_ON_MAP = "show_on_map" -CONF_SLAVE = "slave" -CONF_SOURCE = "source" -CONF_SSL = "ssl" -CONF_STATE = "state" -CONF_STATE_TEMPLATE = "state_template" -CONF_STRUCTURE = "structure" -CONF_SWITCHES = "switches" -CONF_TARGET = "target" -CONF_TEMPERATURE_UNIT = "temperature_unit" -CONF_TIMEOUT = "timeout" -CONF_TIME_ZONE = "time_zone" -CONF_TOKEN = "token" -CONF_TRIGGER_TIME = "trigger_time" -CONF_TTL = "ttl" -CONF_TYPE = "type" -CONF_UNIQUE_ID = "unique_id" -CONF_UNIT_OF_MEASUREMENT = "unit_of_measurement" -CONF_UNIT_SYSTEM = "unit_system" -CONF_UNTIL = "until" -CONF_URL = "url" -CONF_USERNAME = "username" -CONF_VALUE_TEMPLATE = "value_template" -CONF_VARIABLES = "variables" -CONF_VERIFY_SSL = "verify_ssl" -CONF_WAIT_FOR_TRIGGER = "wait_for_trigger" -CONF_WAIT_TEMPLATE = "wait_template" -CONF_WEBHOOK_ID = "webhook_id" -CONF_WEEKDAY = "weekday" -CONF_WHILE = "while" -CONF_WHITELIST = "whitelist" -CONF_ALLOWLIST_EXTERNAL_DIRS = "allowlist_external_dirs" -LEGACY_CONF_WHITELIST_EXTERNAL_DIRS = "whitelist_external_dirs" -CONF_WHITE_VALUE = "white_value" -CONF_XY = "xy" -CONF_ZONE = "zone" +CONF_ABOVE: Final = "above" +CONF_ACCESS_TOKEN: Final = "access_token" +CONF_ADDRESS: Final = "address" +CONF_AFTER: Final = "after" +CONF_ALIAS: Final = "alias" +CONF_ALLOWLIST_EXTERNAL_URLS: Final = "allowlist_external_urls" +CONF_API_KEY: Final = "api_key" +CONF_API_TOKEN: Final = "api_token" +CONF_API_VERSION: Final = "api_version" +CONF_ARMING_TIME: Final = "arming_time" +CONF_AT: Final = "at" +CONF_ATTRIBUTE: Final = "attribute" +CONF_AUTH_MFA_MODULES: Final = "auth_mfa_modules" +CONF_AUTH_PROVIDERS: Final = "auth_providers" +CONF_AUTHENTICATION: Final = "authentication" +CONF_BASE: Final = "base" +CONF_BEFORE: Final = "before" +CONF_BELOW: Final = "below" +CONF_BINARY_SENSORS: Final = "binary_sensors" +CONF_BRIGHTNESS: Final = "brightness" +CONF_BROADCAST_ADDRESS: Final = "broadcast_address" +CONF_BROADCAST_PORT: Final = "broadcast_port" +CONF_CHOOSE: Final = "choose" +CONF_CLIENT_ID: Final = "client_id" +CONF_CLIENT_SECRET: Final = "client_secret" +CONF_CODE: Final = "code" +CONF_COLOR_TEMP: Final = "color_temp" +CONF_COMMAND: Final = "command" +CONF_COMMAND_CLOSE: Final = "command_close" +CONF_COMMAND_OFF: Final = "command_off" +CONF_COMMAND_ON: Final = "command_on" +CONF_COMMAND_OPEN: Final = "command_open" +CONF_COMMAND_STATE: Final = "command_state" +CONF_COMMAND_STOP: Final = "command_stop" +CONF_CONDITION: Final = "condition" +CONF_CONDITIONS: Final = "conditions" +CONF_CONTINUE_ON_TIMEOUT: Final = "continue_on_timeout" +CONF_COUNT: Final = "count" +CONF_COVERS: Final = "covers" +CONF_CURRENCY: Final = "currency" +CONF_CUSTOMIZE: Final = "customize" +CONF_CUSTOMIZE_DOMAIN: Final = "customize_domain" +CONF_CUSTOMIZE_GLOB: Final = "customize_glob" +CONF_DEFAULT: Final = "default" +CONF_DELAY: Final = "delay" +CONF_DELAY_TIME: Final = "delay_time" +CONF_DESCRIPTION: Final = "description" +CONF_DEVICE: Final = "device" +CONF_DEVICES: Final = "devices" +CONF_DEVICE_CLASS: Final = "device_class" +CONF_DEVICE_ID: Final = "device_id" +CONF_DISARM_AFTER_TRIGGER: Final = "disarm_after_trigger" +CONF_DISCOVERY: Final = "discovery" +CONF_DISKS: Final = "disks" +CONF_DISPLAY_CURRENCY: Final = "display_currency" +CONF_DISPLAY_OPTIONS: Final = "display_options" +CONF_DOMAIN: Final = "domain" +CONF_DOMAINS: Final = "domains" +CONF_EFFECT: Final = "effect" +CONF_ELEVATION: Final = "elevation" +CONF_EMAIL: Final = "email" +CONF_ENTITIES: Final = "entities" +CONF_ENTITY_ID: Final = "entity_id" +CONF_ENTITY_NAMESPACE: Final = "entity_namespace" +CONF_ENTITY_PICTURE_TEMPLATE: Final = "entity_picture_template" +CONF_EVENT: Final = "event" +CONF_EVENT_DATA: Final = "event_data" +CONF_EVENT_DATA_TEMPLATE: Final = "event_data_template" +CONF_EXCLUDE: Final = "exclude" +CONF_EXTERNAL_URL: Final = "external_url" +CONF_FILENAME: Final = "filename" +CONF_FILE_PATH: Final = "file_path" +CONF_FOR: Final = "for" +CONF_FORCE_UPDATE: Final = "force_update" +CONF_FRIENDLY_NAME: Final = "friendly_name" +CONF_FRIENDLY_NAME_TEMPLATE: Final = "friendly_name_template" +CONF_HEADERS: Final = "headers" +CONF_HOST: Final = "host" +CONF_HOSTS: Final = "hosts" +CONF_HS: Final = "hs" +CONF_ICON: Final = "icon" +CONF_ICON_TEMPLATE: Final = "icon_template" +CONF_ID: Final = "id" +CONF_INCLUDE: Final = "include" +CONF_INTERNAL_URL: Final = "internal_url" +CONF_IP_ADDRESS: Final = "ip_address" +CONF_LATITUDE: Final = "latitude" +CONF_LEGACY_TEMPLATES: Final = "legacy_templates" +CONF_LIGHTS: Final = "lights" +CONF_LONGITUDE: Final = "longitude" +CONF_MAC: Final = "mac" +CONF_MAXIMUM: Final = "maximum" +CONF_MEDIA_DIRS: Final = "media_dirs" +CONF_METHOD: Final = "method" +CONF_MINIMUM: Final = "minimum" +CONF_MODE: Final = "mode" +CONF_MONITORED_CONDITIONS: Final = "monitored_conditions" +CONF_MONITORED_VARIABLES: Final = "monitored_variables" +CONF_NAME: Final = "name" +CONF_OFFSET: Final = "offset" +CONF_OPTIMISTIC: Final = "optimistic" +CONF_PACKAGES: Final = "packages" +CONF_PARAMS: Final = "params" +CONF_PASSWORD: Final = "password" +CONF_PATH: Final = "path" +CONF_PAYLOAD: Final = "payload" +CONF_PAYLOAD_OFF: Final = "payload_off" +CONF_PAYLOAD_ON: Final = "payload_on" +CONF_PENDING_TIME: Final = "pending_time" +CONF_PIN: Final = "pin" +CONF_PLATFORM: Final = "platform" +CONF_PORT: Final = "port" +CONF_PREFIX: Final = "prefix" +CONF_PROFILE_NAME: Final = "profile_name" +CONF_PROTOCOL: Final = "protocol" +CONF_PROXY_SSL: Final = "proxy_ssl" +CONF_QUOTE: Final = "quote" +CONF_RADIUS: Final = "radius" +CONF_RECIPIENT: Final = "recipient" +CONF_REGION: Final = "region" +CONF_REPEAT: Final = "repeat" +CONF_RESOURCE: Final = "resource" +CONF_RESOURCES: Final = "resources" +CONF_RESOURCE_TEMPLATE: Final = "resource_template" +CONF_RGB: Final = "rgb" +CONF_ROOM: Final = "room" +CONF_SCAN_INTERVAL: Final = "scan_interval" +CONF_SCENE: Final = "scene" +CONF_SELECTOR: Final = "selector" +CONF_SENDER: Final = "sender" +CONF_SENSORS: Final = "sensors" +CONF_SENSOR_TYPE: Final = "sensor_type" +CONF_SEQUENCE: Final = "sequence" +CONF_SERVICE: Final = "service" +CONF_SERVICE_DATA: Final = "data" +CONF_SERVICE_TEMPLATE: Final = "service_template" +CONF_SHOW_ON_MAP: Final = "show_on_map" +CONF_SLAVE: Final = "slave" +CONF_SOURCE: Final = "source" +CONF_SSL: Final = "ssl" +CONF_STATE: Final = "state" +CONF_STATE_TEMPLATE: Final = "state_template" +CONF_STRUCTURE: Final = "structure" +CONF_SWITCHES: Final = "switches" +CONF_TARGET: Final = "target" +CONF_TEMPERATURE_UNIT: Final = "temperature_unit" +CONF_TIMEOUT: Final = "timeout" +CONF_TIME_ZONE: Final = "time_zone" +CONF_TOKEN: Final = "token" +CONF_TRIGGER_TIME: Final = "trigger_time" +CONF_TTL: Final = "ttl" +CONF_TYPE: Final = "type" +CONF_UNIQUE_ID: Final = "unique_id" +CONF_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement" +CONF_UNIT_SYSTEM: Final = "unit_system" +CONF_UNTIL: Final = "until" +CONF_URL: Final = "url" +CONF_USERNAME: Final = "username" +CONF_VALUE_TEMPLATE: Final = "value_template" +CONF_VARIABLES: Final = "variables" +CONF_VERIFY_SSL: Final = "verify_ssl" +CONF_WAIT_FOR_TRIGGER: Final = "wait_for_trigger" +CONF_WAIT_TEMPLATE: Final = "wait_template" +CONF_WEBHOOK_ID: Final = "webhook_id" +CONF_WEEKDAY: Final = "weekday" +CONF_WHILE: Final = "while" +CONF_WHITELIST: Final = "whitelist" +CONF_ALLOWLIST_EXTERNAL_DIRS: Final = "allowlist_external_dirs" +LEGACY_CONF_WHITELIST_EXTERNAL_DIRS: Final = "whitelist_external_dirs" +CONF_WHITE_VALUE: Final = "white_value" +CONF_XY: Final = "xy" +CONF_ZONE: Final = "zone" # #### EVENTS #### -EVENT_CALL_SERVICE = "call_service" -EVENT_COMPONENT_LOADED = "component_loaded" -EVENT_CORE_CONFIG_UPDATE = "core_config_updated" -EVENT_HOMEASSISTANT_CLOSE = "homeassistant_close" -EVENT_HOMEASSISTANT_START = "homeassistant_start" -EVENT_HOMEASSISTANT_STARTED = "homeassistant_started" -EVENT_HOMEASSISTANT_STOP = "homeassistant_stop" -EVENT_HOMEASSISTANT_FINAL_WRITE = "homeassistant_final_write" -EVENT_LOGBOOK_ENTRY = "logbook_entry" -EVENT_SERVICE_REGISTERED = "service_registered" -EVENT_SERVICE_REMOVED = "service_removed" -EVENT_STATE_CHANGED = "state_changed" -EVENT_THEMES_UPDATED = "themes_updated" -EVENT_TIMER_OUT_OF_SYNC = "timer_out_of_sync" -EVENT_TIME_CHANGED = "time_changed" +EVENT_CALL_SERVICE: Final = "call_service" +EVENT_COMPONENT_LOADED: Final = "component_loaded" +EVENT_CORE_CONFIG_UPDATE: Final = "core_config_updated" +EVENT_HOMEASSISTANT_CLOSE: Final = "homeassistant_close" +EVENT_HOMEASSISTANT_START: Final = "homeassistant_start" +EVENT_HOMEASSISTANT_STARTED: Final = "homeassistant_started" +EVENT_HOMEASSISTANT_STOP: Final = "homeassistant_stop" +EVENT_HOMEASSISTANT_FINAL_WRITE: Final = "homeassistant_final_write" +EVENT_LOGBOOK_ENTRY: Final = "logbook_entry" +EVENT_SERVICE_REGISTERED: Final = "service_registered" +EVENT_SERVICE_REMOVED: Final = "service_removed" +EVENT_STATE_CHANGED: Final = "state_changed" +EVENT_THEMES_UPDATED: Final = "themes_updated" +EVENT_TIMER_OUT_OF_SYNC: Final = "timer_out_of_sync" +EVENT_TIME_CHANGED: Final = "time_changed" # #### DEVICE CLASSES #### -DEVICE_CLASS_BATTERY = "battery" -DEVICE_CLASS_CO = "carbon_monoxide" -DEVICE_CLASS_CO2 = "carbon_dioxide" -DEVICE_CLASS_HUMIDITY = "humidity" -DEVICE_CLASS_ILLUMINANCE = "illuminance" -DEVICE_CLASS_SIGNAL_STRENGTH = "signal_strength" -DEVICE_CLASS_TEMPERATURE = "temperature" -DEVICE_CLASS_TIMESTAMP = "timestamp" -DEVICE_CLASS_PRESSURE = "pressure" -DEVICE_CLASS_POWER = "power" -DEVICE_CLASS_CURRENT = "current" -DEVICE_CLASS_ENERGY = "energy" -DEVICE_CLASS_POWER_FACTOR = "power_factor" -DEVICE_CLASS_VOLTAGE = "voltage" +DEVICE_CLASS_BATTERY: Final = "battery" +DEVICE_CLASS_CO: Final = "carbon_monoxide" +DEVICE_CLASS_CO2: Final = "carbon_dioxide" +DEVICE_CLASS_HUMIDITY: Final = "humidity" +DEVICE_CLASS_ILLUMINANCE: Final = "illuminance" +DEVICE_CLASS_SIGNAL_STRENGTH: Final = "signal_strength" +DEVICE_CLASS_TEMPERATURE: Final = "temperature" +DEVICE_CLASS_TIMESTAMP: Final = "timestamp" +DEVICE_CLASS_PRESSURE: Final = "pressure" +DEVICE_CLASS_POWER: Final = "power" +DEVICE_CLASS_CURRENT: Final = "current" +DEVICE_CLASS_ENERGY: Final = "energy" +DEVICE_CLASS_POWER_FACTOR: Final = "power_factor" +DEVICE_CLASS_VOLTAGE: Final = "voltage" # #### STATES #### -STATE_ON = "on" -STATE_OFF = "off" -STATE_HOME = "home" -STATE_NOT_HOME = "not_home" -STATE_UNKNOWN = "unknown" -STATE_OPEN = "open" -STATE_OPENING = "opening" -STATE_CLOSED = "closed" -STATE_CLOSING = "closing" -STATE_PLAYING = "playing" -STATE_PAUSED = "paused" -STATE_IDLE = "idle" -STATE_STANDBY = "standby" -STATE_ALARM_DISARMED = "disarmed" -STATE_ALARM_ARMED_HOME = "armed_home" -STATE_ALARM_ARMED_AWAY = "armed_away" -STATE_ALARM_ARMED_NIGHT = "armed_night" -STATE_ALARM_ARMED_CUSTOM_BYPASS = "armed_custom_bypass" -STATE_ALARM_PENDING = "pending" -STATE_ALARM_ARMING = "arming" -STATE_ALARM_DISARMING = "disarming" -STATE_ALARM_TRIGGERED = "triggered" -STATE_LOCKED = "locked" -STATE_UNLOCKED = "unlocked" -STATE_UNAVAILABLE = "unavailable" -STATE_OK = "ok" -STATE_PROBLEM = "problem" +STATE_ON: Final = "on" +STATE_OFF: Final = "off" +STATE_HOME: Final = "home" +STATE_NOT_HOME: Final = "not_home" +STATE_UNKNOWN: Final = "unknown" +STATE_OPEN: Final = "open" +STATE_OPENING: Final = "opening" +STATE_CLOSED: Final = "closed" +STATE_CLOSING: Final = "closing" +STATE_PLAYING: Final = "playing" +STATE_PAUSED: Final = "paused" +STATE_IDLE: Final = "idle" +STATE_STANDBY: Final = "standby" +STATE_ALARM_DISARMED: Final = "disarmed" +STATE_ALARM_ARMED_HOME: Final = "armed_home" +STATE_ALARM_ARMED_AWAY: Final = "armed_away" +STATE_ALARM_ARMED_NIGHT: Final = "armed_night" +STATE_ALARM_ARMED_CUSTOM_BYPASS: Final = "armed_custom_bypass" +STATE_ALARM_PENDING: Final = "pending" +STATE_ALARM_ARMING: Final = "arming" +STATE_ALARM_DISARMING: Final = "disarming" +STATE_ALARM_TRIGGERED: Final = "triggered" +STATE_LOCKED: Final = "locked" +STATE_UNLOCKED: Final = "unlocked" +STATE_UNAVAILABLE: Final = "unavailable" +STATE_OK: Final = "ok" +STATE_PROBLEM: Final = "problem" # #### STATE AND EVENT ATTRIBUTES #### # Attribution -ATTR_ATTRIBUTION = "attribution" +ATTR_ATTRIBUTION: Final = "attribution" # Credentials -ATTR_CREDENTIALS = "credentials" +ATTR_CREDENTIALS: Final = "credentials" # Contains time-related attributes -ATTR_NOW = "now" -ATTR_DATE = "date" -ATTR_TIME = "time" -ATTR_SECONDS = "seconds" +ATTR_NOW: Final = "now" +ATTR_DATE: Final = "date" +ATTR_TIME: Final = "time" +ATTR_SECONDS: Final = "seconds" # Contains domain, service for a SERVICE_CALL event -ATTR_DOMAIN = "domain" -ATTR_SERVICE = "service" -ATTR_SERVICE_DATA = "service_data" +ATTR_DOMAIN: Final = "domain" +ATTR_SERVICE: Final = "service" +ATTR_SERVICE_DATA: Final = "service_data" # IDs -ATTR_ID = "id" +ATTR_ID: Final = "id" # Name -ATTR_NAME = "name" +ATTR_NAME: Final = "name" # Contains one string or a list of strings, each being an entity id -ATTR_ENTITY_ID = "entity_id" +ATTR_ENTITY_ID: Final = "entity_id" # Contains one string or a list of strings, each being an area id -ATTR_AREA_ID = "area_id" +ATTR_AREA_ID: Final = "area_id" # Contains one string, the device ID -ATTR_DEVICE_ID = "device_id" +ATTR_DEVICE_ID: Final = "device_id" # String with a friendly name for the entity -ATTR_FRIENDLY_NAME = "friendly_name" +ATTR_FRIENDLY_NAME: Final = "friendly_name" # A picture to represent entity -ATTR_ENTITY_PICTURE = "entity_picture" +ATTR_ENTITY_PICTURE: Final = "entity_picture" + +ATTR_IDENTIFIERS: Final = "identifiers" # Icon to use in the frontend -ATTR_ICON = "icon" +ATTR_ICON: Final = "icon" # The unit of measurement if applicable -ATTR_UNIT_OF_MEASUREMENT = "unit_of_measurement" +ATTR_UNIT_OF_MEASUREMENT: Final = "unit_of_measurement" -CONF_UNIT_SYSTEM_METRIC: str = "metric" -CONF_UNIT_SYSTEM_IMPERIAL: str = "imperial" +CONF_UNIT_SYSTEM_METRIC: Final = "metric" +CONF_UNIT_SYSTEM_IMPERIAL: Final = "imperial" # Electrical attributes -ATTR_VOLTAGE = "voltage" +ATTR_VOLTAGE: Final = "voltage" # Location of the device/sensor -ATTR_LOCATION = "location" +ATTR_LOCATION: Final = "location" -ATTR_MODE = "mode" +ATTR_MODE: Final = "mode" -ATTR_BATTERY_CHARGING = "battery_charging" -ATTR_BATTERY_LEVEL = "battery_level" -ATTR_WAKEUP = "wake_up_interval" +ATTR_MANUFACTURER: Final = "manufacturer" +ATTR_MODEL: Final = "model" +ATTR_SW_VERSION: Final = "sw_version" + +ATTR_BATTERY_CHARGING: Final = "battery_charging" +ATTR_BATTERY_LEVEL: Final = "battery_level" +ATTR_WAKEUP: Final = "wake_up_interval" # For devices which support a code attribute -ATTR_CODE = "code" -ATTR_CODE_FORMAT = "code_format" +ATTR_CODE: Final = "code" +ATTR_CODE_FORMAT: Final = "code_format" # For calling a device specific command -ATTR_COMMAND = "command" +ATTR_COMMAND: Final = "command" # For devices which support an armed state -ATTR_ARMED = "device_armed" +ATTR_ARMED: Final = "device_armed" # For devices which support a locked state -ATTR_LOCKED = "locked" +ATTR_LOCKED: Final = "locked" # For sensors that support 'tripping', eg. motion and door sensors -ATTR_TRIPPED = "device_tripped" +ATTR_TRIPPED: Final = "device_tripped" # For sensors that support 'tripping' this holds the most recent # time the device was tripped -ATTR_LAST_TRIP_TIME = "last_tripped_time" +ATTR_LAST_TRIP_TIME: Final = "last_tripped_time" # For all entity's, this hold whether or not it should be hidden -ATTR_HIDDEN = "hidden" +ATTR_HIDDEN: Final = "hidden" # Location of the entity -ATTR_LATITUDE = "latitude" -ATTR_LONGITUDE = "longitude" +ATTR_LATITUDE: Final = "latitude" +ATTR_LONGITUDE: Final = "longitude" # Accuracy of location in meters -ATTR_GPS_ACCURACY = "gps_accuracy" +ATTR_GPS_ACCURACY: Final = "gps_accuracy" # If state is assumed -ATTR_ASSUMED_STATE = "assumed_state" -ATTR_STATE = "state" +ATTR_ASSUMED_STATE: Final = "assumed_state" +ATTR_STATE: Final = "state" -ATTR_EDITABLE = "editable" -ATTR_OPTION = "option" +ATTR_EDITABLE: Final = "editable" +ATTR_OPTION: Final = "option" # The entity has been restored with restore state -ATTR_RESTORED = "restored" +ATTR_RESTORED: Final = "restored" # Bitfield of supported component features for the entity -ATTR_SUPPORTED_FEATURES = "supported_features" +ATTR_SUPPORTED_FEATURES: Final = "supported_features" # Class of device within its domain -ATTR_DEVICE_CLASS = "device_class" +ATTR_DEVICE_CLASS: Final = "device_class" # Temperature attribute -ATTR_TEMPERATURE = "temperature" +ATTR_TEMPERATURE: Final = "temperature" # #### UNITS OF MEASUREMENT #### # Power units -POWER_WATT = "W" -POWER_KILO_WATT = "kW" +POWER_WATT: Final = "W" +POWER_KILO_WATT: Final = "kW" # Voltage units -VOLT = "V" +VOLT: Final = "V" # Energy units -ENERGY_WATT_HOUR = "Wh" -ENERGY_KILO_WATT_HOUR = "kWh" +ENERGY_WATT_HOUR: Final = "Wh" +ENERGY_KILO_WATT_HOUR: Final = "kWh" # Electrical units -ELECTRICAL_CURRENT_AMPERE = "A" -ELECTRICAL_VOLT_AMPERE = "VA" +ELECTRICAL_CURRENT_AMPERE: Final = "A" +ELECTRICAL_VOLT_AMPERE: Final = "VA" # Degree units -DEGREE = "°" +DEGREE: Final = "°" # Currency units -CURRENCY_EURO = "€" -CURRENCY_DOLLAR = "$" -CURRENCY_CENT = "¢" +CURRENCY_EURO: Final = "€" +CURRENCY_DOLLAR: Final = "$" +CURRENCY_CENT: Final = "¢" # Temperature units -TEMP_CELSIUS = "°C" -TEMP_FAHRENHEIT = "°F" -TEMP_KELVIN = "K" +TEMP_CELSIUS: Final = "°C" +TEMP_FAHRENHEIT: Final = "°F" +TEMP_KELVIN: Final = "K" # Time units -TIME_MICROSECONDS = "μs" -TIME_MILLISECONDS = "ms" -TIME_SECONDS = "s" -TIME_MINUTES = "min" -TIME_HOURS = "h" -TIME_DAYS = "d" -TIME_WEEKS = "w" -TIME_MONTHS = "m" -TIME_YEARS = "y" +TIME_MICROSECONDS: Final = "μs" +TIME_MILLISECONDS: Final = "ms" +TIME_SECONDS: Final = "s" +TIME_MINUTES: Final = "min" +TIME_HOURS: Final = "h" +TIME_DAYS: Final = "d" +TIME_WEEKS: Final = "w" +TIME_MONTHS: Final = "m" +TIME_YEARS: Final = "y" # Length units -LENGTH_MILLIMETERS: str = "mm" -LENGTH_CENTIMETERS: str = "cm" -LENGTH_METERS: str = "m" -LENGTH_KILOMETERS: str = "km" +LENGTH_MILLIMETERS: Final = "mm" +LENGTH_CENTIMETERS: Final = "cm" +LENGTH_METERS: Final = "m" +LENGTH_KILOMETERS: Final = "km" -LENGTH_INCHES: str = "in" -LENGTH_FEET: str = "ft" -LENGTH_YARD: str = "yd" -LENGTH_MILES: str = "mi" +LENGTH_INCHES: Final = "in" +LENGTH_FEET: Final = "ft" +LENGTH_YARD: Final = "yd" +LENGTH_MILES: Final = "mi" # Frequency units -FREQUENCY_HERTZ = "Hz" -FREQUENCY_GIGAHERTZ = "GHz" +FREQUENCY_HERTZ: Final = "Hz" +FREQUENCY_GIGAHERTZ: Final = "GHz" # Pressure units -PRESSURE_PA: str = "Pa" -PRESSURE_HPA: str = "hPa" -PRESSURE_BAR: str = "bar" -PRESSURE_MBAR: str = "mbar" -PRESSURE_INHG: str = "inHg" -PRESSURE_PSI: str = "psi" +PRESSURE_PA: Final = "Pa" +PRESSURE_HPA: Final = "hPa" +PRESSURE_BAR: Final = "bar" +PRESSURE_MBAR: Final = "mbar" +PRESSURE_INHG: Final = "inHg" +PRESSURE_PSI: Final = "psi" # Volume units -VOLUME_LITERS: str = "L" -VOLUME_MILLILITERS: str = "mL" -VOLUME_CUBIC_METERS = "m³" -VOLUME_CUBIC_FEET = "ft³" +VOLUME_LITERS: Final = "L" +VOLUME_MILLILITERS: Final = "mL" +VOLUME_CUBIC_METERS: Final = "m³" +VOLUME_CUBIC_FEET: Final = "ft³" -VOLUME_GALLONS: str = "gal" -VOLUME_FLUID_OUNCE: str = "fl. oz." +VOLUME_GALLONS: Final = "gal" +VOLUME_FLUID_OUNCE: Final = "fl. oz." # Volume Flow Rate units -VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR = "m³/h" -VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE = "ft³/m" +VOLUME_FLOW_RATE_CUBIC_METERS_PER_HOUR: Final = "m³/h" +VOLUME_FLOW_RATE_CUBIC_FEET_PER_MINUTE: Final = "ft³/m" # Area units -AREA_SQUARE_METERS = "m²" +AREA_SQUARE_METERS: Final = "m²" # Mass units -MASS_GRAMS: str = "g" -MASS_KILOGRAMS: str = "kg" -MASS_MILLIGRAMS = "mg" -MASS_MICROGRAMS = "µg" +MASS_GRAMS: Final = "g" +MASS_KILOGRAMS: Final = "kg" +MASS_MILLIGRAMS: Final = "mg" +MASS_MICROGRAMS: Final = "µg" -MASS_OUNCES: str = "oz" -MASS_POUNDS: str = "lb" +MASS_OUNCES: Final = "oz" +MASS_POUNDS: Final = "lb" # Conductivity units -CONDUCTIVITY: str = "µS/cm" +CONDUCTIVITY: Final = "µS/cm" # Light units -LIGHT_LUX: str = "lx" +LIGHT_LUX: Final = "lx" # UV Index units -UV_INDEX: str = "UV index" +UV_INDEX: Final = "UV index" # Percentage units -PERCENTAGE = "%" +PERCENTAGE: Final = "%" # Irradiation units -IRRADIATION_WATTS_PER_SQUARE_METER = "W/m²" +IRRADIATION_WATTS_PER_SQUARE_METER: Final = "W/m²" # Precipitation units -PRECIPITATION_MILLIMETERS_PER_HOUR = "mm/h" +PRECIPITATION_MILLIMETERS_PER_HOUR: Final = "mm/h" # Concentration units -CONCENTRATION_MICROGRAMS_PER_CUBIC_METER = "µg/m³" -CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER = "mg/m³" -CONCENTRATION_PARTS_PER_CUBIC_METER = "p/m³" -CONCENTRATION_PARTS_PER_MILLION = "ppm" -CONCENTRATION_PARTS_PER_BILLION = "ppb" +CONCENTRATION_MICROGRAMS_PER_CUBIC_METER: Final = "µg/m³" +CONCENTRATION_MILLIGRAMS_PER_CUBIC_METER: Final = "mg/m³" +CONCENTRATION_PARTS_PER_CUBIC_METER: Final = "p/m³" +CONCENTRATION_PARTS_PER_MILLION: Final = "ppm" +CONCENTRATION_PARTS_PER_BILLION: Final = "ppb" # Speed units -SPEED_MILLIMETERS_PER_DAY = "mm/d" -SPEED_INCHES_PER_DAY = "in/d" -SPEED_METERS_PER_SECOND = "m/s" -SPEED_INCHES_PER_HOUR = "in/h" -SPEED_KILOMETERS_PER_HOUR = "km/h" -SPEED_MILES_PER_HOUR = "mph" +SPEED_MILLIMETERS_PER_DAY: Final = "mm/d" +SPEED_INCHES_PER_DAY: Final = "in/d" +SPEED_METERS_PER_SECOND: Final = "m/s" +SPEED_INCHES_PER_HOUR: Final = "in/h" +SPEED_KILOMETERS_PER_HOUR: Final = "km/h" +SPEED_MILES_PER_HOUR: Final = "mph" # Signal_strength units -SIGNAL_STRENGTH_DECIBELS = "dB" -SIGNAL_STRENGTH_DECIBELS_MILLIWATT = "dBm" +SIGNAL_STRENGTH_DECIBELS: Final = "dB" +SIGNAL_STRENGTH_DECIBELS_MILLIWATT: Final = "dBm" # Data units -DATA_BITS = "bit" -DATA_KILOBITS = "kbit" -DATA_MEGABITS = "Mbit" -DATA_GIGABITS = "Gbit" -DATA_BYTES = "B" -DATA_KILOBYTES = "kB" -DATA_MEGABYTES = "MB" -DATA_GIGABYTES = "GB" -DATA_TERABYTES = "TB" -DATA_PETABYTES = "PB" -DATA_EXABYTES = "EB" -DATA_ZETTABYTES = "ZB" -DATA_YOTTABYTES = "YB" -DATA_KIBIBYTES = "KiB" -DATA_MEBIBYTES = "MiB" -DATA_GIBIBYTES = "GiB" -DATA_TEBIBYTES = "TiB" -DATA_PEBIBYTES = "PiB" -DATA_EXBIBYTES = "EiB" -DATA_ZEBIBYTES = "ZiB" -DATA_YOBIBYTES = "YiB" -DATA_RATE_BITS_PER_SECOND = "bit/s" -DATA_RATE_KILOBITS_PER_SECOND = "kbit/s" -DATA_RATE_MEGABITS_PER_SECOND = "Mbit/s" -DATA_RATE_GIGABITS_PER_SECOND = "Gbit/s" -DATA_RATE_BYTES_PER_SECOND = "B/s" -DATA_RATE_KILOBYTES_PER_SECOND = "kB/s" -DATA_RATE_MEGABYTES_PER_SECOND = "MB/s" -DATA_RATE_GIGABYTES_PER_SECOND = "GB/s" -DATA_RATE_KIBIBYTES_PER_SECOND = "KiB/s" -DATA_RATE_MEBIBYTES_PER_SECOND = "MiB/s" -DATA_RATE_GIBIBYTES_PER_SECOND = "GiB/s" +DATA_BITS: Final = "bit" +DATA_KILOBITS: Final = "kbit" +DATA_MEGABITS: Final = "Mbit" +DATA_GIGABITS: Final = "Gbit" +DATA_BYTES: Final = "B" +DATA_KILOBYTES: Final = "kB" +DATA_MEGABYTES: Final = "MB" +DATA_GIGABYTES: Final = "GB" +DATA_TERABYTES: Final = "TB" +DATA_PETABYTES: Final = "PB" +DATA_EXABYTES: Final = "EB" +DATA_ZETTABYTES: Final = "ZB" +DATA_YOTTABYTES: Final = "YB" +DATA_KIBIBYTES: Final = "KiB" +DATA_MEBIBYTES: Final = "MiB" +DATA_GIBIBYTES: Final = "GiB" +DATA_TEBIBYTES: Final = "TiB" +DATA_PEBIBYTES: Final = "PiB" +DATA_EXBIBYTES: Final = "EiB" +DATA_ZEBIBYTES: Final = "ZiB" +DATA_YOBIBYTES: Final = "YiB" +DATA_RATE_BITS_PER_SECOND: Final = "bit/s" +DATA_RATE_KILOBITS_PER_SECOND: Final = "kbit/s" +DATA_RATE_MEGABITS_PER_SECOND: Final = "Mbit/s" +DATA_RATE_GIGABITS_PER_SECOND: Final = "Gbit/s" +DATA_RATE_BYTES_PER_SECOND: Final = "B/s" +DATA_RATE_KILOBYTES_PER_SECOND: Final = "kB/s" +DATA_RATE_MEGABYTES_PER_SECOND: Final = "MB/s" +DATA_RATE_GIGABYTES_PER_SECOND: Final = "GB/s" +DATA_RATE_KIBIBYTES_PER_SECOND: Final = "KiB/s" +DATA_RATE_MEBIBYTES_PER_SECOND: Final = "MiB/s" +DATA_RATE_GIBIBYTES_PER_SECOND: Final = "GiB/s" # #### SERVICES #### -SERVICE_HOMEASSISTANT_STOP = "stop" -SERVICE_HOMEASSISTANT_RESTART = "restart" +SERVICE_HOMEASSISTANT_STOP: Final = "stop" +SERVICE_HOMEASSISTANT_RESTART: Final = "restart" -SERVICE_TURN_ON = "turn_on" -SERVICE_TURN_OFF = "turn_off" -SERVICE_TOGGLE = "toggle" -SERVICE_RELOAD = "reload" +SERVICE_TURN_ON: Final = "turn_on" +SERVICE_TURN_OFF: Final = "turn_off" +SERVICE_TOGGLE: Final = "toggle" +SERVICE_RELOAD: Final = "reload" -SERVICE_VOLUME_UP = "volume_up" -SERVICE_VOLUME_DOWN = "volume_down" -SERVICE_VOLUME_MUTE = "volume_mute" -SERVICE_VOLUME_SET = "volume_set" -SERVICE_MEDIA_PLAY_PAUSE = "media_play_pause" -SERVICE_MEDIA_PLAY = "media_play" -SERVICE_MEDIA_PAUSE = "media_pause" -SERVICE_MEDIA_STOP = "media_stop" -SERVICE_MEDIA_NEXT_TRACK = "media_next_track" -SERVICE_MEDIA_PREVIOUS_TRACK = "media_previous_track" -SERVICE_MEDIA_SEEK = "media_seek" -SERVICE_REPEAT_SET = "repeat_set" -SERVICE_SHUFFLE_SET = "shuffle_set" +SERVICE_VOLUME_UP: Final = "volume_up" +SERVICE_VOLUME_DOWN: Final = "volume_down" +SERVICE_VOLUME_MUTE: Final = "volume_mute" +SERVICE_VOLUME_SET: Final = "volume_set" +SERVICE_MEDIA_PLAY_PAUSE: Final = "media_play_pause" +SERVICE_MEDIA_PLAY: Final = "media_play" +SERVICE_MEDIA_PAUSE: Final = "media_pause" +SERVICE_MEDIA_STOP: Final = "media_stop" +SERVICE_MEDIA_NEXT_TRACK: Final = "media_next_track" +SERVICE_MEDIA_PREVIOUS_TRACK: Final = "media_previous_track" +SERVICE_MEDIA_SEEK: Final = "media_seek" +SERVICE_REPEAT_SET: Final = "repeat_set" +SERVICE_SHUFFLE_SET: Final = "shuffle_set" -SERVICE_ALARM_DISARM = "alarm_disarm" -SERVICE_ALARM_ARM_HOME = "alarm_arm_home" -SERVICE_ALARM_ARM_AWAY = "alarm_arm_away" -SERVICE_ALARM_ARM_NIGHT = "alarm_arm_night" -SERVICE_ALARM_ARM_CUSTOM_BYPASS = "alarm_arm_custom_bypass" -SERVICE_ALARM_TRIGGER = "alarm_trigger" +SERVICE_ALARM_DISARM: Final = "alarm_disarm" +SERVICE_ALARM_ARM_HOME: Final = "alarm_arm_home" +SERVICE_ALARM_ARM_AWAY: Final = "alarm_arm_away" +SERVICE_ALARM_ARM_NIGHT: Final = "alarm_arm_night" +SERVICE_ALARM_ARM_CUSTOM_BYPASS: Final = "alarm_arm_custom_bypass" +SERVICE_ALARM_TRIGGER: Final = "alarm_trigger" -SERVICE_LOCK = "lock" -SERVICE_UNLOCK = "unlock" +SERVICE_LOCK: Final = "lock" +SERVICE_UNLOCK: Final = "unlock" -SERVICE_OPEN = "open" -SERVICE_CLOSE = "close" +SERVICE_OPEN: Final = "open" +SERVICE_CLOSE: Final = "close" -SERVICE_CLOSE_COVER = "close_cover" -SERVICE_CLOSE_COVER_TILT = "close_cover_tilt" -SERVICE_OPEN_COVER = "open_cover" -SERVICE_OPEN_COVER_TILT = "open_cover_tilt" -SERVICE_SET_COVER_POSITION = "set_cover_position" -SERVICE_SET_COVER_TILT_POSITION = "set_cover_tilt_position" -SERVICE_STOP_COVER = "stop_cover" -SERVICE_STOP_COVER_TILT = "stop_cover_tilt" -SERVICE_TOGGLE_COVER_TILT = "toggle_cover_tilt" +SERVICE_CLOSE_COVER: Final = "close_cover" +SERVICE_CLOSE_COVER_TILT: Final = "close_cover_tilt" +SERVICE_OPEN_COVER: Final = "open_cover" +SERVICE_OPEN_COVER_TILT: Final = "open_cover_tilt" +SERVICE_SET_COVER_POSITION: Final = "set_cover_position" +SERVICE_SET_COVER_TILT_POSITION: Final = "set_cover_tilt_position" +SERVICE_STOP_COVER: Final = "stop_cover" +SERVICE_STOP_COVER_TILT: Final = "stop_cover_tilt" +SERVICE_TOGGLE_COVER_TILT: Final = "toggle_cover_tilt" -SERVICE_SELECT_OPTION = "select_option" +SERVICE_SELECT_OPTION: Final = "select_option" # #### API / REMOTE #### -SERVER_PORT = 8123 +SERVER_PORT: Final = 8123 -URL_ROOT = "/" -URL_API = "/api/" -URL_API_STREAM = "/api/stream" -URL_API_CONFIG = "/api/config" -URL_API_DISCOVERY_INFO = "/api/discovery_info" -URL_API_STATES = "/api/states" -URL_API_STATES_ENTITY = "/api/states/{}" -URL_API_EVENTS = "/api/events" -URL_API_EVENTS_EVENT = "/api/events/{}" -URL_API_SERVICES = "/api/services" -URL_API_SERVICES_SERVICE = "/api/services/{}/{}" -URL_API_COMPONENTS = "/api/components" -URL_API_ERROR_LOG = "/api/error_log" -URL_API_LOG_OUT = "/api/log_out" -URL_API_TEMPLATE = "/api/template" +URL_ROOT: Final = "/" +URL_API: Final = "/api/" +URL_API_STREAM: Final = "/api/stream" +URL_API_CONFIG: Final = "/api/config" +URL_API_DISCOVERY_INFO: Final = "/api/discovery_info" +URL_API_STATES: Final = "/api/states" +URL_API_STATES_ENTITY: Final = "/api/states/{}" +URL_API_EVENTS: Final = "/api/events" +URL_API_EVENTS_EVENT: Final = "/api/events/{}" +URL_API_SERVICES: Final = "/api/services" +URL_API_SERVICES_SERVICE: Final = "/api/services/{}/{}" +URL_API_COMPONENTS: Final = "/api/components" +URL_API_ERROR_LOG: Final = "/api/error_log" +URL_API_LOG_OUT: Final = "/api/log_out" +URL_API_TEMPLATE: Final = "/api/template" -HTTP_OK = 200 -HTTP_CREATED = 201 -HTTP_ACCEPTED = 202 -HTTP_MOVED_PERMANENTLY = 301 -HTTP_BAD_REQUEST = 400 -HTTP_UNAUTHORIZED = 401 -HTTP_FORBIDDEN = 403 -HTTP_NOT_FOUND = 404 -HTTP_METHOD_NOT_ALLOWED = 405 -HTTP_UNPROCESSABLE_ENTITY = 422 -HTTP_TOO_MANY_REQUESTS = 429 -HTTP_INTERNAL_SERVER_ERROR = 500 -HTTP_BAD_GATEWAY = 502 -HTTP_SERVICE_UNAVAILABLE = 503 +HTTP_OK: Final = 200 +HTTP_CREATED: Final = 201 +HTTP_ACCEPTED: Final = 202 +HTTP_MOVED_PERMANENTLY: Final = 301 +HTTP_BAD_REQUEST: Final = 400 +HTTP_UNAUTHORIZED: Final = 401 +HTTP_FORBIDDEN: Final = 403 +HTTP_NOT_FOUND: Final = 404 +HTTP_METHOD_NOT_ALLOWED: Final = 405 +HTTP_UNPROCESSABLE_ENTITY: Final = 422 +HTTP_TOO_MANY_REQUESTS: Final = 429 +HTTP_INTERNAL_SERVER_ERROR: Final = 500 +HTTP_BAD_GATEWAY: Final = 502 +HTTP_SERVICE_UNAVAILABLE: Final = 503 -HTTP_BASIC_AUTHENTICATION = "basic" -HTTP_DIGEST_AUTHENTICATION = "digest" +HTTP_BASIC_AUTHENTICATION: Final = "basic" +HTTP_DIGEST_AUTHENTICATION: Final = "digest" -HTTP_HEADER_X_REQUESTED_WITH = "X-Requested-With" +HTTP_HEADER_X_REQUESTED_WITH: Final = "X-Requested-With" -CONTENT_TYPE_JSON = "application/json" -CONTENT_TYPE_MULTIPART = "multipart/x-mixed-replace; boundary={}" -CONTENT_TYPE_TEXT_PLAIN = "text/plain" +CONTENT_TYPE_JSON: Final = "application/json" +CONTENT_TYPE_MULTIPART: Final = "multipart/x-mixed-replace; boundary={}" +CONTENT_TYPE_TEXT_PLAIN: Final = "text/plain" # The exit code to send to request a restart -RESTART_EXIT_CODE = 100 +RESTART_EXIT_CODE: Final = 100 -UNIT_NOT_RECOGNIZED_TEMPLATE: str = "{} is not a recognized {} unit." +UNIT_NOT_RECOGNIZED_TEMPLATE: Final = "{} is not a recognized {} unit." -LENGTH: str = "length" -MASS: str = "mass" -PRESSURE: str = "pressure" -VOLUME: str = "volume" -TEMPERATURE: str = "temperature" -SPEED_MS: str = "speed_ms" -ILLUMINANCE: str = "illuminance" +LENGTH: Final = "length" +MASS: Final = "mass" +PRESSURE: Final = "pressure" +VOLUME: Final = "volume" +TEMPERATURE: Final = "temperature" +SPEED_MS: Final = "speed_ms" +ILLUMINANCE: Final = "illuminance" -WEEKDAYS = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] +WEEKDAYS: Final[list[str]] = ["mon", "tue", "wed", "thu", "fri", "sat", "sun"] # The degree of precision for platforms -PRECISION_WHOLE = 1 -PRECISION_HALVES = 0.5 -PRECISION_TENTHS = 0.1 +PRECISION_WHOLE: Final = 1 +PRECISION_HALVES: Final = 0.5 +PRECISION_TENTHS: Final = 0.1 # Static list of entities that will never be exposed to # cloud, alexa, or google_home components -CLOUD_NEVER_EXPOSED_ENTITIES = ["group.all_locks"] +CLOUD_NEVER_EXPOSED_ENTITIES: Final[list[str]] = ["group.all_locks"] # The ID of the Home Assistant Cast App -CAST_APP_ID_HOMEASSISTANT = "B12CE3CA" +CAST_APP_ID_HOMEASSISTANT: Final = "B12CE3CA" diff --git a/homeassistant/core.py b/homeassistant/core.py index c22526474a4..b9bf97e7e6c 100644 --- a/homeassistant/core.py +++ b/homeassistant/core.py @@ -47,7 +47,8 @@ from homeassistant.const import ( EVENT_TIMER_OUT_OF_SYNC, LENGTH_METERS, MATCH_ALL, - MAX_LENGTH_EVENT_TYPE, + MAX_LENGTH_EVENT_EVENT_TYPE, + MAX_LENGTH_STATE_STATE, __version__, ) from homeassistant.exceptions import ( @@ -130,7 +131,7 @@ def valid_entity_id(entity_id: str) -> bool: def valid_state(state: str) -> bool: """Test if a state is valid.""" - return len(state) < 256 + return len(state) <= MAX_LENGTH_STATE_STATE def callback(func: CALLABLE_T) -> CALLABLE_T: @@ -163,7 +164,7 @@ class HassJob: __slots__ = ("job_type", "target") - def __init__(self, target: Callable): + def __init__(self, target: Callable) -> None: """Create a job object.""" if asyncio.iscoroutine(target): raise ValueError("Coroutine not allowed to be passed to HassJob") @@ -373,8 +374,15 @@ class HomeAssistant: return task + def create_task(self, target: Awaitable) -> None: + """Add task to the executor pool. + + target: target to call. + """ + self.loop.call_soon_threadsafe(self.async_create_task, target) + @callback - def async_create_task(self, target: Coroutine) -> asyncio.tasks.Task: + def async_create_task(self, target: Awaitable) -> asyncio.tasks.Task: """Create a task from within the eventloop. This method must be run in the event loop. @@ -693,8 +701,10 @@ class EventBus: This method must be run in the event loop. """ - if len(event_type) > MAX_LENGTH_EVENT_TYPE: - raise MaxLengthExceeded(event_type, "event_type", MAX_LENGTH_EVENT_TYPE) + if len(event_type) > MAX_LENGTH_EVENT_EVENT_TYPE: + raise MaxLengthExceeded( + event_type, "event_type", MAX_LENGTH_EVENT_EVENT_TYPE + ) listeners = self._listeners.get(event_type, []) @@ -1531,7 +1541,7 @@ class Config: self.longitude: float = 0 self.elevation: int = 0 self.location_name: str = "Home" - self.time_zone: datetime.tzinfo = dt_util.UTC + self.time_zone: str = "UTC" self.units: UnitSystem = METRIC_SYSTEM self.internal_url: str | None = None self.external_url: str | None = None @@ -1621,17 +1631,13 @@ class Config: Async friendly. """ - time_zone = dt_util.UTC.zone - if self.time_zone and getattr(self.time_zone, "zone"): - time_zone = getattr(self.time_zone, "zone") - return { "latitude": self.latitude, "longitude": self.longitude, "elevation": self.elevation, "unit_system": self.units.as_dict(), "location_name": self.location_name, - "time_zone": time_zone, + "time_zone": self.time_zone, "components": self.components, "config_dir": self.config_dir, # legacy, backwards compat @@ -1651,7 +1657,7 @@ class Config: time_zone = dt_util.get_time_zone(time_zone_str) if time_zone: - self.time_zone = time_zone + self.time_zone = time_zone_str dt_util.set_default_time_zone(time_zone) else: raise ValueError(f"Received invalid time zone {time_zone_str}") @@ -1721,17 +1727,13 @@ class Config: async def async_store(self) -> None: """Store [homeassistant] core config.""" - time_zone = dt_util.UTC.zone - if self.time_zone and getattr(self.time_zone, "zone"): - time_zone = getattr(self.time_zone, "zone") - data = { "latitude": self.latitude, "longitude": self.longitude, "elevation": self.elevation, "unit_system": self.units.name, "location_name": self.location_name, - "time_zone": time_zone, + "time_zone": self.time_zone, "external_url": self.external_url, "internal_url": self.internal_url, } diff --git a/homeassistant/data_entry_flow.py b/homeassistant/data_entry_flow.py index f442e4662c6..786cfe7e286 100644 --- a/homeassistant/data_entry_flow.py +++ b/homeassistant/data_entry_flow.py @@ -44,7 +44,9 @@ class UnknownStep(FlowError): class AbortFlow(FlowError): """Exception to indicate a flow needs to be aborted.""" - def __init__(self, reason: str, description_placeholders: dict | None = None): + def __init__( + self, reason: str, description_placeholders: dict | None = None + ) -> None: """Initialize an abort flow exception.""" super().__init__(f"Flow aborted: {reason}") self.reason = reason @@ -73,6 +75,7 @@ class FlowResult(TypedDict, total=False): context: dict[str, Any] result: Any last_step: bool | None + options: Mapping[str, Any] class FlowManager(abc.ABC): @@ -307,7 +310,9 @@ class FlowHandler: # Set by flow manager cur_step: dict[str, str] | None = None - # Ignore types: https://github.com/PyCQA/pylint/issues/3167 + + # While not purely typed, it makes typehinting more useful for us + # and removes the need for constant None checks or asserts. flow_id: str = None # type: ignore hass: HomeAssistant = None # type: ignore handler: str = None # type: ignore diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 3b408860d59..79245491a7e 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -33,10 +33,12 @@ FLOWS = [ "blink", "bmw_connected_drive", "bond", + "bosch_shc", "braviatv", "broadlink", "brother", "bsblan", + "buienradar", "canary", "cast", "cert_expiry", @@ -79,6 +81,7 @@ FLOWS = [ "fritz", "fritzbox", "fritzbox_callmonitor", + "garages_amsterdam", "garmin_connect", "gdacs", "geofency", @@ -91,6 +94,7 @@ FLOWS = [ "google_travel_time", "gpslogger", "gree", + "growatt_server", "guardian", "habitica", "hangouts", @@ -128,6 +132,7 @@ FLOWS = [ "kodi", "konnected", "kostal_plenticore", + "kraken", "kulersky", "life360", "lifx", @@ -145,6 +150,7 @@ FLOWS = [ "met", "met_eireann", "meteo_france", + "meteoclimatic", "metoffice", "mikrotik", "mill", @@ -158,6 +164,7 @@ FLOWS = [ "mutesync", "myq", "mysensors", + "nam", "neato", "nest", "netatmo", @@ -213,6 +220,7 @@ FLOWS = [ "sharkiq", "shelly", "shopping_list", + "sia", "simplisafe", "sma", "smappee", @@ -237,8 +245,10 @@ FLOWS = [ "srp_energy", "starline", "subaru", + "syncthing", "syncthru", "synology_dsm", + "system_bridge", "tado", "tasmota", "tellduslive", @@ -266,6 +276,7 @@ FLOWS = [ "vilfo", "vizio", "volumio", + "wallbox", "waze_travel_time", "wemo", "wiffi", diff --git a/homeassistant/generated/dhcp.py b/homeassistant/generated/dhcp.py index 0fa1777d2a4..82b09e5f7ef 100644 --- a/homeassistant/generated/dhcp.py +++ b/homeassistant/generated/dhcp.py @@ -64,13 +64,35 @@ DHCP = [ }, { "domain": "flume", - "hostname": "flume-gw-*", - "macaddress": "ECFABC*" + "hostname": "flume-gw-*" }, { - "domain": "flume", - "hostname": "flume-gw-*", - "macaddress": "B4E62D*" + "domain": "goalzero", + "hostname": "yeti*" + }, + { + "domain": "gogogate2", + "hostname": "ismartgate*" + }, + { + "domain": "guardian", + "hostname": "gvc*", + "macaddress": "30AEA4*" + }, + { + "domain": "guardian", + "hostname": "guardian*", + "macaddress": "30AEA4*" + }, + { + "domain": "hunterdouglas_powerview", + "hostname": "hunter*", + "macaddress": "002674*" + }, + { + "domain": "isy994", + "hostname": "isy*", + "macaddress": "0021B9*" }, { "domain": "lyric", @@ -149,6 +171,10 @@ DHCP = [ "hostname": "roomba-*", "macaddress": "80A589*" }, + { + "domain": "samsungtv", + "hostname": "tizen*" + }, { "domain": "screenlogic", "hostname": "pentair: *", @@ -164,6 +190,31 @@ DHCP = [ "hostname": "sense-*", "macaddress": "DCEFCA*" }, + { + "domain": "smartthings", + "hostname": "st*", + "macaddress": "24FD5B*" + }, + { + "domain": "smartthings", + "hostname": "smartthings*", + "macaddress": "24FD5B*" + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "24FD5B*" + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "D052A8*" + }, + { + "domain": "smartthings", + "hostname": "hub*", + "macaddress": "286D97*" + }, { "domain": "solaredge", "hostname": "target", @@ -179,6 +230,10 @@ DHCP = [ "hostname": "squeezebox*", "macaddress": "000420*" }, + { + "domain": "tado", + "hostname": "tado*" + }, { "domain": "tesla", "hostname": "tesla_*", @@ -199,6 +254,81 @@ DHCP = [ "hostname": "eneco-*", "macaddress": "74C63B*" }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "1C3BF3*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "50C7BF*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "68FF7B*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "98DAC4*" + }, + { + "domain": "tplink", + "hostname": "hs*", + "macaddress": "B09575*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "1C3BF3*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "50C7BF*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "68FF7B*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "98DAC4*" + }, + { + "domain": "tplink", + "hostname": "k[lp]*", + "macaddress": "B09575*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "1C3BF3*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "50C7BF*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "68FF7B*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "98DAC4*" + }, + { + "domain": "tplink", + "hostname": "lb*", + "macaddress": "B09575*" + }, { "domain": "tuya", "macaddress": "508A06*" @@ -226,5 +356,9 @@ DHCP = [ { "domain": "verisure", "macaddress": "0023C1*" + }, + { + "domain": "yeelight", + "hostname": "yeelink-*" } ] diff --git a/homeassistant/generated/ssdp.py b/homeassistant/generated/ssdp.py index 4141de31f73..0f6c01a0605 100644 --- a/homeassistant/generated/ssdp.py +++ b/homeassistant/generated/ssdp.py @@ -136,6 +136,16 @@ SSDP = { "manufacturer": "Universal Devices Inc." } ], + "keenetic_ndms2": [ + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "Keenetic Ltd." + }, + { + "deviceType": "urn:schemas-upnp-org:device:InternetGatewayDevice:1", + "manufacturer": "ZyXEL Communications Corp." + } + ], "konnected": [ { "manufacturer": "konnected.io" diff --git a/homeassistant/generated/zeroconf.py b/homeassistant/generated/zeroconf.py index 4c017b07628..014edc4b1f3 100644 --- a/homeassistant/generated/zeroconf.py +++ b/homeassistant/generated/zeroconf.py @@ -11,6 +11,12 @@ ZEROCONF = { "domain": "volumio" } ], + "_airplay._tcp.local.": [ + { + "domain": "samsungtv", + "manufacturer": "samsung*" + } + ], "_api._udp.local.": [ { "domain": "guardian" @@ -94,6 +100,22 @@ ZEROCONF = { } ], "_http._tcp.local.": [ + { + "domain": "bosch_shc", + "name": "bosch shc*" + }, + { + "domain": "nam", + "name": "nam-*" + }, + { + "domain": "rachio", + "name": "rachio*" + }, + { + "domain": "rainmachine", + "name": "rainmachine*" + }, { "domain": "shelly", "name": "shelly*" @@ -143,6 +165,11 @@ ZEROCONF = { "domain": "plugwise" } ], + "_powerview._tcp.local.": [ + { + "domain": "hunterdouglas_powerview" + } + ], "_printer._tcp.local.": [ { "domain": "brother", @@ -168,6 +195,11 @@ ZEROCONF = { "name": "smappee50*" } ], + "_system-bridge._udp.local.": [ + { + "domain": "system_bridge" + } + ], "_touch-able._tcp.local.": [ { "domain": "apple_tv" @@ -208,11 +240,14 @@ HOMEKIT = { "PowerView": "hunterdouglas_powerview", "Presence": "netatmo", "Rachio": "rachio", + "SPK5": "rainmachine", "Smart Bridge": "lutron_caseta", "Socket": "wemo", "TRADFRI": "tradfri", + "Touch HD": "rainmachine", "Welcome": "netatmo", "Wemo": "wemo", + "YLDP*": "yeelight", "iSmartGate": "gogogate2", "iZone": "izone", "tado": "tado" diff --git a/homeassistant/helpers/collection.py b/homeassistant/helpers/collection.py index bfffb8523dd..6d9815e54d5 100644 --- a/homeassistant/helpers/collection.py +++ b/homeassistant/helpers/collection.py @@ -66,7 +66,7 @@ class CollectionError(HomeAssistantError): class ItemNotFound(CollectionError): """Raised when an item is not found.""" - def __init__(self, item_id: str): + def __init__(self, item_id: str) -> None: """Initialize item not found error.""" super().__init__(f"Item {item_id} not found.") self.item_id = item_id @@ -103,7 +103,9 @@ class IDManager: class ObservableCollection(ABC): """Base collection type that can be observed.""" - def __init__(self, logger: logging.Logger, id_manager: IDManager | None = None): + def __init__( + self, logger: logging.Logger, id_manager: IDManager | None = None + ) -> None: """Initialize the base collection.""" self.logger = logger self.id_manager = id_manager or IDManager() @@ -190,7 +192,7 @@ class StorageCollection(ObservableCollection): store: Store, logger: logging.Logger, id_manager: IDManager | None = None, - ): + ) -> None: """Initialize the storage collection.""" super().__init__(logger, id_manager) self.store = store @@ -389,7 +391,7 @@ class StorageCollectionWebsocket: model_name: str, create_schema: dict, update_schema: dict, - ): + ) -> None: """Initialize a websocket CRUD.""" self.storage_collection = storage_collection self.api_prefix = api_prefix diff --git a/homeassistant/helpers/condition.py b/homeassistant/helpers/condition.py index d1a7c95d16c..a467d952683 100644 --- a/homeassistant/helpers/condition.py +++ b/homeassistant/helpers/condition.py @@ -112,15 +112,22 @@ def condition_trace_update_result(**kwargs: Any) -> None: @contextmanager def trace_condition(variables: TemplateVarsType) -> Generator: """Trace condition evaluation.""" - trace_element = condition_trace_append(variables, trace_path_get()) - trace_stack_push(trace_stack_cv, trace_element) + should_pop = True + trace_element = trace_stack_top(trace_stack_cv) + if trace_element and trace_element.reuse_by_child: + should_pop = False + trace_element.reuse_by_child = False + else: + trace_element = condition_trace_append(variables, trace_path_get()) + trace_stack_push(trace_stack_cv, trace_element) try: yield trace_element except Exception as ex: trace_element.set_error(ex) raise ex finally: - trace_stack_pop(trace_stack_cv) + if should_pop: + trace_stack_pop(trace_stack_cv) def trace_condition_function(condition: ConditionCheckerType) -> ConditionCheckerType: @@ -745,19 +752,21 @@ def time( before_entity.attributes.get("hour", 23), before_entity.attributes.get("minute", 59), before_entity.attributes.get("second", 59), - 999999, ) if after < before: + condition_trace_update_result(after=after, now_time=now_time, before=before) if not after <= now_time < before: return False else: + condition_trace_update_result(after=after, now_time=now_time, before=before) if before <= now_time < after: return False if weekday is not None: now_weekday = WEEKDAYS[now.weekday()] + condition_trace_update_result(weekday=weekday, now_weekday=now_weekday) if ( isinstance(weekday, str) and weekday != now_weekday diff --git a/homeassistant/helpers/config_entry_flow.py b/homeassistant/helpers/config_entry_flow.py index 4020c9bb44c..05365b85645 100644 --- a/homeassistant/helpers/config_entry_flow.py +++ b/homeassistant/helpers/config_entry_flow.py @@ -1,14 +1,17 @@ """Helpers for data entry flows for config entries.""" from __future__ import annotations +import logging from typing import Any, Awaitable, Callable, Union from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult -from homeassistant.helpers.typing import DiscoveryInfoType +from homeassistant.helpers.typing import UNDEFINED, DiscoveryInfoType, UndefinedType -DiscoveryFunctionType = Callable[[], Union[Awaitable[bool], bool]] +DiscoveryFunctionType = Callable[[HomeAssistant], Union[Awaitable[bool], bool]] + +_LOGGER = logging.getLogger(__name__) class DiscoveryFlowHandler(config_entries.ConfigFlow): @@ -21,13 +24,11 @@ class DiscoveryFlowHandler(config_entries.ConfigFlow): domain: str, title: str, discovery_function: DiscoveryFunctionType, - connection_class: str, ) -> None: """Initialize the discovery config flow.""" self._domain = domain self._title = title self._discovery_function = discovery_function - self.CONNECTION_CLASS = connection_class # pylint: disable=invalid-name async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -104,15 +105,29 @@ def register_discovery_flow( domain: str, title: str, discovery_function: DiscoveryFunctionType, - connection_class: str, + connection_class: str | UndefinedType = UNDEFINED, ) -> None: """Register flow for discovered integrations that not require auth.""" + if connection_class is not UNDEFINED: + _LOGGER.warning( + ( + "The %s (%s) integration is setting a connection_class" + " when calling the 'register_discovery_flow()' method in its" + " config flow. The connection class has been deprecated and will" + " be removed in a future release of Home Assistant." + " If '%s' is a custom integration, please contact the author" + " of that integration about this warning.", + ), + title, + domain, + domain, + ) class DiscoveryFlow(DiscoveryFlowHandler): """Discovery flow handler.""" def __init__(self) -> None: - super().__init__(domain, title, discovery_function, connection_class) + super().__init__(domain, title, discovery_function) config_entries.HANDLERS.register(domain)(DiscoveryFlow) diff --git a/homeassistant/helpers/config_entry_oauth2_flow.py b/homeassistant/helpers/config_entry_oauth2_flow.py index ede345ce7de..8704932db73 100644 --- a/homeassistant/helpers/config_entry_oauth2_flow.py +++ b/homeassistant/helpers/config_entry_oauth2_flow.py @@ -107,7 +107,7 @@ class LocalOAuth2Implementation(AbstractOAuth2Implementation): client_secret: str, authorize_url: str, token_url: str, - ): + ) -> None: """Initialize local auth implementation.""" self.hass = hass self._domain = domain @@ -212,7 +212,6 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): DOMAIN = "" VERSION = 1 - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN def __init__(self) -> None: """Instantiate config flow.""" @@ -317,7 +316,11 @@ class AbstractOAuth2FlowHandler(config_entries.ConfigFlow, metaclass=ABCMeta): """ return self.async_create_entry(title=self.flow_impl.name, data=data) - async_step_user = async_step_pick_implementation + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow start.""" + return await self.async_step_pick_implementation(user_input) @classmethod def async_register_implementation( @@ -335,7 +338,7 @@ def async_register_implementation( if isinstance(implementation, LocalOAuth2Implementation) and not hass.data.get( DATA_VIEW_REGISTERED, False ): - hass.http.register_view(OAuth2AuthorizeCallbackView()) # type: ignore + hass.http.register_view(OAuth2AuthorizeCallbackView()) hass.data[DATA_VIEW_REGISTERED] = True implementations = hass.data.setdefault(DATA_IMPLEMENTATIONS, {}) @@ -434,7 +437,7 @@ class OAuth2Session: hass: HomeAssistant, config_entry: config_entries.ConfigEntry, implementation: AbstractOAuth2Implementation, - ): + ) -> None: """Initialize an OAuth2 session.""" self.hass = hass self.config_entry = config_entry diff --git a/homeassistant/helpers/config_validation.py b/homeassistant/helpers/config_validation.py index 8ad8d4a45a2..ed619cc9678 100644 --- a/homeassistant/helpers/config_validation.py +++ b/homeassistant/helpers/config_validation.py @@ -862,17 +862,19 @@ ENTITY_SERVICE_FIELDS = { def make_entity_service_schema( schema: dict, *, extra: int = vol.PREVENT_EXTRA -) -> vol.All: +) -> vol.Schema: """Create an entity service schema.""" - return vol.All( - vol.Schema( - { - **schema, - **ENTITY_SERVICE_FIELDS, - }, - extra=extra, - ), - has_at_least_one_key(*ENTITY_SERVICE_FIELDS), + return vol.Schema( + vol.All( + vol.Schema( + { + **schema, + **ENTITY_SERVICE_FIELDS, + }, + extra=extra, + ), + has_at_least_one_key(*ENTITY_SERVICE_FIELDS), + ) ) diff --git a/homeassistant/helpers/debounce.py b/homeassistant/helpers/debounce.py index 8e7e57fa142..75e0215d2cb 100644 --- a/homeassistant/helpers/debounce.py +++ b/homeassistant/helpers/debounce.py @@ -20,7 +20,7 @@ class Debouncer: cooldown: float, immediate: bool, function: Callable[..., Awaitable[Any]] | None = None, - ): + ) -> None: """Initialize debounce. immediate: indicate if the function needs to be called right away and diff --git a/homeassistant/helpers/deprecation.py b/homeassistant/helpers/deprecation.py index 06f09327dc9..adf3d8a5d88 100644 --- a/homeassistant/helpers/deprecation.py +++ b/homeassistant/helpers/deprecation.py @@ -80,6 +80,23 @@ def get_deprecated( return config.get(new_name, default) +def deprecated_class(replacement: str) -> Any: + """Mark class as deprecated and provide a replacement class to be used instead.""" + + def deprecated_decorator(cls: Any) -> Any: + """Decorate class as deprecated.""" + + @functools.wraps(cls) + def deprecated_cls(*args: tuple, **kwargs: dict[str, Any]) -> Any: + """Wrap for the original class.""" + _print_deprecation_warning(cls, replacement, "class") + return cls(*args, **kwargs) + + return deprecated_cls + + return deprecated_decorator + + def deprecated_function(replacement: str) -> Callable[..., Callable]: """Mark function as deprecated and provide a replacement function to be used instead.""" @@ -89,32 +106,39 @@ def deprecated_function(replacement: str) -> Callable[..., Callable]: @functools.wraps(func) def deprecated_func(*args: tuple, **kwargs: dict[str, Any]) -> Any: """Wrap for the original function.""" - logger = logging.getLogger(func.__module__) - try: - _, integration, path = get_integration_frame() - if path == "custom_components/": - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead, please report this to the maintainer of %s", - func.__name__, - integration, - replacement, - integration, - ) - else: - logger.warning( - "%s was called from %s, this is a deprecated function. Use %s instead", - func.__name__, - integration, - replacement, - ) - except MissingIntegrationFrame: - logger.warning( - "%s is a deprecated function. Use %s instead", - func.__name__, - replacement, - ) + _print_deprecation_warning(func, replacement, "function") return func(*args, **kwargs) return deprecated_func return deprecated_decorator + + +def _print_deprecation_warning(obj: Any, replacement: str, description: str) -> None: + logger = logging.getLogger(obj.__module__) + try: + _, integration, path = get_integration_frame() + if path == "custom_components/": + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead, please report this to the maintainer of %s", + obj.__name__, + integration, + description, + replacement, + integration, + ) + else: + logger.warning( + "%s was called from %s, this is a deprecated %s. Use %s instead", + obj.__name__, + integration, + description, + replacement, + ) + except MissingIntegrationFrame: + logger.warning( + "%s is a deprecated %s. Use %s instead", + obj.__name__, + description, + replacement, + ) diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 80c54ed296f..9f09bbbf642 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -4,7 +4,7 @@ from __future__ import annotations from collections import OrderedDict import logging import time -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, NamedTuple, cast import attr @@ -37,11 +37,6 @@ CONNECTION_NETWORK_MAC = "mac" CONNECTION_UPNP = "upnp" CONNECTION_ZIGBEE = "zigbee" -IDX_CONNECTIONS = "connections" -IDX_IDENTIFIERS = "identifiers" -REGISTERED_DEVICE = "registered" -DELETED_DEVICE = "deleted" - DISABLED_CONFIG_ENTRY = "config_entry" DISABLED_INTEGRATION = "integration" DISABLED_USER = "user" @@ -49,6 +44,11 @@ DISABLED_USER = "user" ORPHANED_DEVICE_KEEP_SECONDS = 86400 * 30 +class _DeviceIndex(NamedTuple): + identifiers: dict[tuple[str, str], str] + connections: dict[tuple[str, str], str] + + @attr.s(slots=True, frozen=True) class DeviceEntry: """Device Registry Entry.""" @@ -133,12 +133,30 @@ def format_mac(mac: str) -> str: return mac +def _async_get_device_id_from_index( + devices_index: _DeviceIndex, + identifiers: set[tuple[str, str]], + connections: set[tuple[str, str]] | None, +) -> str | None: + """Check if device has previously been registered.""" + for identifier in identifiers: + if identifier in devices_index.identifiers: + return devices_index.identifiers[identifier] + if not connections: + return None + for connection in _normalize_connections(connections): + if connection in devices_index.connections: + return devices_index.connections[connection] + return None + + class DeviceRegistry: """Class to hold a registry of devices.""" devices: dict[str, DeviceEntry] deleted_devices: dict[str, DeletedDeviceEntry] - _devices_index: dict[str, dict[str, dict[tuple[str, str], str]]] + _registered_index: _DeviceIndex + _deleted_index: _DeviceIndex def __init__(self, hass: HomeAssistant) -> None: """Initialize the device registry.""" @@ -158,8 +176,8 @@ class DeviceRegistry: connections: set[tuple[str, str]] | None = None, ) -> DeviceEntry | None: """Check if device is registered.""" - device_id = self._async_get_device_id_from_index( - REGISTERED_DEVICE, identifiers, connections + device_id = _async_get_device_id_from_index( + self._registered_index, identifiers, connections ) if device_id is None: return None @@ -171,38 +189,20 @@ class DeviceRegistry: connections: set[tuple[str, str]] | None, ) -> DeletedDeviceEntry | None: """Check if device is deleted.""" - device_id = self._async_get_device_id_from_index( - DELETED_DEVICE, identifiers, connections + device_id = _async_get_device_id_from_index( + self._deleted_index, identifiers, connections ) if device_id is None: return None return self.deleted_devices[device_id] - def _async_get_device_id_from_index( - self, - index: str, - identifiers: set[tuple[str, str]], - connections: set[tuple[str, str]] | None, - ) -> str | None: - """Check if device has previously been registered.""" - devices_index = self._devices_index[index] - for identifier in identifiers: - if identifier in devices_index[IDX_IDENTIFIERS]: - return devices_index[IDX_IDENTIFIERS][identifier] - if not connections: - return None - for connection in _normalize_connections(connections): - if connection in devices_index[IDX_CONNECTIONS]: - return devices_index[IDX_CONNECTIONS][connection] - return None - def _add_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: """Add a device and index it.""" if isinstance(device, DeletedDeviceEntry): - devices_index = self._devices_index[DELETED_DEVICE] + devices_index = self._deleted_index self.deleted_devices[device.id] = device else: - devices_index = self._devices_index[REGISTERED_DEVICE] + devices_index = self._registered_index self.devices[device.id] = device _add_device_to_index(devices_index, device) @@ -210,10 +210,10 @@ class DeviceRegistry: def _remove_device(self, device: DeviceEntry | DeletedDeviceEntry) -> None: """Remove a device and remove it from the index.""" if isinstance(device, DeletedDeviceEntry): - devices_index = self._devices_index[DELETED_DEVICE] + devices_index = self._deleted_index self.deleted_devices.pop(device.id) else: - devices_index = self._devices_index[REGISTERED_DEVICE] + devices_index = self._registered_index self.devices.pop(device.id) _remove_device_from_index(devices_index, device) @@ -222,24 +222,22 @@ class DeviceRegistry: """Update a device and the index.""" self.devices[new_device.id] = new_device - devices_index = self._devices_index[REGISTERED_DEVICE] + devices_index = self._registered_index _remove_device_from_index(devices_index, old_device) _add_device_to_index(devices_index, new_device) def _clear_index(self) -> None: """Clear the index.""" - self._devices_index = { - REGISTERED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, - DELETED_DEVICE: {IDX_IDENTIFIERS: {}, IDX_CONNECTIONS: {}}, - } + self._registered_index = _DeviceIndex(identifiers={}, connections={}) + self._deleted_index = _DeviceIndex(identifiers={}, connections={}) def _rebuild_index(self) -> None: """Create the index after loading devices.""" self._clear_index() for device in self.devices.values(): - _add_device_to_index(self._devices_index[REGISTERED_DEVICE], device) + _add_device_to_index(self._registered_index, device) for deleted_device in self.deleted_devices.values(): - _add_device_to_index(self._devices_index[DELETED_DEVICE], deleted_device) + _add_device_to_index(self._deleted_index, deleted_device) @callback def async_get_or_create( @@ -786,24 +784,24 @@ def _normalize_connections(connections: set[tuple[str, str]]) -> set[tuple[str, def _add_device_to_index( - devices_index: dict[str, dict[tuple[str, str], str]], + devices_index: _DeviceIndex, device: DeviceEntry | DeletedDeviceEntry, ) -> None: """Add a device to the index.""" for identifier in device.identifiers: - devices_index[IDX_IDENTIFIERS][identifier] = device.id + devices_index.identifiers[identifier] = device.id for connection in device.connections: - devices_index[IDX_CONNECTIONS][connection] = device.id + devices_index.connections[connection] = device.id def _remove_device_from_index( - devices_index: dict[str, dict[tuple[str, str], str]], + devices_index: _DeviceIndex, device: DeviceEntry | DeletedDeviceEntry, ) -> None: """Remove a device from the index.""" for identifier in device.identifiers: - if identifier in devices_index[IDX_IDENTIFIERS]: - del devices_index[IDX_IDENTIFIERS][identifier] + if identifier in devices_index.identifiers: + del devices_index.identifiers[identifier] for connection in device.connections: - if connection in devices_index[IDX_CONNECTIONS]: - del devices_index[IDX_CONNECTIONS][connection] + if connection in devices_index.connections: + del devices_index.connections[connection] diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index 1706d9b309d..724280b19c9 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -10,7 +10,7 @@ import logging import math import sys from timeit import default_timer as timer -from typing import Any +from typing import Any, TypedDict from homeassistant.config import DATA_CUSTOMIZE from homeassistant.const import ( @@ -110,6 +110,23 @@ def get_supported_features(hass: HomeAssistant, entity_id: str) -> int: return entry.supported_features or 0 +class DeviceInfo(TypedDict, total=False): + """Entity device information for device registry.""" + + name: str + connections: set[tuple[str, str]] + identifiers: set[tuple[str, str]] + manufacturer: str + model: str + suggested_area: str + sw_version: str + via_device: tuple[str, str] + entry_type: str | None + default_name: str + default_manufacturer: str + default_model: str + + class Entity(ABC): """An abstract class for Home Assistant entities.""" @@ -121,7 +138,6 @@ class Entity(ABC): # Owning hass instance. Will be set by EntityPlatform # While not purely typed, it makes typehinting more useful for us # and removes the need for constant None checks or asserts. - # Ignore types: https://github.com/PyCQA/pylint/issues/3167 hass: HomeAssistant = None # type: ignore # Owning platform instance. Will be set by EntityPlatform @@ -152,28 +168,46 @@ class Entity(ABC): # If entity is added to an entity platform _added = False + # Entity Properties + _attr_assumed_state: bool = False + _attr_available: bool = True + _attr_context_recent_time: timedelta = timedelta(seconds=5) + _attr_device_class: str | None = None + _attr_device_info: DeviceInfo | None = None + _attr_entity_picture: str | None = None + _attr_entity_registry_enabled_default: bool = True + _attr_extra_state_attributes: Mapping[str, Any] | None = None + _attr_force_update: bool = False + _attr_icon: str | None = None + _attr_name: str | None = None + _attr_should_poll: bool = True + _attr_state: StateType = STATE_UNKNOWN + _attr_supported_features: int | None = None + _attr_unique_id: str | None = None + _attr_unit_of_measurement: str | None = None + @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. False if entity pushes its state to HA. """ - return True + return self._attr_should_poll @property def unique_id(self) -> str | None: """Return a unique ID.""" - return None + return self._attr_unique_id @property def name(self) -> str | None: """Return the name of the entity.""" - return None + return self._attr_name @property def state(self) -> StateType: """Return the state of the entity.""" - return STATE_UNKNOWN + return self._attr_state @property def capability_attributes(self) -> Mapping[str, Any] | None: @@ -211,45 +245,45 @@ class Entity(ABC): Implemented by platform classes. Convention for attribute names is lowercase snake_case. """ - return None + return self._attr_extra_state_attributes @property - def device_info(self) -> Mapping[str, Any] | None: + def device_info(self) -> DeviceInfo | None: """Return device specific attributes. Implemented by platform classes. """ - return None + return self._attr_device_info @property def device_class(self) -> str | None: """Return the class of this device, from component DEVICE_CLASSES.""" - return None + return self._attr_device_class @property def unit_of_measurement(self) -> str | None: """Return the unit of measurement of this entity, if any.""" - return None + return self._attr_unit_of_measurement @property def icon(self) -> str | None: """Return the icon to use in the frontend, if any.""" - return None + return self._attr_icon @property def entity_picture(self) -> str | None: """Return the entity picture to use in the frontend, if any.""" - return None + return self._attr_entity_picture @property def available(self) -> bool: """Return True if entity is available.""" - return True + return self._attr_available @property def assumed_state(self) -> bool: """Return True if unable to access real state of the entity.""" - return False + return self._attr_assumed_state @property def force_update(self) -> bool: @@ -258,22 +292,22 @@ class Entity(ABC): If True, a state change will be triggered anytime the state property is updated, not just when the value changes. """ - return False + return self._attr_force_update @property def supported_features(self) -> int | None: """Flag supported features.""" - return None + return self._attr_supported_features @property def context_recent_time(self) -> timedelta: """Time that a context is considered recent.""" - return timedelta(seconds=5) + return self._attr_context_recent_time @property def entity_registry_enabled_default(self) -> bool: """Return if the entity should be enabled when first added to the entity registry.""" - return True + return self._attr_entity_registry_enabled_default # DO NOT OVERWRITE # These properties and methods are either managed by Home Assistant or they @@ -733,7 +767,7 @@ class ToggleEntity(Entity): """An abstract class for entities that can be turned on and off.""" @property - def state(self) -> str: + def state(self) -> str | None: """Return the state.""" return STATE_ON if self.is_on else STATE_OFF diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 7ac221ea06e..37c0a7620ab 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -76,7 +76,7 @@ class EntityComponent: domain: str, hass: HomeAssistant, scan_interval: timedelta = DEFAULT_SCAN_INTERVAL, - ): + ) -> None: """Initialize an entity component.""" self.logger = logger self.hass = hass @@ -246,9 +246,7 @@ class EntityComponent: platform_type, platform, scan_interval, entity_namespace ) - await self._platforms[key].async_setup( # type: ignore - platform_config, discovery_info - ) + await self._platforms[key].async_setup(platform_config, discovery_info) async def _async_reset(self) -> None: """Remove entities and reset the entity component to initial values. diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index e87960db779..b22fb9ec2d2 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -8,7 +8,10 @@ from datetime import datetime, timedelta import logging from logging import Logger from types import ModuleType -from typing import TYPE_CHECKING, Callable +from typing import TYPE_CHECKING, Any, Callable + +from typing_extensions import Protocol +import voluptuous as vol from homeassistant import config_entries from homeassistant.const import ( @@ -30,17 +33,19 @@ from homeassistant.exceptions import ( PlatformNotReady, RequiredParameterMissing, ) -from homeassistant.helpers import ( +from homeassistant.setup import async_start_setup +from homeassistant.util.async_ import run_callback_threadsafe + +from . import ( config_validation as cv, device_registry as dev_reg, entity_registry as ent_reg, service, ) -from homeassistant.setup import async_start_setup -from homeassistant.util.async_ import run_callback_threadsafe - -from .entity_registry import DISABLED_INTEGRATION +from .device_registry import DeviceRegistry +from .entity_registry import DISABLED_INTEGRATION, EntityRegistry from .event import async_call_later, async_track_time_interval +from .typing import ConfigType, DiscoveryInfoType if TYPE_CHECKING: from .entity import Entity @@ -58,6 +63,15 @@ PLATFORM_NOT_READY_BASE_WAIT_TIME = 30 # seconds _LOGGER = logging.getLogger(__name__) +class AddEntitiesCallback(Protocol): + """Protocol type for EntityPlatform.add_entities callback.""" + + def __call__( + self, new_entities: Iterable[Entity], update_before_add: bool = False + ) -> None: + """Define add_entities type.""" + + class EntityPlatform: """Manage the entities for a single platform.""" @@ -71,7 +85,7 @@ class EntityPlatform: platform: ModuleType | None, scan_interval: timedelta, entity_namespace: str | None, - ): + ) -> None: """Initialize the entity platform.""" self.hass = hass self.logger = logger @@ -136,7 +150,11 @@ class EntityPlatform: return self.parallel_updates - async def async_setup(self, platform_config, discovery_info=None): # type: ignore[no-untyped-def] + async def async_setup( + self, + platform_config: ConfigType, + discovery_info: DiscoveryInfoType | None = None, + ) -> None: """Set up the platform from a config file.""" platform = self.platform hass = self.hass @@ -194,9 +212,10 @@ class EntityPlatform: platform = self.platform @callback - def async_create_setup_task(): # type: ignore[no-untyped-def] + def async_create_setup_task() -> Coroutine: """Get task to set up platform.""" - return platform.async_setup_entry( # type: ignore + config_entries.current_entry.set(config_entry) + return platform.async_setup_entry( # type: ignore[no-any-return,union-attr] self.hass, config_entry, self._async_schedule_add_entities ) @@ -261,7 +280,7 @@ class EntityPlatform: wait_time, ) - async def setup_again(*_): # type: ignore[no-untyped-def] + async def setup_again(*_args: Any) -> None: """Run setup again.""" self._async_cancel_retry_setup = None await self._async_setup_platform(async_create_setup_task, tries) @@ -348,7 +367,7 @@ class EntityPlatform: device_registry = dev_reg.async_get(hass) entity_registry = ent_reg.async_get(hass) tasks = [ - self._async_add_entity( # type: ignore + self._async_add_entity( entity, update_before_add, entity_registry, device_registry ) for entity in new_entities @@ -377,8 +396,10 @@ class EntityPlatform: ) raise - if self._async_unsub_polling is not None or not any( - entity.should_poll for entity in self.entities.values() + if ( + (self.config_entry and self.config_entry.pref_disable_polling) + or self._async_unsub_polling is not None + or not any(entity.should_poll for entity in self.entities.values()) ): return @@ -388,9 +409,13 @@ class EntityPlatform: self.scan_interval, ) - async def _async_add_entity( # type: ignore[no-untyped-def] # noqa: C901 - self, entity, update_before_add, entity_registry, device_registry - ): + async def _async_add_entity( # noqa: C901 + self, + entity: Entity, + update_before_add: bool, + entity_registry: EntityRegistry, + device_registry: DeviceRegistry, + ) -> None: """Add an entity to the platform.""" if entity is None: raise ValueError("Entity cannot be None") @@ -412,6 +437,7 @@ class EntityPlatform: requested_entity_id = None suggested_object_id: str | None = None + generate_new_entity_id = False # Get entity_id from unique ID registration if entity.unique_id is not None: @@ -419,7 +445,7 @@ class EntityPlatform: requested_entity_id = entity.entity_id suggested_object_id = split_entity_id(entity.entity_id)[1] else: - suggested_object_id = entity.name + suggested_object_id = entity.name # type: ignore[unreachable] if self.entity_namespace is not None: suggested_object_id = f"{self.entity_namespace} {suggested_object_id}" @@ -449,10 +475,10 @@ class EntityPlatform: "suggested_area", ): if key in device_info: - processed_dev_info[key] = device_info[key] + processed_dev_info[key] = device_info[key] # type: ignore[misc] try: - device = device_registry.async_get_or_create(**processed_dev_info) + device = device_registry.async_get_or_create(**processed_dev_info) # type: ignore[arg-type] device_id = device.id except RequiredParameterMissing: pass @@ -498,10 +524,10 @@ class EntityPlatform: ): # If entity already registered, convert entity id to suggestion suggested_object_id = split_entity_id(entity.entity_id)[1] - entity.entity_id = None + generate_new_entity_id = True # Generate entity ID - if entity.entity_id is None: + if entity.entity_id is None or generate_new_entity_id: suggested_object_id = ( suggested_object_id or entity.name or DEVICE_DEFAULT_NAME ) @@ -553,7 +579,11 @@ class EntityPlatform: # has a chance to finish. self.hass.states.async_reserve(entity.entity_id) - entity.async_on_remove(lambda: self.entities.pop(entity_id)) + def remove_entity_cb() -> None: + """Remove entity from entities list.""" + self.entities.pop(entity_id) + + entity.async_on_remove(remove_entity_cb) await entity.add_to_platform_finish() @@ -614,7 +644,13 @@ class EntityPlatform: ) @callback - def async_register_entity_service(self, name, schema, func, required_features=None): # type: ignore[no-untyped-def] + def async_register_entity_service( + self, + name: str, + schema: dict | vol.Schema, + func: str | Callable[..., Any], + required_features: Iterable[int] | None = None, + ) -> None: """Register an entity service. Services will automatically be shared by all platforms of the same domain. @@ -678,6 +714,15 @@ current_platform: ContextVar[EntityPlatform | None] = ContextVar( ) +@callback +def async_get_current_platform() -> EntityPlatform: + """Get the current platform from context.""" + platform = current_platform.get() + if platform is None: + raise RuntimeError("Cannot get non-set current platform") + return platform + + @callback def async_get_platforms( hass: HomeAssistant, integration_name: str diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 936464dc423..fc9ef575c7d 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -10,7 +10,7 @@ timer. from __future__ import annotations from collections import OrderedDict -from collections.abc import Iterable +from collections.abc import Iterable, Mapping import logging from typing import TYPE_CHECKING, Any, Callable, cast @@ -24,6 +24,8 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, ATTR_UNIT_OF_MEASUREMENT, EVENT_HOMEASSISTANT_START, + MAX_LENGTH_STATE_DOMAIN, + MAX_LENGTH_STATE_ENTITY_ID, STATE_UNAVAILABLE, ) from homeassistant.core import ( @@ -33,6 +35,7 @@ from homeassistant.core import ( split_entity_id, valid_entity_id, ) +from homeassistant.exceptions import MaxLengthExceeded from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import EVENT_DEVICE_REGISTRY_UPDATED from homeassistant.loader import bind_hass @@ -96,7 +99,7 @@ class RegistryEntry: ) ), ) - capabilities: dict[str, Any] | None = attr.ib(default=None) + capabilities: Mapping[str, Any] | None = attr.ib(default=None) supported_features: int = attr.ib(default=0) device_class: str | None = attr.ib(default=None) unit_of_measurement: str | None = attr.ib(default=None) @@ -146,7 +149,7 @@ class RegistryEntry: class EntityRegistry: """Class to hold a registry of entities.""" - def __init__(self, hass: HomeAssistant): + def __init__(self, hass: HomeAssistant) -> None: """Initialize the registry.""" self.hass = hass self.entities: dict[str, RegistryEntry] @@ -201,6 +204,10 @@ class EntityRegistry: Conflicts checked against registered and currently existing entities. """ preferred_string = f"{domain}.{slugify(suggested_object_id)}" + + if len(domain) > MAX_LENGTH_STATE_DOMAIN: + raise MaxLengthExceeded(domain, "domain", MAX_LENGTH_STATE_DOMAIN) + test_string = preferred_string if not known_object_ids: known_object_ids = {} @@ -214,6 +221,11 @@ class EntityRegistry: tries += 1 test_string = f"{preferred_string}_{tries}" + if len(test_string) > MAX_LENGTH_STATE_ENTITY_ID: + raise MaxLengthExceeded( + test_string, "generated_entity_id", MAX_LENGTH_STATE_ENTITY_ID + ) + return test_string @callback @@ -232,7 +244,7 @@ class EntityRegistry: config_entry: ConfigEntry | None = None, device_id: str | None = None, area_id: str | None = None, - capabilities: dict[str, Any] | None = None, + capabilities: Mapping[str, Any] | None = None, supported_features: int | None = None, device_class: str | None = None, unit_of_measurement: str | None = None, @@ -274,7 +286,7 @@ class EntityRegistry: if ( disabled_by is None and config_entry - and config_entry.system_options.disable_new_entities + and config_entry.pref_disable_new_entities ): disabled_by = DISABLED_INTEGRATION @@ -392,7 +404,7 @@ class EntityRegistry: area_id: str | None | UndefinedType = UNDEFINED, new_unique_id: str | UndefinedType = UNDEFINED, disabled_by: str | None | UndefinedType = UNDEFINED, - capabilities: dict[str, Any] | None | UndefinedType = UNDEFINED, + capabilities: Mapping[str, Any] | None | UndefinedType = UNDEFINED, supported_features: int | UndefinedType = UNDEFINED, device_class: str | None | UndefinedType = UNDEFINED, unit_of_measurement: str | None | UndefinedType = UNDEFINED, diff --git a/homeassistant/helpers/event.py b/homeassistant/helpers/event.py index b8a8db8f03d..48dd05d2311 100644 --- a/homeassistant/helpers/event.py +++ b/homeassistant/helpers/event.py @@ -139,7 +139,7 @@ def threaded_listener_factory( def async_track_state_change( hass: HomeAssistant, entity_ids: str | Iterable[str], - action: Callable[[str, State, State], None], + action: Callable[[str, State, State], Awaitable[None] | None], from_state: None | str | Iterable[str] = None, to_state: None | str | Iterable[str] = None, ) -> CALLBACK_TYPE: @@ -534,7 +534,7 @@ class _TrackStateChangeFiltered: hass: HomeAssistant, track_states: TrackStates, action: Callable[[Event], Any], - ): + ) -> None: """Handle removal / refresh of tracker init.""" self.hass = hass self._action = action @@ -683,7 +683,7 @@ def async_track_state_change_filtered( def async_track_template( hass: HomeAssistant, template: Template, - action: Callable[[str, State | None, State | None], None], + action: Callable[[str, State | None, State | None], Awaitable[None] | None], variables: TemplateVarsType | None = None, ) -> Callable[[], None]: """Add a listener that fires when a a template evaluates to 'true'. @@ -775,7 +775,7 @@ class _TrackTemplateResultInfo: hass: HomeAssistant, track_templates: Iterable[TrackTemplate], action: Callable, - ): + ) -> None: """Handle removal / refresh of tracker init.""" self.hass = hass self._job = HassJob(action) @@ -1072,7 +1072,7 @@ def async_track_template_result( def async_track_same_state( hass: HomeAssistant, period: timedelta, - action: Callable[..., None], + action: Callable[..., Awaitable[None] | None], async_check_same_func: Callable[[str, State | None, State | None], bool], entity_ids: str | Iterable[str] = MATCH_ALL, ) -> CALLBACK_TYPE: @@ -1141,7 +1141,7 @@ track_same_state = threaded_listener_factory(async_track_same_state) @bind_hass def async_track_point_in_time( hass: HomeAssistant, - action: HassJob | Callable[..., None], + action: HassJob | Callable[..., Awaitable[None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in time.""" @@ -1162,7 +1162,7 @@ track_point_in_time = threaded_listener_factory(async_track_point_in_time) @bind_hass def async_track_point_in_utc_time( hass: HomeAssistant, - action: HassJob | Callable[..., None], + action: HassJob | Callable[..., Awaitable[None] | None], point_in_time: datetime, ) -> CALLBACK_TYPE: """Add a listener that fires once after a specific point in UTC time.""" @@ -1213,7 +1213,9 @@ track_point_in_utc_time = threaded_listener_factory(async_track_point_in_utc_tim @callback @bind_hass def async_call_later( - hass: HomeAssistant, delay: float, action: HassJob | Callable[..., None] + hass: HomeAssistant, + delay: float, + action: HassJob | Callable[..., Awaitable[None] | None], ) -> CALLBACK_TYPE: """Add a listener that is called in .""" return async_track_point_in_utc_time( @@ -1228,7 +1230,7 @@ call_later = threaded_listener_factory(async_call_later) @bind_hass def async_track_time_interval( hass: HomeAssistant, - action: Callable[..., None | Awaitable], + action: Callable[..., Awaitable[None] | None], interval: timedelta, ) -> CALLBACK_TYPE: """Add a listener that fires repetitively at every timedelta interval.""" @@ -1360,7 +1362,7 @@ time_tracker_utcnow = dt_util.utcnow @bind_hass def async_track_utc_time_change( hass: HomeAssistant, - action: Callable[..., None], + action: Callable[..., Awaitable[None] | None], hour: Any | None = None, minute: Any | None = None, second: Any | None = None, diff --git a/homeassistant/helpers/ratelimit.py b/homeassistant/helpers/ratelimit.py index c34cfb72b36..389b3f4d2d5 100644 --- a/homeassistant/helpers/ratelimit.py +++ b/homeassistant/helpers/ratelimit.py @@ -19,7 +19,7 @@ class KeyedRateLimit: def __init__( self, hass: HomeAssistant, - ): + ) -> None: """Initialize ratelimit tracker.""" self.hass = hass self._last_triggered: dict[Hashable, datetime] = {} diff --git a/homeassistant/helpers/reload.py b/homeassistant/helpers/reload.py index 01350b579c4..da6c6935b35 100644 --- a/homeassistant/helpers/reload.py +++ b/homeassistant/helpers/reload.py @@ -124,7 +124,7 @@ async def _async_reconfig_platform( ) -> None: """Reconfigure an already loaded platform.""" await platform.async_reset() - tasks = [platform.async_setup(p_config) for p_config in platform_configs] # type: ignore + tasks = [platform.async_setup(p_config) for p_config in platform_configs] await asyncio.gather(*tasks) diff --git a/homeassistant/helpers/script.py b/homeassistant/helpers/script.py index 1deb5a5073f..ea3635888bb 100644 --- a/homeassistant/helpers/script.py +++ b/homeassistant/helpers/script.py @@ -87,6 +87,8 @@ from .trace import ( trace_stack_cv, trace_stack_pop, trace_stack_push, + trace_stack_top, + trace_update_result, ) # mypy: allow-untyped-calls, allow-untyped-defs, no-check-untyped-defs @@ -619,14 +621,16 @@ class _ScriptRun: ) cond = await self._async_get_condition(self._action) try: - with trace_path("condition"): - check = cond(self._hass, self._variables) + trace_element = trace_stack_top(trace_stack_cv) + if trace_element: + trace_element.reuse_by_child = True + check = cond(self._hass, self._variables) except exceptions.ConditionError as ex: _LOGGER.warning("Error in 'condition' evaluation:\n%s", ex) check = False self._log("Test condition %s: %s", self._script.last_action, check) - trace_set_result(result=check) + trace_update_result(result=check) if not check: raise _StopScript diff --git a/homeassistant/helpers/script_variables.py b/homeassistant/helpers/script_variables.py index a72d0b5543f..23241f22d1e 100644 --- a/homeassistant/helpers/script_variables.py +++ b/homeassistant/helpers/script_variables.py @@ -12,7 +12,7 @@ from . import template class ScriptVariables: """Class to hold and render script variables.""" - def __init__(self, variables: dict[str, Any]): + def __init__(self, variables: dict[str, Any]) -> None: """Initialize script variables.""" self.variables = variables self._has_template: bool | None = None diff --git a/homeassistant/helpers/service.py b/homeassistant/helpers/service.py index ed23926b0a3..ff037998f34 100644 --- a/homeassistant/helpers/service.py +++ b/homeassistant/helpers/service.py @@ -73,7 +73,7 @@ class ServiceParams(TypedDict): class ServiceTargetSelector: """Class to hold a target selector for a service.""" - def __init__(self, service_call: ServiceCall): + def __init__(self, service_call: ServiceCall) -> None: """Extract ids from service call data.""" entity_ids: str | list | None = service_call.data.get(ATTR_ENTITY_ID) device_ids: str | list | None = service_call.data.get(ATTR_DEVICE_ID) @@ -783,3 +783,43 @@ def verify_domain_control( return check_permissions return decorator + + +class ReloadServiceHelper: + """Helper for reload services to minimize unnecessary reloads.""" + + def __init__(self, service_func: Callable[[ServiceCall], Awaitable]) -> None: + """Initialize ReloadServiceHelper.""" + self._service_func = service_func + self._service_running = False + self._service_condition = asyncio.Condition() + + async def execute_service(self, service_call: ServiceCall) -> None: + """Execute the service. + + If a previous reload task if currently in progress, wait for it to finish first. + Once the previous reload task has finished, one of the waiting tasks will be + assigned to execute the reload, the others will wait for the reload to finish. + """ + + do_reload = False + async with self._service_condition: + if self._service_running: + # A previous reload task is already in progress, wait for it to finish + await self._service_condition.wait() + + async with self._service_condition: + if not self._service_running: + # This task will do the reload + self._service_running = True + do_reload = True + else: + # Another task will perform the reload, wait for it to finish + await self._service_condition.wait() + + if do_reload: + # Reload, then notify other tasks + await self._service_func(service_call) + async with self._service_condition: + self._service_running = False + self._service_condition.notify_all() diff --git a/homeassistant/helpers/start.py b/homeassistant/helpers/start.py new file mode 100644 index 00000000000..e7e827ec5c3 --- /dev/null +++ b/homeassistant/helpers/start.py @@ -0,0 +1,25 @@ +"""Helpers to help during startup.""" +from collections.abc import Awaitable +from typing import Callable + +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import Event, HomeAssistant, callback + + +@callback +def async_at_start( + hass: HomeAssistant, at_start_cb: Callable[[HomeAssistant], Awaitable] +) -> None: + """Execute something when Home Assistant is started. + + Will execute it now if Home Assistant is already started. + """ + if hass.is_running: + hass.async_create_task(at_start_cb(hass)) + return + + async def _matched_event(event: Event) -> None: + """Call the callback when Home Assistant started.""" + await at_start_cb(hass) + + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, _matched_event) diff --git a/homeassistant/helpers/storage.py b/homeassistant/helpers/storage.py index 456e9b04709..5700a7f854b 100644 --- a/homeassistant/helpers/storage.py +++ b/homeassistant/helpers/storage.py @@ -75,7 +75,7 @@ class Store: private: bool = False, *, encoder: type[JSONEncoder] | None = None, - ): + ) -> None: """Initialize storage class.""" self.version = version self.key = key diff --git a/homeassistant/helpers/template.py b/homeassistant/helpers/template.py index 6ac220788e0..f65100a8775 100644 --- a/homeassistant/helpers/template.py +++ b/homeassistant/helpers/template.py @@ -22,9 +22,9 @@ from urllib.parse import urlencode as urllib_urlencode import weakref import jinja2 -from jinja2 import contextfilter, contextfunction +from jinja2 import contextfunction, pass_context from jinja2.sandbox import ImmutableSandboxedEnvironment -from jinja2.utils import Namespace # type: ignore +from jinja2.utils import Namespace import voluptuous as vol from homeassistant.const import ( @@ -175,7 +175,7 @@ class TupleWrapper(tuple, ResultWrapper): # pylint: disable=super-init-not-called - def __init__(self, value: tuple, *, render_result: str | None = None): + def __init__(self, value: tuple, *, render_result: str | None = None) -> None: """Initialize a new tuple class.""" self.render_result = render_result @@ -581,9 +581,8 @@ class Template: self._strict = strict env = self._env - self._compiled = cast( - jinja2.Template, - jinja2.Template.from_code(env, self._compiled_code, env.globals, None), + self._compiled = jinja2.Template.from_code( + env, self._compiled_code, env.globals, None ) return self._compiled @@ -1316,7 +1315,7 @@ def to_json(value): return json.dumps(value) -@contextfilter +@pass_context def random_every_time(context, values): """Choose a random value. @@ -1483,7 +1482,7 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return contextfunction(wrapper) self.globals["device_entities"] = hassfunction(device_entities) - self.filters["device_entities"] = contextfilter(self.globals["device_entities"]) + self.filters["device_entities"] = pass_context(self.globals["device_entities"]) if limited: # Only device_entities is available to limited templates, mark other @@ -1515,9 +1514,9 @@ class TemplateEnvironment(ImmutableSandboxedEnvironment): return self.globals["expand"] = hassfunction(expand) - self.filters["expand"] = contextfilter(self.globals["expand"]) + self.filters["expand"] = pass_context(self.globals["expand"]) self.globals["closest"] = hassfunction(closest) - self.filters["closest"] = contextfilter(hassfunction(closest_filter)) + self.filters["closest"] = pass_context(hassfunction(closest_filter)) self.globals["distance"] = hassfunction(distance) self.globals["is_state"] = hassfunction(is_state) self.globals["is_state_attr"] = hassfunction(is_state_attr) diff --git a/homeassistant/helpers/trace.py b/homeassistant/helpers/trace.py index e2d5144f374..33fe76c9eab 100644 --- a/homeassistant/helpers/trace.py +++ b/homeassistant/helpers/trace.py @@ -15,13 +15,14 @@ import homeassistant.util.dt as dt_util class TraceElement: """Container for trace data.""" - def __init__(self, variables: TemplateVarsType, path: str): + def __init__(self, variables: TemplateVarsType, path: str) -> None: """Container for trace data.""" self._child_key: tuple[str, str] | None = None self._child_run_id: str | None = None self._error: Exception | None = None self.path: str = path self._result: dict | None = None + self.reuse_by_child = False self._timestamp = dt_util.utcnow() if variables is None: @@ -198,6 +199,12 @@ def trace_set_result(**kwargs: Any) -> None: node.set_result(**kwargs) +def trace_update_result(**kwargs: Any) -> None: + """Update the result of TraceElement at the top of the stack.""" + node = cast(TraceElement, trace_stack_top(trace_stack_cv)) + node.update_result(**kwargs) + + class StopReason: """Mutable container class for script_execution.""" diff --git a/homeassistant/helpers/typing.py b/homeassistant/helpers/typing.py index 58f999c5adc..7d01b0b6a77 100644 --- a/homeassistant/helpers/typing.py +++ b/homeassistant/helpers/typing.py @@ -9,17 +9,10 @@ ConfigType = Dict[str, Any] ContextType = homeassistant.core.Context DiscoveryInfoType = Dict[str, Any] EventType = homeassistant.core.Event -ServiceCallType = homeassistant.core.ServiceCall ServiceDataType = Dict[str, Any] StateType = Union[None, str, int, float] TemplateVarsType = Optional[Mapping[str, Any]] -# HomeAssistantType is not to be used, -# It is not present in the core code base. -# It is kept in order not to break custom components -# In due time it will be removed. -HomeAssistantType = homeassistant.core.HomeAssistant - # Custom type for recorder Queries QueryType = Any @@ -31,3 +24,11 @@ class UndefinedType(Enum): UNDEFINED = UndefinedType._singleton # pylint: disable=protected-access + +# The following types should not used and +# are not present in the core code base. +# They are kept in order not to break custom integrations +# that may rely on them. +# In due time they will be removed. +HomeAssistantType = homeassistant.core.HomeAssistant +ServiceCallType = homeassistant.core.ServiceCall diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index f9b97698220..e83a2d0edc3 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -6,7 +6,7 @@ from collections.abc import Awaitable from datetime import datetime, timedelta import logging from time import monotonic -from typing import Any, Callable, Generic, TypeVar +from typing import Callable, Generic, TypeVar import urllib.error import aiohttp @@ -25,8 +25,6 @@ REQUEST_REFRESH_DEFAULT_IMMEDIATE = True T = TypeVar("T") -# mypy: disallow-any-generics - class UpdateFailed(Exception): """Raised when an update has failed.""" @@ -44,15 +42,21 @@ class DataUpdateCoordinator(Generic[T]): update_interval: timedelta | None = None, update_method: Callable[[], Awaitable[T]] | None = None, request_refresh_debouncer: Debouncer | None = None, - ): + ) -> None: """Initialize global data updater.""" self.hass = hass self.logger = logger self.name = name self.update_method = update_method self.update_interval = update_interval + self.config_entry = config_entries.current_entry.get() - self.data: T | None = None + # It's None before the first successful update. + # Components should call async_config_entry_first_refresh + # to make sure the first update was successful. + # Set type to just T to remove annoying checks that data is not None + # when it was already checked during setup. + self.data: T = None # type: ignore[assignment] self._listeners: list[CALLBACK_TYPE] = [] self._job = HassJob(self._handle_refresh_interval) @@ -107,6 +111,9 @@ class DataUpdateCoordinator(Generic[T]): if self.update_interval is None: return + if self.config_entry and self.config_entry.pref_disable_polling: + return + if self._unsub_refresh: self._unsub_refresh() self._unsub_refresh = None @@ -133,7 +140,7 @@ class DataUpdateCoordinator(Generic[T]): """ await self._debounced_refresh.async_call() - async def _async_update_data(self) -> T | None: + async def _async_update_data(self) -> T: """Fetch the latest data from the source.""" if self.update_method is None: raise NotImplementedError("Update method not implemented") @@ -226,9 +233,8 @@ class DataUpdateCoordinator(Generic[T]): if raise_on_auth_failed: raise - config_entry = config_entries.current_entry.get() - if config_entry: - config_entry.async_start_reauth(self.hass) + if self.config_entry: + self.config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err @@ -289,10 +295,10 @@ class DataUpdateCoordinator(Generic[T]): self._unsub_refresh = None -class CoordinatorEntity(entity.Entity): +class CoordinatorEntity(Generic[T], entity.Entity): """A class for entities using DataUpdateCoordinator.""" - def __init__(self, coordinator: DataUpdateCoordinator[Any]) -> None: + def __init__(self, coordinator: DataUpdateCoordinator[T]) -> None: """Create the entity with a DataUpdateCoordinator.""" self.coordinator = coordinator diff --git a/homeassistant/loader.py b/homeassistant/loader.py index cdf9a831450..06bf5045c9f 100644 --- a/homeassistant/loader.py +++ b/homeassistant/loader.py @@ -17,7 +17,11 @@ import sys from types import ModuleType from typing import TYPE_CHECKING, Any, Callable, Dict, TypedDict, TypeVar, cast -from awesomeversion import AwesomeVersion, AwesomeVersionStrategy +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) from homeassistant.generated.dhcp import DHCP from homeassistant.generated.mqtt import MQTT @@ -43,22 +47,12 @@ DATA_CUSTOM_COMPONENTS = "custom_components" PACKAGE_CUSTOM_COMPONENTS = "custom_components" PACKAGE_BUILTIN = "homeassistant.components" CUSTOM_WARNING = ( - "You are using a custom integration %s which has not " + "We found a custom integration %s which has not " "been tested by Home Assistant. This component might " "cause stability problems, be sure to disable it if you " "experience issues with Home Assistant" ) -CUSTOM_WARNING_VERSION_MISSING = ( - "No 'version' key in the manifest file for " - "custom integration '%s'. As of Home Assistant " - "2021.6, this integration will no longer be " - "loaded. Please report this to the maintainer of '%s'" -) -CUSTOM_WARNING_VERSION_TYPE = ( - "'%s' is not a valid version for " - "custom integration '%s'. " - "Please report this to the maintainer of '%s'" -) + _UNDEF = object() # Internal; not helpers.typing.UNDEFINED due to circular dependency MAX_LOAD_CONCURRENTLY = 4 @@ -296,37 +290,48 @@ class Integration: ) continue - return cls( - hass, f"{root_module.__name__}.{domain}", manifest_path.parent, manifest + integration = cls( + hass, + f"{root_module.__name__}.{domain}", + manifest_path.parent, + manifest, ) + if integration.is_built_in: + return integration + + _LOGGER.warning(CUSTOM_WARNING, integration.domain) + try: + AwesomeVersion( + integration.version, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], + ) + except AwesomeVersionException: + _LOGGER.error( + "The custom integration '%s' does not have a " + "valid version key (%s) in the manifest file and was blocked from loading. " + "See https://developers.home-assistant.io/blog/2021/01/29/custom-integration-changes#versions for more details", + integration.domain, + integration.version, + ) + return None + return integration + return None - @classmethod - def resolve_legacy(cls, hass: HomeAssistant, domain: str) -> Integration | None: - """Resolve legacy component. - - Will create a stub manifest. - """ - comp = _load_file(hass, domain, _lookup_path(hass)) - - if comp is None: - return None - - return cls( - hass, - comp.__name__, - pathlib.Path(comp.__file__).parent, - manifest_from_legacy_module(domain, comp), - ) - def __init__( self, hass: HomeAssistant, pkg_path: str, file_path: pathlib.Path, manifest: Manifest, - ): + ) -> None: """Initialize an integration.""" self.hass = hass self.pkg_path = pkg_path @@ -527,40 +532,33 @@ async def async_get_integration(hass: HomeAssistant, domain: str) -> Integration event = cache[domain] = asyncio.Event() + try: + integration = await _async_get_integration(hass, domain) + except Exception: # pylint: disable=broad-except + # Remove event from cache. + cache.pop(domain) + event.set() + raise + + cache[domain] = integration + event.set() + return integration + + +async def _async_get_integration(hass: HomeAssistant, domain: str) -> Integration: # Instead of using resolve_from_root we use the cache of custom # components to find the integration. - integration = (await async_get_custom_components(hass)).get(domain) - if integration is not None: - custom_integration_warning(integration) - cache[domain] = integration - event.set() + if integration := (await async_get_custom_components(hass)).get(domain): return integration from homeassistant import components # pylint: disable=import-outside-toplevel - integration = await hass.async_add_executor_job( + if integration := await hass.async_add_executor_job( Integration.resolve_from_root, hass, components, domain - ) - - if integration is not None: - cache[domain] = integration - event.set() + ): return integration - integration = Integration.resolve_legacy(hass, domain) - if integration is not None: - custom_integration_warning(integration) - cache[domain] = integration - else: - # Remove event from cache. - cache.pop(domain) - - event.set() - - if not integration: - raise IntegrationNotFound(domain) - - return integration + raise IntegrationNotFound(domain) class LoaderError(Exception): @@ -770,35 +768,3 @@ def _lookup_path(hass: HomeAssistant) -> list[str]: if hass.config.safe_mode: return [PACKAGE_BUILTIN] return [PACKAGE_CUSTOM_COMPONENTS, PACKAGE_BUILTIN] - - -def validate_custom_integration_version(version: str) -> bool: - """Validate the version of custom integrations.""" - return AwesomeVersion(version).strategy in ( - AwesomeVersionStrategy.CALVER, - AwesomeVersionStrategy.SEMVER, - AwesomeVersionStrategy.SIMPLEVER, - AwesomeVersionStrategy.BUILDVER, - AwesomeVersionStrategy.PEP440, - ) - - -def custom_integration_warning(integration: Integration) -> None: - """Create logs for custom integrations.""" - if not integration.pkg_path.startswith(PACKAGE_CUSTOM_COMPONENTS): - return None - - _LOGGER.warning(CUSTOM_WARNING, integration.domain) - - if integration.manifest.get("version") is None: - _LOGGER.error( - CUSTOM_WARNING_VERSION_MISSING, integration.domain, integration.domain - ) - else: - if not validate_custom_integration_version(integration.manifest["version"]): - _LOGGER.error( - CUSTOM_WARNING_VERSION_TYPE, - integration.manifest["version"], - integration.domain, - integration.domain, - ) diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index 5e4d3e2c2ff..6dc7bedaab8 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -1,31 +1,31 @@ PyJWT==1.7.1 PyNaCl==1.3.0 -aiodiscover==1.4.0 +aiodiscover==1.4.2 aiohttp==3.7.4.post0 aiohttp_cors==0.7.0 astral==2.2 -async-upnp-client==0.16.2 +async-upnp-client==0.18.0 async_timeout==3.0.1 -attrs==20.3.0 -awesomeversion==21.2.3 +attrs==21.2.0 +awesomeversion==21.4.0 +backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 cryptography==3.3.2 -defusedxml==0.6.0 +defusedxml==0.7.1 distro==1.5.0 emoji==1.2.0 hass-nabucasa==0.43.0 -home-assistant-frontend==20210504.0 +home-assistant-frontend==20210601.1 httpx==0.18.0 -jinja2>=2.11.3 -netdisco==2.8.2 +ifaddr==0.1.7 +jinja2>=3.0.1 +netdisco==2.8.3 paho-mqtt==1.5.1 pillow==8.1.2 pip>=8.0.3,<20.3 -pyroute2==0.5.18 python-slugify==4.0.1 -pytz>=2021.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 @@ -34,7 +34,7 @@ sqlalchemy==1.4.13 voluptuous-serialize==2.4.0 voluptuous==0.12.1 yarl==1.6.3 -zeroconf==0.29.0 +zeroconf==0.31.0 pycryptodome>=3.6.6 diff --git a/homeassistant/util/dt.py b/homeassistant/util/dt.py index b0c6cb21fec..a9a6ca4e3a3 100644 --- a/homeassistant/util/dt.py +++ b/homeassistant/util/dt.py @@ -1,23 +1,25 @@ """Helper methods to handle the time in Home Assistant.""" from __future__ import annotations +import bisect from contextlib import suppress import datetime as dt import re +import sys from typing import Any, cast import ciso8601 -import pytz -import pytz.exceptions as pytzexceptions -import pytz.tzinfo as pytzinfo from homeassistant.const import MATCH_ALL -DATE_STR_FORMAT = "%Y-%m-%d" -NATIVE_UTC = dt.timezone.utc -UTC = pytz.utc -DEFAULT_TIME_ZONE: dt.tzinfo = pytz.utc +if sys.version_info[:2] >= (3, 9): + import zoneinfo # pylint: disable=import-error +else: + from backports import zoneinfo # pylint: disable=import-error +DATE_STR_FORMAT = "%Y-%m-%d" +UTC = dt.timezone.utc +DEFAULT_TIME_ZONE: dt.tzinfo = dt.timezone.utc # Copyright (c) Django Software Foundation and individual contributors. # All rights reserved. @@ -37,7 +39,6 @@ def set_default_time_zone(time_zone: dt.tzinfo) -> None: """ global DEFAULT_TIME_ZONE # pylint: disable=global-statement - # NOTE: Remove in the future in favour of typing assert isinstance(time_zone, dt.tzinfo) DEFAULT_TIME_ZONE = time_zone @@ -49,14 +50,15 @@ def get_time_zone(time_zone_str: str) -> dt.tzinfo | None: Async friendly. """ try: - return pytz.timezone(time_zone_str) - except pytzexceptions.UnknownTimeZoneError: + # Cast can be removed when mypy is switched to Python 3.9. + return cast(dt.tzinfo, zoneinfo.ZoneInfo(time_zone_str)) + except zoneinfo.ZoneInfoNotFoundError: return None def utcnow() -> dt.datetime: """Get now in UTC time.""" - return dt.datetime.now(NATIVE_UTC) + return dt.datetime.now(UTC) def now(time_zone: dt.tzinfo | None = None) -> dt.datetime: @@ -72,15 +74,16 @@ def as_utc(dattim: dt.datetime) -> dt.datetime: if dattim.tzinfo == UTC: return dattim if dattim.tzinfo is None: - dattim = DEFAULT_TIME_ZONE.localize(dattim) # type: ignore + dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) return dattim.astimezone(UTC) -def as_timestamp(dt_value: dt.datetime) -> float: +def as_timestamp(dt_value: dt.datetime | str) -> float: """Convert a date/time into a unix time (seconds since 1970).""" - if hasattr(dt_value, "timestamp"): - parsed_dt: dt.datetime | None = dt_value + parsed_dt: dt.datetime | None + if isinstance(dt_value, dt.datetime): + parsed_dt = dt_value else: parsed_dt = parse_datetime(str(dt_value)) if parsed_dt is None: @@ -93,14 +96,14 @@ def as_local(dattim: dt.datetime) -> dt.datetime: if dattim.tzinfo == DEFAULT_TIME_ZONE: return dattim if dattim.tzinfo is None: - dattim = UTC.localize(dattim) + dattim = dattim.replace(tzinfo=DEFAULT_TIME_ZONE) return dattim.astimezone(DEFAULT_TIME_ZONE) def utc_from_timestamp(timestamp: float) -> dt.datetime: """Return a UTC time from a timestamp.""" - return UTC.localize(dt.datetime.utcfromtimestamp(timestamp)) + return dt.datetime.utcfromtimestamp(timestamp).replace(tzinfo=UTC) def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datetime: @@ -112,9 +115,7 @@ def start_of_local_day(dt_or_d: dt.date | dt.datetime | None = None) -> dt.datet else: date = dt_or_d - return DEFAULT_TIME_ZONE.localize( # type: ignore - dt.datetime.combine(date, dt.time()) - ) + return dt.datetime.combine(date, dt.time(), tzinfo=DEFAULT_TIME_ZONE) # Copyright (c) Django Software Foundation and individual contributors. @@ -239,6 +240,12 @@ def parse_time_expression(parameter: Any, min_value: int, max_value: int) -> lis return res +def _dst_offset_diff(dattim: dt.datetime) -> dt.timedelta: + """Return the offset when crossing the DST barrier.""" + delta = dt.timedelta(hours=24) + return (dattim + delta).utcoffset() - (dattim - delta).utcoffset() # type: ignore[operator] + + def find_next_time_expression_time( now: dt.datetime, # pylint: disable=redefined-outer-name seconds: list[int], @@ -262,15 +269,7 @@ def find_next_time_expression_time( Return None if no such value exists. """ - left = 0 - right = len(arr) - while left < right: - mid = (left + right) // 2 - if arr[mid] < cmp: - left = mid + 1 - else: - right = mid - + left = bisect.bisect_left(arr, cmp) if left == len(arr): return None return arr[left] @@ -312,38 +311,28 @@ def find_next_time_expression_time( result = result.replace(hour=next_hour) - if result.tzinfo is None: + if result.tzinfo in (None, UTC): return result - # Now we need to handle timezones. We will make this datetime object - # "naive" first and then re-convert it to the target timezone. - # This is so that we can call pytz's localize and handle DST changes. - tzinfo: pytzinfo.DstTzInfo = UTC if result.tzinfo == NATIVE_UTC else result.tzinfo - result = result.replace(tzinfo=None) - - try: - result = tzinfo.localize(result, is_dst=None) - except pytzexceptions.AmbiguousTimeError: + if _datetime_ambiguous(result): # This happens when we're leaving daylight saving time and local # clocks are rolled back. In this case, we want to trigger # on both the DST and non-DST time. So when "now" is in the DST # use the DST-on time, and if not, use the DST-off time. - use_dst = bool(now.dst()) - result = tzinfo.localize(result, is_dst=use_dst) - except pytzexceptions.NonExistentTimeError: + fold = 1 if now.dst() else 0 + if result.fold != fold: + result = result.replace(fold=fold) + + if not _datetime_exists(result): # This happens when we're entering daylight saving time and local # clocks are rolled forward, thus there are local times that do # not exist. In this case, we want to trigger on the next time # that *does* exist. # In the worst case, this will run through all the seconds in the # time shift, but that's max 3600 operations for once per year - result = result.replace(tzinfo=tzinfo) + dt.timedelta(seconds=1) - return find_next_time_expression_time(result, seconds, minutes, hours) - - result_dst = cast(dt.timedelta, result.dst()) - now_dst = cast(dt.timedelta, now.dst()) or dt.timedelta(0) - if result_dst >= now_dst: - return result + return find_next_time_expression_time( + result + dt.timedelta(seconds=1), seconds, minutes, hours + ) # Another edge-case when leaving DST: # When now is in DST and ambiguous *and* the next trigger time we *should* @@ -351,23 +340,26 @@ def find_next_time_expression_time( # For example: if triggering on 2:30 and now is 28.10.2018 2:30 (in DST) # we should trigger next on 28.10.2018 2:30 (out of DST), but our # algorithm above would produce 29.10.2018 2:30 (out of DST) + if _datetime_ambiguous(now): + check_result = find_next_time_expression_time( + now + _dst_offset_diff(now), seconds, minutes, hours + ) + if _datetime_ambiguous(check_result): + return check_result - # Step 1: Check if now is ambiguous - try: - tzinfo.localize(now.replace(tzinfo=None), is_dst=None) - return result - except pytzexceptions.AmbiguousTimeError: - pass + return result - # Step 2: Check if result of (now - DST) is ambiguous. - check = now - now_dst - check_result = find_next_time_expression_time(check, seconds, minutes, hours) - try: - tzinfo.localize(check_result.replace(tzinfo=None), is_dst=None) - return result - except pytzexceptions.AmbiguousTimeError: - pass - # OK, edge case does apply. We must override the DST to DST-off - check_result = tzinfo.localize(check_result.replace(tzinfo=None), is_dst=False) - return check_result +def _datetime_exists(dattim: dt.datetime) -> bool: + """Check if a datetime exists.""" + assert dattim.tzinfo is not None + original_tzinfo = dattim.tzinfo + # Check if we can round trip to UTC + return dattim == dattim.astimezone(UTC).astimezone(original_tzinfo) + + +def _datetime_ambiguous(dattim: dt.datetime) -> bool: + """Check whether a datetime is ambiguous.""" + assert dattim.tzinfo is not None + opposite_fold = dattim.replace(fold=not dattim.fold) + return _datetime_exists(dattim) and dattim.utcoffset() != opposite_fold.utcoffset() diff --git a/homeassistant/util/executor.py b/homeassistant/util/executor.py index 6765fc5d8ae..c25c6b9c13f 100644 --- a/homeassistant/util/executor.py +++ b/homeassistant/util/executor.py @@ -2,6 +2,7 @@ from __future__ import annotations from concurrent.futures import ThreadPoolExecutor +import contextlib import logging import queue import sys @@ -49,7 +50,11 @@ def join_or_interrupt_threads( if log: _log_thread_running_at_shutdown(thread.name, thread.ident) - async_raise(thread.ident, SystemExit) + with contextlib.suppress(SystemError): + # SystemError at this stage is usually a race condition + # where the thread happens to die right before we force + # it to raise the exception + async_raise(thread.ident, SystemExit) return joined diff --git a/homeassistant/util/location.py b/homeassistant/util/location.py index c22f5213130..2a3a4ff0922 100644 --- a/homeassistant/util/location.py +++ b/homeassistant/util/location.py @@ -12,9 +12,7 @@ from typing import Any import aiohttp -ELEVATION_URL = "https://api.open-elevation.com/api/v1/lookup" -IP_API = "http://ip-api.com/json" -IPAPI = "https://ipapi.co/json/" +WHOAMI_URL = "https://whoami.home-assistant.io/v1" # Constants from https://github.com/maurycyp/vincenty # Earth ellipsoid according to WGS 84 @@ -34,7 +32,6 @@ LocationInfo = collections.namedtuple( [ "ip", "country_code", - "country_name", "region_code", "region_name", "city", @@ -51,10 +48,7 @@ async def async_detect_location_info( session: aiohttp.ClientSession, ) -> LocationInfo | None: """Detect location information.""" - data = await _get_ipapi(session) - - if data is None: - data = await _get_ip_api(session) + data = await _get_whoami(session) if data is None: return None @@ -164,10 +158,10 @@ def vincenty( return round(s, 6) -async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None: - """Query ipapi.co for location data.""" +async def _get_whoami(session: aiohttp.ClientSession) -> dict[str, Any] | None: + """Query whoami.home-assistant.io for location data.""" try: - resp = await session.get(IPAPI, timeout=5) + resp = await session.get(WHOAMI_URL, timeout=30) except (aiohttp.ClientError, asyncio.TimeoutError): return None @@ -176,44 +170,14 @@ async def _get_ipapi(session: aiohttp.ClientSession) -> dict[str, Any] | None: except (aiohttp.ClientError, ValueError): return None - # ipapi allows 30k free requests/month. Some users exhaust those. - if raw_info.get("latitude") == "Sign up to access": - return None - return { "ip": raw_info.get("ip"), "country_code": raw_info.get("country"), - "country_name": raw_info.get("country_name"), "region_code": raw_info.get("region_code"), "region_name": raw_info.get("region"), "city": raw_info.get("city"), - "zip_code": raw_info.get("postal"), + "zip_code": raw_info.get("postal_code"), "time_zone": raw_info.get("timezone"), - "latitude": raw_info.get("latitude"), - "longitude": raw_info.get("longitude"), - } - - -async def _get_ip_api(session: aiohttp.ClientSession) -> dict[str, Any] | None: - """Query ip-api.com for location data.""" - try: - resp = await session.get(IP_API, timeout=5) - except (aiohttp.ClientError, asyncio.TimeoutError): - return None - - try: - raw_info = await resp.json() - except (aiohttp.ClientError, ValueError): - return None - return { - "ip": raw_info.get("query"), - "country_code": raw_info.get("countryCode"), - "country_name": raw_info.get("country"), - "region_code": raw_info.get("region"), - "region_name": raw_info.get("regionName"), - "city": raw_info.get("city"), - "zip_code": raw_info.get("zip"), - "time_zone": raw_info.get("timezone"), - "latitude": raw_info.get("lat"), - "longitude": raw_info.get("lon"), + "latitude": float(raw_info.get("latitude")), + "longitude": float(raw_info.get("longitude")), } diff --git a/homeassistant/util/yaml/loader.py b/homeassistant/util/yaml/loader.py index dbff753aa68..5e98e4cfc6f 100644 --- a/homeassistant/util/yaml/loader.py +++ b/homeassistant/util/yaml/loader.py @@ -27,7 +27,7 @@ _LOGGER = logging.getLogger(__name__) class Secrets: """Store secrets while loading YAML.""" - def __init__(self, config_dir: Path): + def __init__(self, config_dir: Path) -> None: """Initialize secrets.""" self.config_dir = config_dir self._cache: dict[Path, dict[str, str]] = {} diff --git a/mypy.ini b/mypy.ini index d07714ae3ed..c65f28336ff 100644 --- a/mypy.ini +++ b/mypy.ini @@ -7,9 +7,11 @@ python_version = 3.8 show_error_codes = true follow_imports = silent ignore_missing_imports = true +strict_equality = true warn_incomplete_stub = true warn_redundant_casts = true warn_unused_configs = true +warn_unused_ignores = true check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -17,10 +19,8 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true [mypy-homeassistant.components.*] check_untyped_defs = false @@ -30,12 +30,10 @@ disallow_untyped_calls = false disallow_untyped_decorators = false disallow_untyped_defs = false no_implicit_optional = false -strict_equality = false warn_return_any = false warn_unreachable = false -warn_unused_ignores = false -[mypy-homeassistant.components,homeassistant.components.automation.*,homeassistant.components.binary_sensor.*,homeassistant.components.bond.*,homeassistant.components.calendar.*,homeassistant.components.cover.*,homeassistant.components.device_automation.*,homeassistant.components.frontend.*,homeassistant.components.geo_location.*,homeassistant.components.group.*,homeassistant.components.history.*,homeassistant.components.http.*,homeassistant.components.huawei_lte.*,homeassistant.components.hyperion.*,homeassistant.components.image_processing.*,homeassistant.components.integration.*,homeassistant.components.knx.*,homeassistant.components.light.*,homeassistant.components.lock.*,homeassistant.components.mailbox.*,homeassistant.components.media_player.*,homeassistant.components.notify.*,homeassistant.components.number.*,homeassistant.components.persistent_notification.*,homeassistant.components.proximity.*,homeassistant.components.recorder.purge,homeassistant.components.recorder.repack,homeassistant.components.remote.*,homeassistant.components.scene.*,homeassistant.components.sensor.*,homeassistant.components.slack.*,homeassistant.components.sonos.media_player,homeassistant.components.sun.*,homeassistant.components.switch.*,homeassistant.components.systemmonitor.*,homeassistant.components.tts.*,homeassistant.components.vacuum.*,homeassistant.components.water_heater.*,homeassistant.components.weather.*,homeassistant.components.websocket_api.*,homeassistant.components.zeroconf.*,homeassistant.components.zone.*,homeassistant.components.zwave_js.*] +[mypy-homeassistant.components] check_untyped_defs = true disallow_incomplete_defs = true disallow_subclassing_any = true @@ -43,10 +41,1431 @@ disallow_untyped_calls = true disallow_untyped_decorators = true disallow_untyped_defs = true no_implicit_optional = true -strict_equality = true warn_return_any = true warn_unreachable = true -warn_unused_ignores = true -[mypy-homeassistant.components.adguard.*,homeassistant.components.aemet.*,homeassistant.components.airly.*,homeassistant.components.alarmdecoder.*,homeassistant.components.alexa.*,homeassistant.components.almond.*,homeassistant.components.amcrest.*,homeassistant.components.analytics.*,homeassistant.components.asuswrt.*,homeassistant.components.atag.*,homeassistant.components.aurora.*,homeassistant.components.awair.*,homeassistant.components.axis.*,homeassistant.components.azure_devops.*,homeassistant.components.azure_event_hub.*,homeassistant.components.blueprint.*,homeassistant.components.bluetooth_tracker.*,homeassistant.components.bmw_connected_drive.*,homeassistant.components.bsblan.*,homeassistant.components.camera.*,homeassistant.components.canary.*,homeassistant.components.cast.*,homeassistant.components.cert_expiry.*,homeassistant.components.climacell.*,homeassistant.components.climate.*,homeassistant.components.cloud.*,homeassistant.components.cloudflare.*,homeassistant.components.config.*,homeassistant.components.control4.*,homeassistant.components.conversation.*,homeassistant.components.deconz.*,homeassistant.components.demo.*,homeassistant.components.denonavr.*,homeassistant.components.device_tracker.*,homeassistant.components.devolo_home_control.*,homeassistant.components.dhcp.*,homeassistant.components.directv.*,homeassistant.components.doorbird.*,homeassistant.components.dsmr.*,homeassistant.components.dynalite.*,homeassistant.components.eafm.*,homeassistant.components.edl21.*,homeassistant.components.elgato.*,homeassistant.components.elkm1.*,homeassistant.components.emonitor.*,homeassistant.components.enphase_envoy.*,homeassistant.components.entur_public_transport.*,homeassistant.components.esphome.*,homeassistant.components.evohome.*,homeassistant.components.fan.*,homeassistant.components.filter.*,homeassistant.components.fints.*,homeassistant.components.fireservicerota.*,homeassistant.components.firmata.*,homeassistant.components.fitbit.*,homeassistant.components.flo.*,homeassistant.components.fortios.*,homeassistant.components.foscam.*,homeassistant.components.freebox.*,homeassistant.components.fritz.*,homeassistant.components.fritzbox.*,homeassistant.components.garmin_connect.*,homeassistant.components.geniushub.*,homeassistant.components.gios.*,homeassistant.components.glances.*,homeassistant.components.gogogate2.*,homeassistant.components.google_assistant.*,homeassistant.components.google_maps.*,homeassistant.components.google_pubsub.*,homeassistant.components.gpmdp.*,homeassistant.components.gree.*,homeassistant.components.growatt_server.*,homeassistant.components.gtfs.*,homeassistant.components.guardian.*,homeassistant.components.habitica.*,homeassistant.components.harmony.*,homeassistant.components.hassio.*,homeassistant.components.hdmi_cec.*,homeassistant.components.here_travel_time.*,homeassistant.components.hisense_aehw4a1.*,homeassistant.components.home_connect.*,homeassistant.components.home_plus_control.*,homeassistant.components.homeassistant.*,homeassistant.components.homekit.*,homeassistant.components.homekit_controller.*,homeassistant.components.homematicip_cloud.*,homeassistant.components.honeywell.*,homeassistant.components.hue.*,homeassistant.components.huisbaasje.*,homeassistant.components.humidifier.*,homeassistant.components.iaqualink.*,homeassistant.components.icloud.*,homeassistant.components.ihc.*,homeassistant.components.image.*,homeassistant.components.incomfort.*,homeassistant.components.influxdb.*,homeassistant.components.input_boolean.*,homeassistant.components.input_datetime.*,homeassistant.components.input_number.*,homeassistant.components.insteon.*,homeassistant.components.ipp.*,homeassistant.components.isy994.*,homeassistant.components.izone.*,homeassistant.components.kaiterra.*,homeassistant.components.keenetic_ndms2.*,homeassistant.components.kodi.*,homeassistant.components.konnected.*,homeassistant.components.kostal_plenticore.*,homeassistant.components.kulersky.*,homeassistant.components.lifx.*,homeassistant.components.litejet.*,homeassistant.components.litterrobot.*,homeassistant.components.lovelace.*,homeassistant.components.luftdaten.*,homeassistant.components.lutron_caseta.*,homeassistant.components.lyric.*,homeassistant.components.marytts.*,homeassistant.components.media_source.*,homeassistant.components.melcloud.*,homeassistant.components.meteo_france.*,homeassistant.components.metoffice.*,homeassistant.components.minecraft_server.*,homeassistant.components.mobile_app.*,homeassistant.components.modbus.*,homeassistant.components.motion_blinds.*,homeassistant.components.motioneye.*,homeassistant.components.mqtt.*,homeassistant.components.mullvad.*,homeassistant.components.mysensors.*,homeassistant.components.n26.*,homeassistant.components.neato.*,homeassistant.components.ness_alarm.*,homeassistant.components.nest.*,homeassistant.components.netatmo.*,homeassistant.components.netio.*,homeassistant.components.nightscout.*,homeassistant.components.nilu.*,homeassistant.components.nmap_tracker.*,homeassistant.components.norway_air.*,homeassistant.components.notion.*,homeassistant.components.nsw_fuel_station.*,homeassistant.components.nuki.*,homeassistant.components.nws.*,homeassistant.components.nzbget.*,homeassistant.components.omnilogic.*,homeassistant.components.onboarding.*,homeassistant.components.ondilo_ico.*,homeassistant.components.onewire.*,homeassistant.components.onvif.*,homeassistant.components.ovo_energy.*,homeassistant.components.ozw.*,homeassistant.components.panasonic_viera.*,homeassistant.components.philips_js.*,homeassistant.components.pilight.*,homeassistant.components.ping.*,homeassistant.components.pioneer.*,homeassistant.components.plaato.*,homeassistant.components.plex.*,homeassistant.components.plugwise.*,homeassistant.components.plum_lightpad.*,homeassistant.components.point.*,homeassistant.components.profiler.*,homeassistant.components.proxmoxve.*,homeassistant.components.rachio.*,homeassistant.components.rainmachine.*,homeassistant.components.recollect_waste.*,homeassistant.components.recorder.*,homeassistant.components.reddit.*,homeassistant.components.ring.*,homeassistant.components.rituals_perfume_genie.*,homeassistant.components.roku.*,homeassistant.components.rpi_power.*,homeassistant.components.ruckus_unleashed.*,homeassistant.components.sabnzbd.*,homeassistant.components.screenlogic.*,homeassistant.components.script.*,homeassistant.components.search.*,homeassistant.components.sense.*,homeassistant.components.sentry.*,homeassistant.components.sesame.*,homeassistant.components.sharkiq.*,homeassistant.components.shell_command.*,homeassistant.components.shelly.*,homeassistant.components.sma.*,homeassistant.components.smart_meter_texas.*,homeassistant.components.smartthings.*,homeassistant.components.smarttub.*,homeassistant.components.smarty.*,homeassistant.components.smhi.*,homeassistant.components.solaredge.*,homeassistant.components.solarlog.*,homeassistant.components.somfy.*,homeassistant.components.somfy_mylink.*,homeassistant.components.sonarr.*,homeassistant.components.songpal.*,homeassistant.components.sonos.*,homeassistant.components.spotify.*,homeassistant.components.stream.*,homeassistant.components.stt.*,homeassistant.components.surepetcare.*,homeassistant.components.switchbot.*,homeassistant.components.switcher_kis.*,homeassistant.components.synology_dsm.*,homeassistant.components.synology_srm.*,homeassistant.components.system_health.*,homeassistant.components.system_log.*,homeassistant.components.tado.*,homeassistant.components.tasmota.*,homeassistant.components.tcp.*,homeassistant.components.telegram_bot.*,homeassistant.components.template.*,homeassistant.components.tesla.*,homeassistant.components.timer.*,homeassistant.components.todoist.*,homeassistant.components.toon.*,homeassistant.components.tplink.*,homeassistant.components.trace.*,homeassistant.components.tradfri.*,homeassistant.components.tuya.*,homeassistant.components.twentemilieu.*,homeassistant.components.unifi.*,homeassistant.components.upcloud.*,homeassistant.components.updater.*,homeassistant.components.upnp.*,homeassistant.components.velbus.*,homeassistant.components.vera.*,homeassistant.components.verisure.*,homeassistant.components.vizio.*,homeassistant.components.volumio.*,homeassistant.components.webostv.*,homeassistant.components.wemo.*,homeassistant.components.wink.*,homeassistant.components.withings.*,homeassistant.components.wled.*,homeassistant.components.wunderground.*,homeassistant.components.xbox.*,homeassistant.components.xiaomi_aqara.*,homeassistant.components.xiaomi_miio.*,homeassistant.components.yamaha.*,homeassistant.components.yeelight.*,homeassistant.components.zerproc.*,homeassistant.components.zha.*,homeassistant.components.zwave.*] +[mypy-homeassistant.components.acer_projector.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.accuweather.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.actiontec.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.aftership.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.air_quality.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.airly.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.aladdin_connect.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.alarm_control_panel.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.amazon_polly.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.ampio.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.automation.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.binary_sensor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.bluetooth_tracker.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.bond.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.brother.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.calendar.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.camera.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.canary.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.cover.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.device_automation.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.device_tracker.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.dunehd.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.elgato.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.fitbit.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.fritzbox.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.frontend.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.geo_location.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.gios.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.group.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.history.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.http.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.huawei_lte.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.hyperion.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.image_processing.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.integration.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.knx.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.kraken.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.light.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.lock.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.mailbox.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.media_player.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.nam.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.network.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.notify.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.number.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.onewire.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.persistent_notification.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.proximity.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.purge] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.repack] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.recorder.statistics] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.remote.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.scene.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.sensor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.slack.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.sonos.media_player] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.sun.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.switch.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.synology_dsm.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.systemmonitor.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.tcp.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.tts.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.upcloud.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.vacuum.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.water_heater.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.weather.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.websocket_api.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.zeroconf.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.zone.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-homeassistant.components.zwave_js.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +no_implicit_optional = true +warn_return_any = true +warn_unreachable = true + +[mypy-tests.*] +check_untyped_defs = false +disallow_incomplete_defs = false +disallow_subclassing_any = false +disallow_untyped_calls = false +disallow_untyped_decorators = false +disallow_untyped_defs = false +no_implicit_optional = false +warn_return_any = false +warn_unreachable = false + +[mypy-homeassistant.components.adguard.*] +ignore_errors = true + +[mypy-homeassistant.components.aemet.*] +ignore_errors = true + +[mypy-homeassistant.components.alarmdecoder.*] +ignore_errors = true + +[mypy-homeassistant.components.alexa.*] +ignore_errors = true + +[mypy-homeassistant.components.almond.*] +ignore_errors = true + +[mypy-homeassistant.components.amcrest.*] +ignore_errors = true + +[mypy-homeassistant.components.analytics.*] +ignore_errors = true + +[mypy-homeassistant.components.asuswrt.*] +ignore_errors = true + +[mypy-homeassistant.components.atag.*] +ignore_errors = true + +[mypy-homeassistant.components.aurora.*] +ignore_errors = true + +[mypy-homeassistant.components.awair.*] +ignore_errors = true + +[mypy-homeassistant.components.azure_devops.*] +ignore_errors = true + +[mypy-homeassistant.components.azure_event_hub.*] +ignore_errors = true + +[mypy-homeassistant.components.blueprint.*] +ignore_errors = true + +[mypy-homeassistant.components.bmw_connected_drive.*] +ignore_errors = true + +[mypy-homeassistant.components.bsblan.*] +ignore_errors = true + +[mypy-homeassistant.components.cast.*] +ignore_errors = true + +[mypy-homeassistant.components.cert_expiry.*] +ignore_errors = true + +[mypy-homeassistant.components.climacell.*] +ignore_errors = true + +[mypy-homeassistant.components.climate.*] +ignore_errors = true + +[mypy-homeassistant.components.cloud.*] +ignore_errors = true + +[mypy-homeassistant.components.cloudflare.*] +ignore_errors = true + +[mypy-homeassistant.components.config.*] +ignore_errors = true + +[mypy-homeassistant.components.control4.*] +ignore_errors = true + +[mypy-homeassistant.components.conversation.*] +ignore_errors = true + +[mypy-homeassistant.components.deconz.*] +ignore_errors = true + +[mypy-homeassistant.components.demo.*] +ignore_errors = true + +[mypy-homeassistant.components.denonavr.*] +ignore_errors = true + +[mypy-homeassistant.components.devolo_home_control.*] +ignore_errors = true + +[mypy-homeassistant.components.dhcp.*] +ignore_errors = true + +[mypy-homeassistant.components.directv.*] +ignore_errors = true + +[mypy-homeassistant.components.doorbird.*] +ignore_errors = true + +[mypy-homeassistant.components.dsmr.*] +ignore_errors = true + +[mypy-homeassistant.components.dynalite.*] +ignore_errors = true + +[mypy-homeassistant.components.eafm.*] +ignore_errors = true + +[mypy-homeassistant.components.edl21.*] +ignore_errors = true + +[mypy-homeassistant.components.elkm1.*] +ignore_errors = true + +[mypy-homeassistant.components.emonitor.*] +ignore_errors = true + +[mypy-homeassistant.components.enphase_envoy.*] +ignore_errors = true + +[mypy-homeassistant.components.entur_public_transport.*] +ignore_errors = true + +[mypy-homeassistant.components.esphome.*] +ignore_errors = true + +[mypy-homeassistant.components.evohome.*] +ignore_errors = true + +[mypy-homeassistant.components.fan.*] +ignore_errors = true + +[mypy-homeassistant.components.filter.*] +ignore_errors = true + +[mypy-homeassistant.components.fints.*] +ignore_errors = true + +[mypy-homeassistant.components.fireservicerota.*] +ignore_errors = true + +[mypy-homeassistant.components.firmata.*] +ignore_errors = true + +[mypy-homeassistant.components.flo.*] +ignore_errors = true + +[mypy-homeassistant.components.fortios.*] +ignore_errors = true + +[mypy-homeassistant.components.foscam.*] +ignore_errors = true + +[mypy-homeassistant.components.freebox.*] +ignore_errors = true + +[mypy-homeassistant.components.garmin_connect.*] +ignore_errors = true + +[mypy-homeassistant.components.geniushub.*] +ignore_errors = true + +[mypy-homeassistant.components.glances.*] +ignore_errors = true + +[mypy-homeassistant.components.gogogate2.*] +ignore_errors = true + +[mypy-homeassistant.components.google_assistant.*] +ignore_errors = true + +[mypy-homeassistant.components.google_maps.*] +ignore_errors = true + +[mypy-homeassistant.components.google_pubsub.*] +ignore_errors = true + +[mypy-homeassistant.components.gpmdp.*] +ignore_errors = true + +[mypy-homeassistant.components.gree.*] +ignore_errors = true + +[mypy-homeassistant.components.growatt_server.*] +ignore_errors = true + +[mypy-homeassistant.components.gtfs.*] +ignore_errors = true + +[mypy-homeassistant.components.guardian.*] +ignore_errors = true + +[mypy-homeassistant.components.habitica.*] +ignore_errors = true + +[mypy-homeassistant.components.harmony.*] +ignore_errors = true + +[mypy-homeassistant.components.hassio.*] +ignore_errors = true + +[mypy-homeassistant.components.hdmi_cec.*] +ignore_errors = true + +[mypy-homeassistant.components.here_travel_time.*] +ignore_errors = true + +[mypy-homeassistant.components.hisense_aehw4a1.*] +ignore_errors = true + +[mypy-homeassistant.components.home_connect.*] +ignore_errors = true + +[mypy-homeassistant.components.home_plus_control.*] +ignore_errors = true + +[mypy-homeassistant.components.homeassistant.*] +ignore_errors = true + +[mypy-homeassistant.components.homekit.*] +ignore_errors = true + +[mypy-homeassistant.components.homekit_controller.*] +ignore_errors = true + +[mypy-homeassistant.components.homematicip_cloud.*] +ignore_errors = true + +[mypy-homeassistant.components.honeywell.*] +ignore_errors = true + +[mypy-homeassistant.components.huisbaasje.*] +ignore_errors = true + +[mypy-homeassistant.components.humidifier.*] +ignore_errors = true + +[mypy-homeassistant.components.iaqualink.*] +ignore_errors = true + +[mypy-homeassistant.components.icloud.*] +ignore_errors = true + +[mypy-homeassistant.components.image.*] +ignore_errors = true + +[mypy-homeassistant.components.incomfort.*] +ignore_errors = true + +[mypy-homeassistant.components.influxdb.*] +ignore_errors = true + +[mypy-homeassistant.components.input_boolean.*] +ignore_errors = true + +[mypy-homeassistant.components.input_datetime.*] +ignore_errors = true + +[mypy-homeassistant.components.input_number.*] +ignore_errors = true + +[mypy-homeassistant.components.insteon.*] +ignore_errors = true + +[mypy-homeassistant.components.ipp.*] +ignore_errors = true + +[mypy-homeassistant.components.isy994.*] +ignore_errors = true + +[mypy-homeassistant.components.izone.*] +ignore_errors = true + +[mypy-homeassistant.components.kaiterra.*] +ignore_errors = true + +[mypy-homeassistant.components.keenetic_ndms2.*] +ignore_errors = true + +[mypy-homeassistant.components.kodi.*] +ignore_errors = true + +[mypy-homeassistant.components.konnected.*] +ignore_errors = true + +[mypy-homeassistant.components.kostal_plenticore.*] +ignore_errors = true + +[mypy-homeassistant.components.kulersky.*] +ignore_errors = true + +[mypy-homeassistant.components.lifx.*] +ignore_errors = true + +[mypy-homeassistant.components.litejet.*] +ignore_errors = true + +[mypy-homeassistant.components.litterrobot.*] +ignore_errors = true + +[mypy-homeassistant.components.lovelace.*] +ignore_errors = true + +[mypy-homeassistant.components.luftdaten.*] +ignore_errors = true + +[mypy-homeassistant.components.lutron_caseta.*] +ignore_errors = true + +[mypy-homeassistant.components.lyric.*] +ignore_errors = true + +[mypy-homeassistant.components.marytts.*] +ignore_errors = true + +[mypy-homeassistant.components.media_source.*] +ignore_errors = true + +[mypy-homeassistant.components.melcloud.*] +ignore_errors = true + +[mypy-homeassistant.components.meteo_france.*] +ignore_errors = true + +[mypy-homeassistant.components.metoffice.*] +ignore_errors = true + +[mypy-homeassistant.components.minecraft_server.*] +ignore_errors = true + +[mypy-homeassistant.components.mobile_app.*] +ignore_errors = true + +[mypy-homeassistant.components.motion_blinds.*] +ignore_errors = true + +[mypy-homeassistant.components.mqtt.*] +ignore_errors = true + +[mypy-homeassistant.components.mullvad.*] +ignore_errors = true + +[mypy-homeassistant.components.mysensors.*] +ignore_errors = true + +[mypy-homeassistant.components.neato.*] +ignore_errors = true + +[mypy-homeassistant.components.ness_alarm.*] +ignore_errors = true + +[mypy-homeassistant.components.nest.*] +ignore_errors = true + +[mypy-homeassistant.components.netatmo.*] +ignore_errors = true + +[mypy-homeassistant.components.netio.*] +ignore_errors = true + +[mypy-homeassistant.components.nightscout.*] +ignore_errors = true + +[mypy-homeassistant.components.nilu.*] +ignore_errors = true + +[mypy-homeassistant.components.nmap_tracker.*] +ignore_errors = true + +[mypy-homeassistant.components.norway_air.*] +ignore_errors = true + +[mypy-homeassistant.components.notion.*] +ignore_errors = true + +[mypy-homeassistant.components.nsw_fuel_station.*] +ignore_errors = true + +[mypy-homeassistant.components.nuki.*] +ignore_errors = true + +[mypy-homeassistant.components.nws.*] +ignore_errors = true + +[mypy-homeassistant.components.nzbget.*] +ignore_errors = true + +[mypy-homeassistant.components.omnilogic.*] +ignore_errors = true + +[mypy-homeassistant.components.onboarding.*] +ignore_errors = true + +[mypy-homeassistant.components.ondilo_ico.*] +ignore_errors = true + +[mypy-homeassistant.components.onvif.*] +ignore_errors = true + +[mypy-homeassistant.components.ovo_energy.*] +ignore_errors = true + +[mypy-homeassistant.components.ozw.*] +ignore_errors = true + +[mypy-homeassistant.components.panasonic_viera.*] +ignore_errors = true + +[mypy-homeassistant.components.philips_js.*] +ignore_errors = true + +[mypy-homeassistant.components.pilight.*] +ignore_errors = true + +[mypy-homeassistant.components.ping.*] +ignore_errors = true + +[mypy-homeassistant.components.pioneer.*] +ignore_errors = true + +[mypy-homeassistant.components.plaato.*] +ignore_errors = true + +[mypy-homeassistant.components.plex.*] +ignore_errors = true + +[mypy-homeassistant.components.plugwise.*] +ignore_errors = true + +[mypy-homeassistant.components.plum_lightpad.*] +ignore_errors = true + +[mypy-homeassistant.components.point.*] +ignore_errors = true + +[mypy-homeassistant.components.profiler.*] +ignore_errors = true + +[mypy-homeassistant.components.proxmoxve.*] +ignore_errors = true + +[mypy-homeassistant.components.rachio.*] +ignore_errors = true + +[mypy-homeassistant.components.rainmachine.*] +ignore_errors = true + +[mypy-homeassistant.components.recollect_waste.*] +ignore_errors = true + +[mypy-homeassistant.components.recorder.*] +ignore_errors = true + +[mypy-homeassistant.components.reddit.*] +ignore_errors = true + +[mypy-homeassistant.components.ring.*] +ignore_errors = true + +[mypy-homeassistant.components.roku.*] +ignore_errors = true + +[mypy-homeassistant.components.rpi_power.*] +ignore_errors = true + +[mypy-homeassistant.components.ruckus_unleashed.*] +ignore_errors = true + +[mypy-homeassistant.components.sabnzbd.*] +ignore_errors = true + +[mypy-homeassistant.components.screenlogic.*] +ignore_errors = true + +[mypy-homeassistant.components.script.*] +ignore_errors = true + +[mypy-homeassistant.components.search.*] +ignore_errors = true + +[mypy-homeassistant.components.sense.*] +ignore_errors = true + +[mypy-homeassistant.components.sesame.*] +ignore_errors = true + +[mypy-homeassistant.components.sharkiq.*] +ignore_errors = true + +[mypy-homeassistant.components.sma.*] +ignore_errors = true + +[mypy-homeassistant.components.smart_meter_texas.*] +ignore_errors = true + +[mypy-homeassistant.components.smartthings.*] +ignore_errors = true + +[mypy-homeassistant.components.smarttub.*] +ignore_errors = true + +[mypy-homeassistant.components.smarty.*] +ignore_errors = true + +[mypy-homeassistant.components.solaredge.*] +ignore_errors = true + +[mypy-homeassistant.components.solarlog.*] +ignore_errors = true + +[mypy-homeassistant.components.somfy.*] +ignore_errors = true + +[mypy-homeassistant.components.somfy_mylink.*] +ignore_errors = true + +[mypy-homeassistant.components.sonarr.*] +ignore_errors = true + +[mypy-homeassistant.components.songpal.*] +ignore_errors = true + +[mypy-homeassistant.components.sonos.*] +ignore_errors = true + +[mypy-homeassistant.components.spotify.*] +ignore_errors = true + +[mypy-homeassistant.components.stt.*] +ignore_errors = true + +[mypy-homeassistant.components.surepetcare.*] +ignore_errors = true + +[mypy-homeassistant.components.switchbot.*] +ignore_errors = true + +[mypy-homeassistant.components.switcher_kis.*] +ignore_errors = true + +[mypy-homeassistant.components.synology_srm.*] +ignore_errors = true + +[mypy-homeassistant.components.system_health.*] +ignore_errors = true + +[mypy-homeassistant.components.system_log.*] +ignore_errors = true + +[mypy-homeassistant.components.tado.*] +ignore_errors = true + +[mypy-homeassistant.components.tasmota.*] +ignore_errors = true + +[mypy-homeassistant.components.telegram_bot.*] +ignore_errors = true + +[mypy-homeassistant.components.template.*] +ignore_errors = true + +[mypy-homeassistant.components.tesla.*] +ignore_errors = true + +[mypy-homeassistant.components.timer.*] +ignore_errors = true + +[mypy-homeassistant.components.todoist.*] +ignore_errors = true + +[mypy-homeassistant.components.toon.*] +ignore_errors = true + +[mypy-homeassistant.components.tplink.*] +ignore_errors = true + +[mypy-homeassistant.components.trace.*] +ignore_errors = true + +[mypy-homeassistant.components.tradfri.*] +ignore_errors = true + +[mypy-homeassistant.components.tuya.*] +ignore_errors = true + +[mypy-homeassistant.components.unifi.*] +ignore_errors = true + +[mypy-homeassistant.components.updater.*] +ignore_errors = true + +[mypy-homeassistant.components.upnp.*] +ignore_errors = true + +[mypy-homeassistant.components.velbus.*] +ignore_errors = true + +[mypy-homeassistant.components.vera.*] +ignore_errors = true + +[mypy-homeassistant.components.verisure.*] +ignore_errors = true + +[mypy-homeassistant.components.vizio.*] +ignore_errors = true + +[mypy-homeassistant.components.volumio.*] +ignore_errors = true + +[mypy-homeassistant.components.webostv.*] +ignore_errors = true + +[mypy-homeassistant.components.wemo.*] +ignore_errors = true + +[mypy-homeassistant.components.wink.*] +ignore_errors = true + +[mypy-homeassistant.components.withings.*] +ignore_errors = true + +[mypy-homeassistant.components.wunderground.*] +ignore_errors = true + +[mypy-homeassistant.components.xbox.*] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_aqara.*] +ignore_errors = true + +[mypy-homeassistant.components.xiaomi_miio.*] +ignore_errors = true + +[mypy-homeassistant.components.yamaha.*] +ignore_errors = true + +[mypy-homeassistant.components.yeelight.*] +ignore_errors = true + +[mypy-homeassistant.components.zerproc.*] +ignore_errors = true + +[mypy-homeassistant.components.zha.*] +ignore_errors = true + +[mypy-homeassistant.components.zwave.*] ignore_errors = true diff --git a/pylint/plugins/hass_constructor.py b/pylint/plugins/hass_constructor.py new file mode 100644 index 00000000000..f0f23ef4c95 --- /dev/null +++ b/pylint/plugins/hass_constructor.py @@ -0,0 +1,52 @@ +"""Plugin for constructor definitions.""" +from astroid import Const, FunctionDef +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter + + +class HassConstructorFormatChecker(BaseChecker): # type: ignore[misc] + """Checker for __init__ definitions.""" + + __implements__ = IAstroidChecker + + name = "hass_constructor" + priority = -1 + msgs = { + "W0006": ( + '__init__ should have explicit return type "None"', + "hass-constructor-return", + "Used when __init__ has all arguments typed " + "but doesn't have return type declared", + ), + } + options = () + + def visit_functiondef(self, node: FunctionDef) -> None: + """Called when a FunctionDef node is visited.""" + if not node.is_method() or node.name != "__init__": + return + + # Check that all arguments are annotated. + # The first argument is "self". + args = node.args + annotations = ( + args.posonlyargs_annotations + + args.annotations + + args.kwonlyargs_annotations + )[1:] + if args.vararg is not None: + annotations.append(args.varargannotation) + if args.kwarg is not None: + annotations.append(args.kwargannotation) + if not annotations or None in annotations: + return + + # Check that return type is specified and it is "None". + if not isinstance(node.returns, Const) or node.returns.value is not None: + self.add_message("hass-constructor-return", node=node) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassConstructorFormatChecker(linter)) diff --git a/pylint/plugins/hass_imports.py b/pylint/plugins/hass_imports.py new file mode 100644 index 00000000000..341abff202a --- /dev/null +++ b/pylint/plugins/hass_imports.py @@ -0,0 +1,52 @@ +"""Plugin for checking imports.""" +from __future__ import annotations + +from astroid import Import, ImportFrom, Module +from pylint.checkers import BaseChecker +from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter + + +class HassImportsFormatChecker(BaseChecker): # type: ignore[misc] + """Checker for imports.""" + + __implements__ = IAstroidChecker + + name = "hass_imports" + priority = -1 + msgs = { + "W0011": ( + "Relative import should be used", + "hass-relative-import", + "Used when absolute import should be replaced with relative import", + ), + } + options = () + + def __init__(self, linter: PyLinter | None = None) -> None: + super().__init__(linter) + self.current_module: str | None = None + + def visit_module(self, node: Module) -> None: + """Called when a Import node is visited.""" + self.current_module = node.name + + def visit_import(self, node: Import) -> None: + """Called when a Import node is visited.""" + for module, _alias in node.names: + if module.startswith(f"{self.current_module}."): + self.add_message("hass-relative-import", node=node) + + def visit_importfrom(self, node: ImportFrom) -> None: + """Called when a ImportFrom node is visited.""" + if node.level is not None: + return + if node.modname == self.current_module or node.modname.startswith( + f"{self.current_module}." + ): + self.add_message("hass-relative-import", node=node) + + +def register(linter: PyLinter) -> None: + """Register the checker.""" + linter.register_checker(HassImportsFormatChecker(linter)) diff --git a/pylint/plugins/hass_logger.py b/pylint/plugins/hass_logger.py index b771b07aa5e..0ca57b8da19 100644 --- a/pylint/plugins/hass_logger.py +++ b/pylint/plugins/hass_logger.py @@ -1,18 +1,18 @@ +"""Plugin for logger invocations.""" import astroid from pylint.checkers import BaseChecker from pylint.interfaces import IAstroidChecker +from pylint.lint import PyLinter LOGGER_NAMES = ("LOGGER", "_LOGGER") LOG_LEVEL_ALLOWED_LOWER_START = ("debug",) -# This is our checker class. -# Checkers should always inherit from `BaseChecker`. -class HassLoggerFormatChecker(BaseChecker): - """Add class member attributes to the class locals dictionary.""" + +class HassLoggerFormatChecker(BaseChecker): # type: ignore[misc] + """Checker for logger invocations.""" __implements__ = IAstroidChecker - # The name defines a custom section of the config for this checker. name = "hass_logger" priority = -1 msgs = { @@ -27,24 +27,10 @@ class HassLoggerFormatChecker(BaseChecker): "All logger messages must start with a capital letter", ), } - options = ( - ( - "hass-logger", - { - "default": "properties", - "help": ( - "Validate _LOGGER or LOGGER messages conform to Home Assistant standards." - ), - }, - ), - ) + options = () - def visit_call(self, node): - """Called when a :class:`.astroid.node_classes.Call` node is visited. - See :mod:`astroid` for the description of available nodes. - :param node: The node to check. - :type node: astroid.node_classes.Call - """ + def visit_call(self, node: astroid.Call) -> None: + """Called when a Call node is visited.""" if not isinstance(node.func, astroid.Attribute) or not isinstance( node.func.expr, astroid.Name ): @@ -67,19 +53,16 @@ class HassLoggerFormatChecker(BaseChecker): return if log_message[-1] == ".": - self.add_message("hass-logger-period", args=node.args, node=node) + self.add_message("hass-logger-period", node=node) if ( isinstance(node.func.attrname, str) and node.func.attrname not in LOG_LEVEL_ALLOWED_LOWER_START and log_message[0].upper() != log_message[0] ): - self.add_message("hass-logger-capital", args=node.args, node=node) + self.add_message("hass-logger-capital", node=node) -def register(linter): - """This required method auto registers the checker. - :param linter: The linter to register the checker to. - :type linter: pylint.lint.PyLinter - """ +def register(linter: PyLinter) -> None: + """Register the checker.""" linter.register_checker(HassLoggerFormatChecker(linter)) diff --git a/pyproject.toml b/pyproject.toml index 0e38a197319..f8d47624c8f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,9 @@ init-hook='from pylint.config.find_default_config_files import find_default_conf load-plugins = [ "pylint.extensions.typing", "pylint_strict_informational", - "hass_logger" + "hass_constructor", + "hass_imports", + "hass_logger", ] persistent = false extension-pkg-whitelist = [ diff --git a/requirements.txt b/requirements.txt index 475ece2b866..7d9b7739669 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,18 +4,18 @@ aiohttp==3.7.4.post0 astral==2.2 async_timeout==3.0.1 -attrs==20.3.0 -awesomeversion==21.2.3 +attrs==21.2.0 +awesomeversion==21.4.0 +backports.zoneinfo;python_version<"3.9" bcrypt==3.1.7 certifi>=2020.12.5 ciso8601==2.1.3 httpx==0.18.0 -jinja2>=2.11.3 +jinja2>=3.0.1 PyJWT==1.7.1 cryptography==3.3.2 pip>=8.0.3,<20.3 python-slugify==4.0.1 -pytz>=2021.1 pyyaml==5.4.1 requests==2.25.1 ruamel.yaml==0.15.100 diff --git a/requirements_all.txt b/requirements_all.txt index 9d227d63288..5e976904b36 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2,7 +2,7 @@ -r requirements.txt # homeassistant.components.aemet -AEMET-OpenData==0.1.8 +AEMET-OpenData==0.2.1 # homeassistant.components.sht31 Adafruit-GPIO==1.0.3 @@ -14,7 +14,7 @@ Adafruit-SHT31==1.0.2 # Adafruit_BBIO==1.1.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.mastodon Mastodon.py==1.5.1 @@ -46,7 +46,7 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.1 +PyRMVtransport==0.3.2 # homeassistant.components.telegram_bot PySocks==1.7.1 @@ -58,7 +58,7 @@ PySocks==1.7.1 PyTransportNSW==0.1.1 # homeassistant.components.homekit -PyTurboJPEG==1.4.0 +PyTurboJPEG==1.5.0 # homeassistant.components.vicare PyViCare==0.2.5 @@ -78,7 +78,7 @@ RtmAPI==0.7.2 TravisPy==0.3.5 # homeassistant.components.twitter -TwitterAPI==2.7.2 +TwitterAPI==2.7.3 # homeassistant.components.tof # VL53L1X2==0.1.5 @@ -93,7 +93,7 @@ WazeRouteCalculator==0.12 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.1.1 +accuweather==0.2.0 # homeassistant.components.bmp280 adafruit-circuitpython-bmp280==3.1.1 @@ -108,7 +108,7 @@ adafruit-circuitpython-mcp230xx==2.2.2 adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 @@ -147,11 +147,11 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.4.0 +aiodiscover==1.4.2 # homeassistant.components.dnsip # homeassistant.components.minecraft_server -aiodns==2.0.0 +aiodns==3.0.0 # homeassistant.components.eafm aioeafm==0.1.2 @@ -160,7 +160,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.6.6 +aioesphomeapi==2.8.0 # homeassistant.components.flo aioflo==0.4.1 @@ -175,14 +175,14 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.61 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.3.1 +aiohue==2.5.0 # homeassistant.components.imap aioimaplib==0.7.15 @@ -218,7 +218,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.0.2 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 @@ -227,11 +227,14 @@ aiopylgtv==0.4.0 aiorecollect==1.0.4 # homeassistant.components.shelly -aioshelly==0.6.2 +aioshelly==0.6.4 # homeassistant.components.switcher_kis aioswitcher==1.2.1 +# homeassistant.components.syncthing +aiosyncthing==0.5.1 + # homeassistant.components.unifi aiounifi==26 @@ -269,7 +272,7 @@ apcaccess==0.0.13 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.9 +apprise==0.9.3 # homeassistant.components.aprs aprslib==0.6.46 @@ -292,7 +295,7 @@ asterisk_mbox==0.5.0 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.2 +async-upnp-client==0.18.0 # homeassistant.components.supla asyncpysupla==0.0.5 @@ -355,7 +358,7 @@ bimmer_connected==0.7.15 bizkaibus==0.1.1 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 @@ -379,18 +382,21 @@ blockchain==1.4.4 # homeassistant.components.bond bond-api==0.1.12 +# homeassistant.components.bosch_shc +boschshcpy==0.2.17 + # homeassistant.components.amazon_polly # homeassistant.components.route53 boto3==1.16.52 # homeassistant.components.braviatv -bravia-tv==1.0.8 +bravia-tv==1.0.11 # homeassistant.components.broadlink broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.0 +brother==1.0.2 # homeassistant.components.brottsplatskartan brottsplatskartan==0.0.1 @@ -461,7 +467,7 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.1 +debugpy==1.3.0 # homeassistant.components.decora # decora==0.6 @@ -473,7 +479,7 @@ debugpy==1.2.1 # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect # homeassistant.components.ssdp -defusedxml==0.6.0 +defusedxml==0.7.1 # homeassistant.components.deluge deluge-client==1.7.1 @@ -491,7 +497,7 @@ directv==0.4.0 discogs_client==2.3.0 # homeassistant.components.discord -discord.py==1.6.0 +discord.py==1.7.2 # homeassistant.components.updater distro==1.5.0 @@ -526,11 +532,8 @@ ebusdpy==0.0.16 # homeassistant.components.ecoal_boiler ecoaliface==0.4.0 -# homeassistant.components.ee_brightbox -eebrightbox==0.0.4 - # homeassistant.components.elgato -elgato==2.0.1 +elgato==2.1.0 # homeassistant.components.eliqonline eliqonline==1.2.2 @@ -557,13 +560,13 @@ env_canada==0.2.5 # envirophat==0.0.6 # homeassistant.components.enphase_envoy -envoy_reader==0.18.4 +envoy_reader==0.19.0 # homeassistant.components.season ephem==3.7.7.0 # homeassistant.components.epson -epson-projector==0.2.3 +epson-projector==0.4.2 # homeassistant.components.epsonworkforce epsonprinter==0.0.9 @@ -572,10 +575,10 @@ epsonprinter==0.0.9 eternalegypt==0.0.12 # homeassistant.components.keyboard_remote -# evdev==1.1.2 +# evdev==1.4.0 # homeassistant.components.evohome -evohome-async==0.3.8 +evohome-async==0.3.15 # homeassistant.components.faa_delays faadelays==0.0.7 @@ -628,18 +631,21 @@ fritzconnection==1.4.2 # homeassistant.components.google_translate gTTS==2.2.2 +# homeassistant.components.garages_amsterdam +garages-amsterdam==2.1.1 + # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geniushub geniushub-client==0.6.30 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed -geojson_client==0.4 +geojson_client==0.6 # homeassistant.components.aprs -geopy==1.21.0 +geopy==2.1.0 # homeassistant.components.geo_rss_events georss_generic_client==0.4 @@ -657,7 +663,7 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.2.1 +gios==1.0.1 # homeassistant.components.gitter gitterpy==0.1.7 @@ -669,10 +675,7 @@ glances_api==0.2.0 gntp==1.0.3 # homeassistant.components.goalzero -goalzero==0.1.4 - -# homeassistant.components.gogogate2 -gogogate2-api==3.0.0 +goalzero==0.1.7 # homeassistant.components.google google-api-python-client==1.6.4 @@ -708,7 +711,7 @@ greeneye_monitor==2.1 greenwavereality==0.5.1 # homeassistant.components.growatt_server -growattServer==1.0.0 +growattServer==1.0.1 # homeassistant.components.gstreamer gstreamer-player==1.1.2 @@ -735,7 +738,7 @@ hass-nabucasa==0.43.0 hass_splunk==0.1.1 # homeassistant.components.tasmota -hatasmota==0.2.13 +hatasmota==0.2.14 # homeassistant.components.jewish_calendar hdate==0.10.2 @@ -762,7 +765,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210504.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -784,7 +787,7 @@ horimote==0.4.1 httplib2==0.19.0 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.17 +huawei-lte-api==1.4.18 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 @@ -807,7 +810,7 @@ iammeter==0.1.7 iaqualink==0.3.4 # homeassistant.components.watson_tts -ibm-watson==4.0.1 +ibm-watson==5.1.0 # homeassistant.components.watson_iot ibmiotf==0.3.4 @@ -815,6 +818,9 @@ ibmiotf==0.3.4 # homeassistant.components.ping icmplib==2.1.1 +# homeassistant.components.network +ifaddr==0.1.7 + # homeassistant.components.iglo iglo==1.2.7 @@ -833,6 +839,9 @@ influxdb==5.2.3 # homeassistant.components.iperf3 iperf3==0.1.11 +# homeassistant.components.gogogate2 +ismartgate==4.0.0 + # homeassistant.components.rest jsonpath==0.82 @@ -851,6 +860,9 @@ konnected==1.2.0 # homeassistant.components.kostal_plenticore kostal_plenticore==0.2.0 +# homeassistant.components.kraken +krakenex==2.1.0 + # homeassistant.components.eufy lakeside==0.12 @@ -872,9 +884,6 @@ libsoundtouch==0.8 # homeassistant.components.life360 life360==4.1.1 -# homeassistant.components.lifx_legacy -liffylights==0.9.4 - # homeassistant.components.osramlightify lightify==1.0.7.3 @@ -900,7 +909,7 @@ logi_circle==0.2.2 london-tube-status==0.2 # homeassistant.components.luftdaten -luftdaten==0.6.4 +luftdaten==0.6.5 # homeassistant.components.lupusec lupupy==0.0.18 @@ -918,7 +927,7 @@ magicseaweed==1.0.3 matrix-client==0.3.2 # homeassistant.components.maxcube -maxcube-api==0.4.2 +maxcube-api==0.4.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -942,7 +951,7 @@ mficlient==0.3.0 miflora==0.7.0 # homeassistant.components.mill -millheater==0.4.0 +millheater==0.4.1 # homeassistant.components.minio minio==4.0.9 @@ -971,9 +980,6 @@ mychevy==2.1.1 # homeassistant.components.mycroft mycroftapi==2.0 -# homeassistant.components.n26 -n26==0.2.7 - # homeassistant.components.nad nad_receiver==0.0.12 @@ -988,13 +994,16 @@ netdata==0.2.0 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.2 +netdisco==2.8.3 + +# homeassistant.components.nam +nettigo-air-monitor==0.2.6 # homeassistant.components.neurio_energy neurio==0.3.1 # homeassistant.components.nexia -nexia==0.9.6 +nexia==0.9.7 # homeassistant.components.nextcloud nextcloudmonitor==1.1.0 @@ -1028,7 +1037,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.20.2 +numpy==1.20.3 # homeassistant.components.oasa_telematics oasatelematics==0.3 @@ -1058,7 +1067,7 @@ onvif-zeep-async==1.0.0 open-garage==0.1.4 # homeassistant.components.opencv -# opencv-python-headless==4.3.0.36 +# opencv-python-headless==4.4.0.42 # homeassistant.components.openerz openerz-api==0.1.0 @@ -1067,7 +1076,7 @@ openerz-api==0.1.0 openevsewifi==1.1.0 # homeassistant.components.openhome -openhomedevice==0.7.2 +openhomedevice==2.0.1 # homeassistant.components.opensensemap opensensemap-api==0.1.5 @@ -1168,7 +1177,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.4 +praw==7.2.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 @@ -1210,7 +1219,7 @@ pwmled==1.6.7 py-canary==0.5.1 # homeassistant.components.cpuspeed -py-cpuinfo==7.0.0 +py-cpuinfo==8.0.0 # homeassistant.components.melissa py-melissa-climate==2.1.4 @@ -1250,7 +1259,7 @@ pyRFXtrx==0.26.1 # pySwitchmate==0.4.6 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.17.0 # homeassistant.components.dlink pyW215==0.7.0 @@ -1286,7 +1295,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.3 +pyatmo==5.0.1 # homeassistant.components.atome pyatome==0.1.1 @@ -1403,7 +1412,7 @@ pyfireservicerota==0.0.40 pyflexit==0.3 # homeassistant.components.flic -pyflic-homeassistant==0.4.dev0 +pyflic==2.0.3 # homeassistant.components.flume pyflume==0.5.5 @@ -1446,7 +1455,7 @@ pyheos==0.7.2 pyhik==0.2.8 # homeassistant.components.hive -pyhiveapi==0.4.1 +pyhiveapi==0.4.2 # homeassistant.components.homematic pyhomematic==0.1.72 @@ -1455,7 +1464,7 @@ pyhomematic==0.1.72 pyhomeworks==0.0.6 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 @@ -1473,7 +1482,7 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.irish_rail_transport pyirishrail==0.0.2 @@ -1482,7 +1491,7 @@ pyirishrail==0.0.2 pyiss==1.0.1 # homeassistant.components.isy994 -pyisy==2.1.1 +pyisy==3.0.0 # homeassistant.components.itach pyitachip2ir==0.0.7 @@ -1496,6 +1505,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.5 +# homeassistant.components.kraken +pykrakenapi==0.1.8 + # homeassistant.components.kulersky pykulersky==0.5.2 @@ -1512,7 +1524,7 @@ pylast==4.2.0 pylaunches==1.0.0 # homeassistant.components.lg_netcast -pylgnetcast-homeassistant==0.2.0.dev0 +pylgnetcast==0.3.3 # homeassistant.components.forked_daapd pylibrespot-java==0.1.0 @@ -1527,7 +1539,7 @@ pylitterbot==2021.3.1 pyloopenergy==0.2.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.9.0 +pylutron-caseta==0.10.0 # homeassistant.components.lutron pylutron==0.2.7 @@ -1539,13 +1551,16 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.0.9 +pymazda==0.1.6 # homeassistant.components.mediaroom pymediaroom==0.6.4.1 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 + +# homeassistant.components.meteoclimatic +pymeteoclimatic==0.0.6 # homeassistant.components.somfy pymfy==0.9.3 @@ -1557,7 +1572,7 @@ pymitv==1.4.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.1 +pymodbus==2.5.2 # homeassistant.components.monoprice pymonoprice==0.3 @@ -1676,10 +1691,7 @@ pyrepetier==3.0.5 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.2 - -# homeassistant.components.zeroconf -pyroute2==0.5.18 +pyrituals==0.0.3 # homeassistant.components.ruckus_unleashed pyruckus==0.12 @@ -1710,6 +1722,9 @@ pysesame2==1.0.1 # homeassistant.components.goalfeed pysher==1.0.1 +# homeassistant.components.sia +pysiaalarm==3.0.0 + # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 @@ -1741,7 +1756,7 @@ pysnmp==4.4.12 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.47 +pysonos==0.0.49 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -1756,7 +1771,7 @@ pystiebeleltron==0.0.1.dev2 pysuez==0.1.19 # homeassistant.components.syncthru -pysyncthru==0.7.0 +pysyncthru==0.7.3 # homeassistant.components.tankerkoenig pytankerkoenig==0.0.6 @@ -1813,7 +1828,7 @@ python-izone==1.1.4 python-join-api==0.0.6 # homeassistant.components.juicenet -python-juicenet==1.0.1 +python-juicenet==1.0.2 # homeassistant.components.lirc # python-lirc==1.2.3 @@ -1846,7 +1861,7 @@ python-qbittorrent==0.4.2 python-ripple-api==0.0.3 # homeassistant.components.smarttub -python-smarttub==0.0.24 +python-smarttub==0.0.25 # homeassistant.components.sochain python-sochain-api==0.0.2 @@ -1919,7 +1934,7 @@ pyvera==0.3.13 pyversasense==0.0.6 # homeassistant.components.vesync -pyvesync==1.3.1 +pyvesync==1.4.0 # homeassistant.components.vizio pyvizio==0.1.57 @@ -1937,7 +1952,7 @@ pywebpush==1.9.2 pywemo==0.6.3 # homeassistant.components.wilight -pywilight==0.0.68 +pywilight==0.0.70 # homeassistant.components.xeoma pyxeoma==1.4.1 @@ -2000,7 +2015,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rova rova==0.2.1 @@ -2042,7 +2057,7 @@ screenlogicpy==0.4.1 scsgate==0.1.0 # homeassistant.components.sendgrid -sendgrid==6.6.0 +sendgrid==6.7.0 # homeassistant.components.sensehat sense-hat==2.2.0 @@ -2052,7 +2067,7 @@ sense-hat==2.2.0 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==1.0.0 +sentry-sdk==1.1.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 @@ -2070,7 +2085,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==9.6.10 +simplisafe-python==10.0.0 # homeassistant.components.sisyphus sisyphus-control==3.0 @@ -2085,7 +2100,7 @@ slackclient==2.5.0 sleepyq==0.8.1 # homeassistant.components.xmpp -slixmpp==1.7.0 +slixmpp==1.7.1 # homeassistant.components.smart_meter_texas smart-meter-texas==0.4.0 @@ -2102,7 +2117,7 @@ smarthab==0.21 # smbus-cffi==0.5.1 # homeassistant.components.smhi -smhi-pkg==1.0.13 +smhi-pkg==1.0.15 # homeassistant.components.snapcast snapcast==2.1.3 @@ -2134,9 +2149,6 @@ speedtest-cli==2.1.3 # homeassistant.components.spider spiderpy==1.4.2 -# homeassistant.components.spotcrime -spotcrime==1.0.4 - # homeassistant.components.spotify spotipy==2.18.0 @@ -2192,6 +2204,9 @@ synology-srm==0.2.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.2 +# homeassistant.components.system_bridge +systembridge==1.1.5 + # homeassistant.components.tahoma tahoma-api==0.0.16 @@ -2220,7 +2235,7 @@ temperusb==1.5.3 # tensorflow==2.3.0 # homeassistant.components.powerwall -tesla-powerwall==0.3.5 +tesla-powerwall==0.3.10 # homeassistant.components.tesla teslajsonpy==0.18.3 @@ -2277,7 +2292,7 @@ unifiled==0.11 upb_lib==0.4.12 # homeassistant.components.upcloud -upcloud-api==1.0.1 +upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru @@ -2293,7 +2308,7 @@ uvcclient==0.11.0 vallox-websocket-api==2.4.0 # homeassistant.components.venstar -venstarcolortouch==0.13 +venstarcolortouch==0.14 # homeassistant.components.vilfo vilfo-api-client==0.3.2 @@ -2313,14 +2328,18 @@ vtjp==0.1.14 # homeassistant.components.vultr vultr==0.1.2 +# homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==2.0.1 +# homeassistant.components.wallbox +wallbox==0.4.4 + # homeassistant.components.waqi waqiasync==1.0.0 # homeassistant.components.folder_watcher -watchdog==2.0.3 +watchdog==2.1.2 # homeassistant.components.waterfurnace waterfurnace==1.1.0 @@ -2356,10 +2375,9 @@ xbox-webapi==2.0.11 xboxapi==2.0.1 # homeassistant.components.knx -xknx==0.18.1 +xknx==0.18.2 # homeassistant.components.bluesound -# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 @@ -2370,19 +2388,19 @@ xmltodict==0.12.0 xs1-api-client==3.0.0 # homeassistant.components.yale_smart_alarm -yalesmartalarmclient==0.1.6 +yalesmartalarmclient==0.3.3 # homeassistant.components.august yalexs==1.1.11 # homeassistant.components.yeelight -yeelight==0.6.2 +yeelight==0.6.3 # homeassistant.components.yeelightsunflower yeelightsunflower==0.0.10 # homeassistant.components.media_extractor -youtube_dl==2021.04.17 +youtube_dl==2021.04.26 # homeassistant.components.onvif zeep[async]==4.0.0 @@ -2391,7 +2409,7 @@ zeep[async]==4.0.0 zengge==0.2 # homeassistant.components.zeroconf -zeroconf==0.29.0 +zeroconf==0.31.0 # homeassistant.components.zha zha-quirks==0.0.57 @@ -2415,7 +2433,7 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.4.0 +zigpy-znp==0.5.1 # homeassistant.components.zha zigpy==0.33.0 @@ -2424,4 +2442,4 @@ zigpy==0.33.0 zm-py==0.5.2 # homeassistant.components.zwave_js -zwave-js-server-python==0.24.0 +zwave-js-server-python==0.26.0 diff --git a/requirements_test.txt b/requirements_test.txt index 42160b36eb1..02b041d6074 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -9,9 +9,8 @@ coverage==5.5 jsonpickle==1.4.1 mock-open==1.4.0 mypy==0.812 -pre-commit==2.12.1 -pylint==2.8.0 -astroid==2.5.5 +pre-commit==2.13.0 +pylint==2.8.2 pipdeptree==1.0.0 pylint-strict-informational==0.1 pytest-aiohttp==0.3.0 @@ -19,9 +18,9 @@ pytest-cov==2.10.1 pytest-test-groups==1.0.3 pytest-sugar==0.9.4 pytest-timeout==1.4.2 -pytest-xdist==2.1.0 -pytest==6.2.3 -requests_mock==1.8.0 +pytest-xdist==2.2.1 +pytest==6.2.4 +requests_mock==1.9.2 responses==0.12.0 respx==0.17.0 stdlib-list==0.7.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index dedf01de7ba..0050f48917f 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -4,10 +4,10 @@ -r requirements_test.txt # homeassistant.components.aemet -AEMET-OpenData==0.1.8 +AEMET-OpenData==0.2.1 # homeassistant.components.homekit -HAP-python==3.4.1 +HAP-python==3.5.0 # homeassistant.components.flick_electric PyFlick==0.0.2 @@ -21,13 +21,13 @@ PyNaCl==1.3.0 PyQRCode==1.2.1 # homeassistant.components.rmvtransport -PyRMVtransport==0.3.1 +PyRMVtransport==0.3.2 # homeassistant.components.transport_nsw PyTransportNSW==0.1.1 # homeassistant.components.homekit -PyTurboJPEG==1.4.0 +PyTurboJPEG==1.5.0 # homeassistant.components.xiaomi_aqara PyXiaomiGateway==0.13.4 @@ -45,13 +45,13 @@ WazeRouteCalculator==0.12 abodepy==1.2.0 # homeassistant.components.accuweather -accuweather==0.1.1 +accuweather==0.2.0 # homeassistant.components.androidtv adb-shell[async]==0.3.1 # homeassistant.components.alarmdecoder -adext==0.4.1 +adext==0.4.2 # homeassistant.components.adguard adguardhome==0.5.0 @@ -87,11 +87,11 @@ aioazuredevops==1.3.5 aiobotocore==1.2.2 # homeassistant.components.dhcp -aiodiscover==1.4.0 +aiodiscover==1.4.2 # homeassistant.components.dnsip # homeassistant.components.minecraft_server -aiodns==2.0.0 +aiodns==3.0.0 # homeassistant.components.eafm aioeafm==0.1.2 @@ -100,7 +100,7 @@ aioeafm==0.1.2 aioemonitor==1.0.5 # homeassistant.components.esphome -aioesphomeapi==2.6.6 +aioesphomeapi==2.8.0 # homeassistant.components.flo aioflo==0.4.1 @@ -112,14 +112,14 @@ aioguardian==1.0.4 aioharmony==0.2.7 # homeassistant.components.homekit_controller -aiohomekit==0.2.61 +aiohomekit==0.2.66 # homeassistant.components.emulated_hue # homeassistant.components.http aiohttp_cors==0.7.0 # homeassistant.components.hue -aiohue==2.3.1 +aiohue==2.5.0 # homeassistant.components.apache_kafka aiokafka==0.6.0 @@ -140,7 +140,7 @@ aiopulse==0.4.2 aiopvapi==1.6.14 # homeassistant.components.pvpc_hourly_pricing -aiopvpc==2.0.2 +aiopvpc==2.1.2 # homeassistant.components.webostv aiopylgtv==0.4.0 @@ -149,11 +149,14 @@ aiopylgtv==0.4.0 aiorecollect==1.0.4 # homeassistant.components.shelly -aioshelly==0.6.2 +aioshelly==0.6.4 # homeassistant.components.switcher_kis aioswitcher==1.2.1 +# homeassistant.components.syncthing +aiosyncthing==0.5.1 + # homeassistant.components.unifi aiounifi==26 @@ -173,7 +176,7 @@ androidtv[async]==0.0.59 apns2==0.3.0 # homeassistant.components.apprise -apprise==0.8.9 +apprise==0.9.3 # homeassistant.components.aprs aprslib==0.6.46 @@ -184,7 +187,7 @@ arcam-fmj==0.5.3 # homeassistant.components.dlna_dmr # homeassistant.components.ssdp # homeassistant.components.upnp -async-upnp-client==0.16.2 +async-upnp-client==0.18.0 # homeassistant.components.aurora auroranoaa==0.0.2 @@ -208,7 +211,7 @@ bellows==0.24.0 bimmer_connected==0.7.15 # homeassistant.components.blebox -blebox_uniapi==1.3.2 +blebox_uniapi==1.3.3 # homeassistant.components.blink blinkpy==0.17.0 @@ -216,14 +219,17 @@ blinkpy==0.17.0 # homeassistant.components.bond bond-api==0.1.12 +# homeassistant.components.bosch_shc +boschshcpy==0.2.17 + # homeassistant.components.braviatv -bravia-tv==1.0.8 +bravia-tv==1.0.11 # homeassistant.components.broadlink broadlink==0.17.0 # homeassistant.components.brother -brother==1.0.0 +brother==1.0.2 # homeassistant.components.bsblan bsblan==0.4.0 @@ -255,13 +261,13 @@ datadog==0.15.0 datapoint==0.9.5 # homeassistant.components.debugpy -debugpy==1.2.1 +debugpy==1.3.0 # homeassistant.components.ihc # homeassistant.components.namecheapdns # homeassistant.components.ohmconnect # homeassistant.components.ssdp -defusedxml==0.6.0 +defusedxml==0.7.1 # homeassistant.components.denonavr denonavr==0.10.8 @@ -284,11 +290,8 @@ dsmr_parser==0.29 # homeassistant.components.dynalite dynalite_devices==0.1.46 -# homeassistant.components.ee_brightbox -eebrightbox==0.0.4 - # homeassistant.components.elgato -elgato==2.0.1 +elgato==2.1.0 # homeassistant.components.elkm1 elkm1-lib==0.8.10 @@ -303,13 +306,13 @@ emulated_roku==0.2.1 enocean==0.50 # homeassistant.components.enphase_envoy -envoy_reader==0.18.4 +envoy_reader==0.19.0 # homeassistant.components.season ephem==3.7.7.0 # homeassistant.components.epson -epson-projector==0.2.3 +epson-projector==0.4.2 # homeassistant.components.faa_delays faadelays==0.0.7 @@ -334,15 +337,18 @@ fritzconnection==1.4.2 # homeassistant.components.google_translate gTTS==2.2.2 +# homeassistant.components.garages_amsterdam +garages-amsterdam==2.1.1 + # homeassistant.components.garmin_connect -garminconnect==0.1.19 +garminconnect_aio==0.1.1 # homeassistant.components.geo_json_events # homeassistant.components.usgs_earthquakes_feed -geojson_client==0.4 +geojson_client==0.6 # homeassistant.components.aprs -geopy==1.21.0 +geopy==2.1.0 # homeassistant.components.geo_rss_events georss_generic_client==0.4 @@ -360,16 +366,13 @@ georss_qld_bushfire_alert_client==0.3 getmac==0.8.2 # homeassistant.components.gios -gios==0.2.1 +gios==1.0.1 # homeassistant.components.glances glances_api==0.2.0 # homeassistant.components.goalzero -goalzero==0.1.4 - -# homeassistant.components.gogogate2 -gogogate2-api==3.0.0 +goalzero==0.1.7 # homeassistant.components.google google-api-python-client==1.6.4 @@ -386,6 +389,9 @@ googlemaps==2.5.1 # homeassistant.components.gree greeclimate==0.11.4 +# homeassistant.components.growatt_server +growattServer==1.0.1 + # homeassistant.components.profiler guppy3==3.1.0 @@ -405,7 +411,7 @@ hangups==0.4.11 hass-nabucasa==0.43.0 # homeassistant.components.tasmota -hatasmota==0.2.13 +hatasmota==0.2.14 # homeassistant.components.jewish_calendar hdate==0.10.2 @@ -423,7 +429,7 @@ hole==0.5.1 holidays==0.11.1 # homeassistant.components.frontend -home-assistant-frontend==20210504.0 +home-assistant-frontend==20210601.1 # homeassistant.components.zwave homeassistant-pyozw==0.1.10 @@ -442,7 +448,7 @@ homepluscontrol==0.0.5 httplib2==0.19.0 # homeassistant.components.huawei_lte -huawei-lte-api==1.4.17 +huawei-lte-api==1.4.18 # homeassistant.components.huisbaasje huisbaasje-client==0.1.0 @@ -456,12 +462,18 @@ iaqualink==0.3.4 # homeassistant.components.ping icmplib==2.1.1 +# homeassistant.components.network +ifaddr==0.1.7 + # homeassistant.components.influxdb influxdb-client==1.14.0 # homeassistant.components.influxdb influxdb==5.2.3 +# homeassistant.components.gogogate2 +ismartgate==4.0.0 + # homeassistant.components.rest jsonpath==0.82 @@ -471,6 +483,9 @@ konnected==1.2.0 # homeassistant.components.kostal_plenticore kostal_plenticore==0.2.0 +# homeassistant.components.kraken +krakenex==2.1.0 + # homeassistant.components.dyson libpurecool==0.6.4 @@ -487,10 +502,10 @@ libsoundtouch==0.8 logi_circle==0.2.2 # homeassistant.components.luftdaten -luftdaten==0.6.4 +luftdaten==0.6.5 # homeassistant.components.maxcube -maxcube-api==0.4.2 +maxcube-api==0.4.3 # homeassistant.components.mythicbeastsdns mbddns==0.1.2 @@ -505,7 +520,7 @@ meteofrance-api==1.0.2 mficlient==0.3.0 # homeassistant.components.mill -millheater==0.4.0 +millheater==0.4.1 # homeassistant.components.minio minio==4.0.9 @@ -533,10 +548,13 @@ nessclient==0.9.15 # homeassistant.components.discovery # homeassistant.components.ssdp -netdisco==2.8.2 +netdisco==2.8.3 + +# homeassistant.components.nam +nettigo-air-monitor==0.2.6 # homeassistant.components.nexia -nexia==0.9.6 +nexia==0.9.7 # homeassistant.components.notify_events notify-events==1.0.4 @@ -555,7 +573,7 @@ numato-gpio==0.10.0 # homeassistant.components.opencv # homeassistant.components.tensorflow # homeassistant.components.trend -numpy==1.20.2 +numpy==1.20.3 # homeassistant.components.google oauth2client==4.0.0 @@ -632,7 +650,7 @@ pmsensor==0.4 poolsense==0.0.8 # homeassistant.components.reddit -praw==7.1.4 +praw==7.2.0 # homeassistant.components.islamic_prayer_times prayer_times_calculator==0.0.3 @@ -678,7 +696,7 @@ pyMetno==0.8.3 pyRFXtrx==0.26.1 # homeassistant.components.tibber -pyTibber==0.16.2 +pyTibber==0.17.0 # homeassistant.components.nextbus py_nextbusnext==0.1.4 @@ -702,7 +720,7 @@ pyarlo==0.2.4 pyatag==0.3.5.3 # homeassistant.components.netatmo -pyatmo==4.2.3 +pyatmo==5.0.1 # homeassistant.components.apple_tv pyatv==0.7.7 @@ -784,13 +802,13 @@ pyhaversion==21.5.0 pyheos==0.7.2 # homeassistant.components.hive -pyhiveapi==0.4.1 +pyhiveapi==0.4.2 # homeassistant.components.homematic pyhomematic==0.1.72 # homeassistant.components.ialarm -pyialarm==1.5 +pyialarm==1.7 # homeassistant.components.icloud pyicloud==0.10.2 @@ -805,10 +823,10 @@ pyipma==2.0.5 pyipp==0.11.0 # homeassistant.components.iqvia -pyiqvia==0.3.1 +pyiqvia==1.0.0 # homeassistant.components.isy994 -pyisy==2.1.1 +pyisy==3.0.0 # homeassistant.components.kira pykira==0.1.1 @@ -819,6 +837,9 @@ pykmtronic==0.3.0 # homeassistant.components.kodi pykodi==0.2.5 +# homeassistant.components.kraken +pykrakenapi==0.1.8 + # homeassistant.components.kulersky pykulersky==0.5.2 @@ -835,7 +856,7 @@ pylitejet==0.3.0 pylitterbot==2021.3.1 # homeassistant.components.lutron_caseta -pylutron-caseta==0.9.0 +pylutron-caseta==0.10.0 # homeassistant.components.mailgun pymailgunner==1.4 @@ -844,10 +865,13 @@ pymailgunner==1.4 pymata-express==1.19 # homeassistant.components.mazda -pymazda==0.0.9 +pymazda==0.1.6 # homeassistant.components.melcloud -pymelcloud==2.5.2 +pymelcloud==2.5.3 + +# homeassistant.components.meteoclimatic +pymeteoclimatic==0.0.6 # homeassistant.components.somfy pymfy==0.9.3 @@ -856,7 +880,7 @@ pymfy==0.9.3 pymochad==0.2.0 # homeassistant.components.modbus -pymodbus==2.5.1 +pymodbus==2.5.2 # homeassistant.components.monoprice pymonoprice==0.3 @@ -924,10 +948,7 @@ pyqwikswitch==0.93 pyrisco==0.3.1 # homeassistant.components.rituals_perfume_genie -pyrituals==0.0.2 - -# homeassistant.components.zeroconf -pyroute2==0.5.18 +pyrituals==0.0.3 # homeassistant.components.ruckus_unleashed pyruckus==0.12 @@ -940,6 +961,9 @@ pyserial-asyncio==0.5 # homeassistant.components.zha pyserial==3.5 +# homeassistant.components.sia +pysiaalarm==3.0.0 + # homeassistant.components.signal_messenger pysignalclirestapi==0.3.4 @@ -959,7 +983,7 @@ pysmartthings==0.7.6 pysoma==0.0.10 # homeassistant.components.sonos -pysonos==0.0.47 +pysonos==0.0.49 # homeassistant.components.spc pyspcwebgw==0.4.0 @@ -968,7 +992,7 @@ pyspcwebgw==0.4.0 pysqueezebox==0.5.5 # homeassistant.components.syncthru -pysyncthru==0.7.0 +pysyncthru==0.7.3 # homeassistant.components.ecobee python-ecobee-api==0.2.11 @@ -980,7 +1004,7 @@ python-forecastio==1.4.0 python-izone==1.1.4 # homeassistant.components.juicenet -python-juicenet==1.0.1 +python-juicenet==1.0.2 # homeassistant.components.xiaomi_miio python-miio==0.5.6 @@ -995,7 +1019,7 @@ python-openzwave-mqtt[mqtt-client]==1.4.0 python-picnic-api==1.1.0 # homeassistant.components.smarttub -python-smarttub==0.0.24 +python-smarttub==0.0.25 # homeassistant.components.songpal python-songpal==0.12 @@ -1025,7 +1049,7 @@ pytradfri[async]==7.0.6 pyvera==0.3.13 # homeassistant.components.vesync -pyvesync==1.3.1 +pyvesync==1.4.0 # homeassistant.components.vizio pyvizio==0.1.57 @@ -1040,7 +1064,7 @@ pywebpush==1.9.2 pywemo==0.6.3 # homeassistant.components.wilight -pywilight==0.0.68 +pywilight==0.0.70 # homeassistant.components.zerproc pyzerproc==0.4.8 @@ -1067,7 +1091,7 @@ rokuecp==0.8.1 roombapy==1.6.3 # homeassistant.components.roon -roonapi==0.0.36 +roonapi==0.0.37 # homeassistant.components.rpi_power rpi-bad-power==0.1.0 @@ -1092,7 +1116,7 @@ screenlogicpy==0.4.1 sense_energy==0.9.0 # homeassistant.components.sentry -sentry-sdk==1.0.0 +sentry-sdk==1.1.0 # homeassistant.components.sharkiq sharkiqpy==0.1.8 @@ -1101,7 +1125,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==9.6.10 +simplisafe-python==10.0.0 # homeassistant.components.slack slackclient==2.5.0 @@ -1116,7 +1140,7 @@ smart-meter-texas==0.4.0 smarthab==0.21 # homeassistant.components.smhi -smhi-pkg==1.0.13 +smhi-pkg==1.0.15 # homeassistant.components.solaredge solaredge==0.0.2 @@ -1173,11 +1197,14 @@ surepy==0.6.0 # homeassistant.components.synology_dsm synologydsm-api==1.0.2 +# homeassistant.components.system_bridge +systembridge==1.1.5 + # homeassistant.components.tellduslive tellduslive==0.10.11 # homeassistant.components.powerwall -tesla-powerwall==0.3.5 +tesla-powerwall==0.3.10 # homeassistant.components.tesla teslajsonpy==0.18.3 @@ -1207,7 +1234,7 @@ twinkly-client==0.0.2 upb_lib==0.4.12 # homeassistant.components.upcloud -upcloud-api==1.0.1 +upcloud-api==2.0.0 # homeassistant.components.huawei_lte # homeassistant.components.syncthru @@ -1216,6 +1243,9 @@ url-normalize==1.4.1 # homeassistant.components.uvc uvcclient==0.11.0 +# homeassistant.components.venstar +venstarcolortouch==0.14 + # homeassistant.components.vilfo vilfo-api-client==0.3.2 @@ -1225,11 +1255,15 @@ vsure==1.7.3 # homeassistant.components.vultr vultr==0.1.2 +# homeassistant.components.samsungtv # homeassistant.components.wake_on_lan wakeonlan==2.0.1 +# homeassistant.components.wallbox +wallbox==0.4.4 + # homeassistant.components.folder_watcher -watchdog==2.0.3 +watchdog==2.1.2 # homeassistant.components.wiffi wiffi==1.0.1 @@ -1247,10 +1281,9 @@ wolf_smartset==0.1.8 xbox-webapi==2.0.11 # homeassistant.components.knx -xknx==0.18.1 +xknx==0.18.2 # homeassistant.components.bluesound -# homeassistant.components.fritz # homeassistant.components.rest # homeassistant.components.startca # homeassistant.components.ted5000 @@ -1261,13 +1294,13 @@ xmltodict==0.12.0 yalexs==1.1.11 # homeassistant.components.yeelight -yeelight==0.6.2 +yeelight==0.6.3 # homeassistant.components.onvif zeep[async]==4.0.0 # homeassistant.components.zeroconf -zeroconf==0.29.0 +zeroconf==0.31.0 # homeassistant.components.zha zha-quirks==0.0.57 @@ -1285,10 +1318,10 @@ zigpy-xbee==0.13.0 zigpy-zigate==0.7.3 # homeassistant.components.zha -zigpy-znp==0.4.0 +zigpy-znp==0.5.1 # homeassistant.components.zha zigpy==0.33.0 # homeassistant.components.zwave_js -zwave-js-server-python==0.24.0 +zwave-js-server-python==0.26.0 diff --git a/requirements_test_pre_commit.txt b/requirements_test_pre_commit.txt index 5d646cc81f3..e403e05fcfd 100644 --- a/requirements_test_pre_commit.txt +++ b/requirements_test_pre_commit.txt @@ -1,16 +1,16 @@ # Automatically generated from .pre-commit-config.yaml by gen_requirements_all.py, do not edit bandit==1.7.0 -black==21.4b0 +black==21.5b1 codespell==2.0.0 -flake8-comprehensions==3.4.0 +flake8-comprehensions==3.5.0 flake8-docstrings==1.6.0 flake8-noqa==1.1.0 -flake8==3.9.1 +flake8==3.9.2 isort==5.8.0 mccabe==0.6.1 pycodestyle==2.7.0 pydocstyle==6.0.0 pyflakes==2.3.1 -pyupgrade==2.12.0 -yamllint==1.24.2 +pyupgrade==2.16.0 +yamllint==1.26.1 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 23d13ee9de9..4fd96cb1b04 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -27,7 +27,6 @@ COMMENT_REQUIREMENTS = ( "face_recognition", "i2csense", "opencv-python-headless", - "py_noaa", "pybluez", "pycups", "PySwitchbot", diff --git a/script/hassfest/coverage.py b/script/hassfest/coverage.py index 06e38902060..1a4b1fbf8ba 100644 --- a/script/hassfest/coverage.py +++ b/script/hassfest/coverage.py @@ -51,7 +51,6 @@ ALLOWED_IGNORE_VIOLATIONS = { ("sense", "config_flow.py"), ("sms", "config_flow.py"), ("solarlog", "config_flow.py"), - ("somfy", "config_flow.py"), ("sonos", "config_flow.py"), ("speedtestdotnet", "config_flow.py"), ("spider", "config_flow.py"), diff --git a/script/hassfest/dependencies.py b/script/hassfest/dependencies.py index cb7458af154..1df09d6f0d5 100644 --- a/script/hassfest/dependencies.py +++ b/script/hassfest/dependencies.py @@ -136,6 +136,8 @@ IGNORE_VIOLATIONS = { ("demo", "openalpr_local"), # Migration wizard from zwave to ozw. "ozw", + # Migration of settings from zeroconf to network + ("network", "zeroconf"), # This should become a helper method that integrations can submit data to ("websocket_api", "lovelace"), ("websocket_api", "shopping_list"), diff --git a/script/hassfest/manifest.py b/script/hassfest/manifest.py index 016e3a0a322..a8e1858cad3 100644 --- a/script/hassfest/manifest.py +++ b/script/hassfest/manifest.py @@ -4,11 +4,14 @@ from __future__ import annotations from pathlib import Path from urllib.parse import urlparse +from awesomeversion import ( + AwesomeVersion, + AwesomeVersionException, + AwesomeVersionStrategy, +) import voluptuous as vol from voluptuous.humanize import humanize_error -from homeassistant.loader import validate_custom_integration_version - from .model import Config, Integration DOCUMENTATION_URL_SCHEMA = "https" @@ -142,10 +145,19 @@ def verify_uppercase(value: str): def verify_version(value: str): """Verify the version.""" - if not validate_custom_integration_version(value): - raise vol.Invalid( - f"'{value}' is not a valid version. This will cause a future version of Home Assistant to block this integration.", + try: + AwesomeVersion( + value, + [ + AwesomeVersionStrategy.CALVER, + AwesomeVersionStrategy.SEMVER, + AwesomeVersionStrategy.SIMPLEVER, + AwesomeVersionStrategy.BUILDVER, + AwesomeVersionStrategy.PEP440, + ], ) + except AwesomeVersionException: + raise vol.Invalid(f"'{value}' is not a valid version.") return value @@ -221,10 +233,7 @@ def validate_version(integration: Integration): Will be removed when the version key is no longer optional for custom integrations. """ if not integration.manifest.get("version"): - integration.add_error( - "manifest", - "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration.", - ) + integration.add_error("manifest", "No 'version' key in the manifest file.") return diff --git a/script/hassfest/model.py b/script/hassfest/model.py index 10bc10626a2..59d75be5c4a 100644 --- a/script/hassfest/model.py +++ b/script/hassfest/model.py @@ -83,7 +83,7 @@ class Integration: @property def disabled(self) -> str | None: - """List of disabled.""" + """Return if integration is disabled.""" return self.manifest.get("disabled") @property diff --git a/script/hassfest/mypy_config.py b/script/hassfest/mypy_config.py index 45fa1eb6539..6310d0117c5 100644 --- a/script/hassfest/mypy_config.py +++ b/script/hassfest/mypy_config.py @@ -3,6 +3,8 @@ from __future__ import annotations import configparser import io +import os +from pathlib import Path from typing import Final from .model import Config, Integration @@ -14,7 +16,6 @@ from .model import Config, Integration IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.adguard.*", "homeassistant.components.aemet.*", - "homeassistant.components.airly.*", "homeassistant.components.alarmdecoder.*", "homeassistant.components.alexa.*", "homeassistant.components.almond.*", @@ -24,15 +25,11 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.atag.*", "homeassistant.components.aurora.*", "homeassistant.components.awair.*", - "homeassistant.components.axis.*", "homeassistant.components.azure_devops.*", "homeassistant.components.azure_event_hub.*", "homeassistant.components.blueprint.*", - "homeassistant.components.bluetooth_tracker.*", "homeassistant.components.bmw_connected_drive.*", "homeassistant.components.bsblan.*", - "homeassistant.components.camera.*", - "homeassistant.components.canary.*", "homeassistant.components.cast.*", "homeassistant.components.cert_expiry.*", "homeassistant.components.climacell.*", @@ -45,7 +42,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.deconz.*", "homeassistant.components.demo.*", "homeassistant.components.denonavr.*", - "homeassistant.components.device_tracker.*", "homeassistant.components.devolo_home_control.*", "homeassistant.components.dhcp.*", "homeassistant.components.directv.*", @@ -54,7 +50,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.dynalite.*", "homeassistant.components.eafm.*", "homeassistant.components.edl21.*", - "homeassistant.components.elgato.*", "homeassistant.components.elkm1.*", "homeassistant.components.emonitor.*", "homeassistant.components.enphase_envoy.*", @@ -66,16 +61,12 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.fints.*", "homeassistant.components.fireservicerota.*", "homeassistant.components.firmata.*", - "homeassistant.components.fitbit.*", "homeassistant.components.flo.*", "homeassistant.components.fortios.*", "homeassistant.components.foscam.*", "homeassistant.components.freebox.*", - "homeassistant.components.fritz.*", - "homeassistant.components.fritzbox.*", "homeassistant.components.garmin_connect.*", "homeassistant.components.geniushub.*", - "homeassistant.components.gios.*", "homeassistant.components.glances.*", "homeassistant.components.gogogate2.*", "homeassistant.components.google_assistant.*", @@ -99,12 +90,10 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.homekit_controller.*", "homeassistant.components.homematicip_cloud.*", "homeassistant.components.honeywell.*", - "homeassistant.components.hue.*", "homeassistant.components.huisbaasje.*", "homeassistant.components.humidifier.*", "homeassistant.components.iaqualink.*", "homeassistant.components.icloud.*", - "homeassistant.components.ihc.*", "homeassistant.components.image.*", "homeassistant.components.incomfort.*", "homeassistant.components.influxdb.*", @@ -135,13 +124,10 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.metoffice.*", "homeassistant.components.minecraft_server.*", "homeassistant.components.mobile_app.*", - "homeassistant.components.modbus.*", "homeassistant.components.motion_blinds.*", - "homeassistant.components.motioneye.*", "homeassistant.components.mqtt.*", "homeassistant.components.mullvad.*", "homeassistant.components.mysensors.*", - "homeassistant.components.n26.*", "homeassistant.components.neato.*", "homeassistant.components.ness_alarm.*", "homeassistant.components.nest.*", @@ -159,7 +145,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.omnilogic.*", "homeassistant.components.onboarding.*", "homeassistant.components.ondilo_ico.*", - "homeassistant.components.onewire.*", "homeassistant.components.onvif.*", "homeassistant.components.ovo_energy.*", "homeassistant.components.ozw.*", @@ -181,7 +166,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.recorder.*", "homeassistant.components.reddit.*", "homeassistant.components.ring.*", - "homeassistant.components.rituals_perfume_genie.*", "homeassistant.components.roku.*", "homeassistant.components.rpi_power.*", "homeassistant.components.ruckus_unleashed.*", @@ -190,17 +174,13 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.script.*", "homeassistant.components.search.*", "homeassistant.components.sense.*", - "homeassistant.components.sentry.*", "homeassistant.components.sesame.*", "homeassistant.components.sharkiq.*", - "homeassistant.components.shell_command.*", - "homeassistant.components.shelly.*", "homeassistant.components.sma.*", "homeassistant.components.smart_meter_texas.*", "homeassistant.components.smartthings.*", "homeassistant.components.smarttub.*", "homeassistant.components.smarty.*", - "homeassistant.components.smhi.*", "homeassistant.components.solaredge.*", "homeassistant.components.solarlog.*", "homeassistant.components.somfy.*", @@ -209,18 +189,15 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.songpal.*", "homeassistant.components.sonos.*", "homeassistant.components.spotify.*", - "homeassistant.components.stream.*", "homeassistant.components.stt.*", "homeassistant.components.surepetcare.*", "homeassistant.components.switchbot.*", "homeassistant.components.switcher_kis.*", - "homeassistant.components.synology_dsm.*", "homeassistant.components.synology_srm.*", "homeassistant.components.system_health.*", "homeassistant.components.system_log.*", "homeassistant.components.tado.*", "homeassistant.components.tasmota.*", - "homeassistant.components.tcp.*", "homeassistant.components.telegram_bot.*", "homeassistant.components.template.*", "homeassistant.components.tesla.*", @@ -231,9 +208,7 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.trace.*", "homeassistant.components.tradfri.*", "homeassistant.components.tuya.*", - "homeassistant.components.twentemilieu.*", "homeassistant.components.unifi.*", - "homeassistant.components.upcloud.*", "homeassistant.components.updater.*", "homeassistant.components.upnp.*", "homeassistant.components.velbus.*", @@ -245,7 +220,6 @@ IGNORED_MODULES: Final[list[str]] = [ "homeassistant.components.wemo.*", "homeassistant.components.wink.*", "homeassistant.components.withings.*", - "homeassistant.components.wled.*", "homeassistant.components.wunderground.*", "homeassistant.components.xbox.*", "homeassistant.components.xiaomi_aqara.*", @@ -268,10 +242,13 @@ GENERAL_SETTINGS: Final[dict[str, str]] = { "python_version": "3.8", "show_error_codes": "true", "follow_imports": "silent", + # Enable some checks globally. "ignore_missing_imports": "true", + "strict_equality": "true", "warn_incomplete_stub": "true", "warn_redundant_casts": "true", "warn_unused_configs": "true", + "warn_unused_ignores": "true", } # This is basically the list of checks which is enabled for "strict=true". @@ -284,10 +261,8 @@ STRICT_SETTINGS: Final[list[str]] = [ "disallow_untyped_decorators", "disallow_untyped_defs", "no_implicit_optional", - "strict_equality", "warn_return_any", "warn_unreachable", - "warn_unused_ignores", # TODO: turn these on, address issues # "disallow_any_generics", # "no_implicit_reexport", @@ -319,7 +294,29 @@ def generate_and_validate(config: Config) -> str: "mypy_config", f"Only components should be added: {module}" ) if module in ignored_modules_set: - config.add_error("mypy_config", f"Module '{module}' is in ignored list") + config.add_error( + "mypy_config", f"Module '{module}' is in ignored list in mypy_config.py" + ) + + # Validate that all modules exist. + all_modules = strict_modules + IGNORED_MODULES + for module in all_modules: + if module.endswith(".*"): + module_path = Path(module[:-2].replace(".", os.path.sep)) + if not module_path.is_dir(): + config.add_error("mypy_config", f"Module '{module} is not a folder") + else: + module = module.replace(".", os.path.sep) + module_path = Path(f"{module}.py") + if module_path.is_file(): + continue + module_path = Path(module) / "__init__.py" + if not module_path.is_file(): + config.add_error("mypy_config", f"Module '{module} doesn't exist") + + # Don't generate mypy.ini if there're errors found because it will likely crash. + if any(err.plugin == "mypy_config" for err in config.errors): + return "" mypy_config = configparser.ConfigParser() @@ -336,14 +333,22 @@ def generate_and_validate(config: Config) -> str: for key in STRICT_SETTINGS: mypy_config.set(components_section, key, "false") - strict_section = "mypy-" + ",".join(strict_modules) - mypy_config.add_section(strict_section) - for key in STRICT_SETTINGS: - mypy_config.set(strict_section, key, "true") + for strict_module in strict_modules: + strict_section = f"mypy-{strict_module}" + mypy_config.add_section(strict_section) + for key in STRICT_SETTINGS: + mypy_config.set(strict_section, key, "true") - ignored_section = "mypy-" + ",".join(IGNORED_MODULES) - mypy_config.add_section(ignored_section) - mypy_config.set(ignored_section, "ignore_errors", "true") + # Disable strict checks for tests + tests_section = "mypy-tests.*" + mypy_config.add_section(tests_section) + for key in STRICT_SETTINGS: + mypy_config.set(tests_section, key, "false") + + for ignored_module in IGNORED_MODULES: + ignored_section = f"mypy-{ignored_module}" + mypy_config.add_section(ignored_section) + mypy_config.set(ignored_section, "ignore_errors", "true") with io.StringIO() as fp: mypy_config.write(fp) @@ -356,6 +361,9 @@ def validate(integrations: dict[str, Integration], config: Config) -> None: config_path = config.root / "mypy.ini" config.cache["mypy_config"] = content = generate_and_validate(config) + if any(err.plugin == "mypy_config" for err in config.errors): + return + with open(str(config_path)) as fp: if fp.read().strip() != content: config.add_error( diff --git a/script/hassfest/requirements.py b/script/hassfest/requirements.py index fa5a36c6559..5927824b21f 100644 --- a/script/hassfest/requirements.py +++ b/script/hassfest/requirements.py @@ -95,6 +95,9 @@ def validate_requirements(integration: Integration): integration_requirements.add(req) integration_packages.add(package) + if integration.disabled: + return + install_ok = install_requirements(integration, integration_requirements) if not install_ok: diff --git a/script/hassfest/services.py b/script/hassfest/services.py index 9c28962fbad..a5d10f8dda5 100644 --- a/script/hassfest/services.py +++ b/script/hassfest/services.py @@ -65,25 +65,23 @@ def grep_dir(path: pathlib.Path, glob_pattern: str, search_pattern: str) -> bool def validate_services(integration: Integration): """Validate services.""" - # Find if integration uses services - has_services = grep_dir( - integration.path, - "**/*.py", - r"(hass\.services\.(register|async_register))|async_register_entity_service|async_register_admin_service", - ) - - if not has_services: - return - try: data = load_yaml(str(integration.path / "services.yaml")) except FileNotFoundError: - integration.add_error("services", "Registers services but has no services.yaml") + # Find if integration uses services + has_services = grep_dir( + integration.path, + "**/*.py", + r"(hass\.services\.(register|async_register))|async_register_entity_service|async_register_admin_service", + ) + + if has_services: + integration.add_error( + "services", "Registers services but has no services.yaml" + ) return except HomeAssistantError: - integration.add_error( - "services", "Registers services but unable to load services.yaml" - ) + integration.add_error("services", "Unable to load services.yaml") return try: diff --git a/script/scaffold/templates/config_flow/integration/config_flow.py b/script/scaffold/templates/config_flow/integration/config_flow.py index 02c00be8e2a..f88390599e7 100644 --- a/script/scaffold/templates/config_flow/integration/config_flow.py +++ b/script/scaffold/templates/config_flow/integration/config_flow.py @@ -8,7 +8,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import FlowResultDict +from homeassistant.data_entry_flow import FlowResult from homeassistant.exceptions import HomeAssistantError from .const import DOMAIN @@ -65,12 +65,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for NEW_NAME.""" VERSION = 1 - # TODO pick one of the available connection classes in homeassistant/config_entries.py - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN async def async_step_user( self, user_input: dict[str, Any] | None = None - ) -> FlowResultDict: + ) -> FlowResult: """Handle the initial step.""" if user_input is None: return self.async_show_form( diff --git a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py index 4d2ee2c9f6b..4e1b70a51f3 100644 --- a/script/scaffold/templates/config_flow_discovery/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_discovery/integration/config_flow.py @@ -1,7 +1,6 @@ """Config flow for NEW_NAME.""" import my_pypi_dependency -from homeassistant import config_entries from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_flow @@ -15,6 +14,4 @@ async def _async_has_devices(hass: HomeAssistant) -> bool: return len(devices) > 0 -config_entry_flow.register_discovery_flow( - DOMAIN, "NEW_NAME", _async_has_devices, config_entries.CONN_CLASS_UNKNOWN -) +config_entry_flow.register_discovery_flow(DOMAIN, "NEW_NAME", _async_has_devices) diff --git a/script/scaffold/templates/config_flow_oauth2/integration/api.py b/script/scaffold/templates/config_flow_oauth2/integration/api.py index 4f15099c8e1..9ae65bb4d85 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/api.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/api.py @@ -9,7 +9,7 @@ from homeassistant.helpers import config_entry_oauth2_flow # TODO the following two API examples are based on our suggested best practices # for libraries using OAuth2 with requests or aiohttp. Delete the one you won't use. -# For more info see the docs at . +# For more info see the docs at https://developers.home-assistant.io/docs/api_lib_auth/#oauth2. class ConfigEntryAuth(my_pypi_package.AbstractAuth): diff --git a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py index 8670c8a7b43..a035a65dbb3 100644 --- a/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py +++ b/script/scaffold/templates/config_flow_oauth2/integration/config_flow.py @@ -1,7 +1,6 @@ """Config flow for NEW_NAME.""" import logging -from homeassistant import config_entries from homeassistant.helpers import config_entry_oauth2_flow from .const import DOMAIN @@ -13,8 +12,6 @@ class OAuth2FlowHandler( """Config flow to handle NEW_NAME OAuth2 authentication.""" DOMAIN = DOMAIN - # TODO Pick one from config_entries.CONN_CLASS_* - CONNECTION_CLASS = config_entries.CONN_CLASS_UNKNOWN @property def logger(self) -> logging.Logger: diff --git a/script/version_bump.py b/script/version_bump.py index f3ed5e99c55..5f1988f3c26 100755 --- a/script/version_bump.py +++ b/script/version_bump.py @@ -106,9 +106,15 @@ def write_version(version): major, minor, patch = str(version).split(".", 2) - content = re.sub("MAJOR_VERSION = .*\n", f"MAJOR_VERSION = {major}\n", content) - content = re.sub("MINOR_VERSION = .*\n", f"MINOR_VERSION = {minor}\n", content) - content = re.sub("PATCH_VERSION = .*\n", f'PATCH_VERSION = "{patch}"\n', content) + content = re.sub( + "MAJOR_VERSION: Final = .*\n", f"MAJOR_VERSION: Final = {major}\n", content + ) + content = re.sub( + "MINOR_VERSION: Final = .*\n", f"MINOR_VERSION: Final = {minor}\n", content + ) + content = re.sub( + "PATCH_VERSION: Final = .*\n", f'PATCH_VERSION: Final = "{patch}"\n', content + ) with open("homeassistant/const.py", "wt") as fil: content = fil.write(content) diff --git a/setup.py b/setup.py index 4791b0815f1..0178b201372 100755 --- a/setup.py +++ b/setup.py @@ -35,19 +35,19 @@ REQUIRES = [ "aiohttp==3.7.4.post0", "astral==2.2", "async_timeout==3.0.1", - "attrs==20.3.0", - "awesomeversion==21.2.3", + "attrs==21.2.0", + "awesomeversion==21.4.0", + 'backports.zoneinfo;python_version<"3.9"', "bcrypt==3.1.7", "certifi>=2020.12.5", "ciso8601==2.1.3", "httpx==0.18.0", - "jinja2>=2.11.3", + "jinja2>=3.0.1", "PyJWT==1.7.1", # PyJWT has loose dependency. We want the latest one. "cryptography==3.3.2", "pip>=8.0.3,<20.3", "python-slugify==4.0.1", - "pytz>=2021.1", "pyyaml==5.4.1", "requests==2.25.1", "ruamel.yaml==0.15.100", diff --git a/tests/auth/providers/test_trusted_networks.py b/tests/auth/providers/test_trusted_networks.py index 4ece4875ba4..39764fa4206 100644 --- a/tests/auth/providers/test_trusted_networks.py +++ b/tests/auth/providers/test_trusted_networks.py @@ -296,7 +296,6 @@ async def test_trusted_group_login(manager_with_user, provider_with_user): schema = step["data_schema"] # only user listed - print(user.id) assert schema({"user": user.id}) with pytest.raises(vol.Invalid): assert schema({"user": owner.id}) diff --git a/tests/common.py b/tests/common.py index d63e3859108..03b53294db0 100644 --- a/tests/common.py +++ b/tests/common.py @@ -5,7 +5,7 @@ import asyncio import collections from collections import OrderedDict from contextlib import contextmanager -from datetime import timedelta +from datetime import datetime, timedelta import functools as ft from io import StringIO import json @@ -44,7 +44,7 @@ from homeassistant.const import ( STATE_OFF, STATE_ON, ) -from homeassistant.core import BLOCK_LOG_TIMEOUT, State +from homeassistant.core import BLOCK_LOG_TIMEOUT, HomeAssistant, State from homeassistant.helpers import ( area_registry, device_registry, @@ -270,7 +270,7 @@ async def async_test_home_assistant(loop, load_registries=True): hass.config.latitude = 32.87336 hass.config.longitude = -117.22743 hass.config.elevation = 0 - hass.config.time_zone = date_util.get_time_zone("US/Pacific") + hass.config.time_zone = "US/Pacific" hass.config.units = METRIC_SYSTEM hass.config.media_dirs = {"local": get_test_config_dir("media")} hass.config.skip_pip = True @@ -361,7 +361,9 @@ fire_mqtt_message = threadsafe_callback_factory(async_fire_mqtt_message) @ha.callback -def async_fire_time_changed(hass, datetime_, fire_all=False): +def async_fire_time_changed( + hass: HomeAssistant, datetime_: datetime, fire_all: bool = False +) -> None: """Fire a time changes event.""" hass.bus.async_fire(EVENT_TIME_CHANGED, {"now": date_util.as_utc(datetime_)}) @@ -730,8 +732,8 @@ class MockConfigEntry(config_entries.ConfigEntry): title="Mock Title", state=None, options={}, - system_options={}, - connection_class=config_entries.CONN_CLASS_UNKNOWN, + pref_disable_new_entities=None, + pref_disable_polling=None, unique_id=None, disabled_by=None, reason=None, @@ -741,11 +743,11 @@ class MockConfigEntry(config_entries.ConfigEntry): "entry_id": entry_id or uuid_util.random_uuid_hex(), "domain": domain, "data": data or {}, - "system_options": system_options, + "pref_disable_new_entities": pref_disable_new_entities, + "pref_disable_polling": pref_disable_polling, "options": options, "version": version, "title": title, - "connection_class": connection_class, "unique_id": unique_id, "disabled_by": disabled_by, } diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index 41219f5ccef..5e58695ace6 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.abode import ( SERVICE_TRIGGER_AUTOMATION, ) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform @@ -79,7 +79,7 @@ async def test_invalid_credentials(hass): async def test_raise_config_entry_not_ready_when_offline(hass): - """Config entry state is ENTRY_STATE_SETUP_RETRY when abode is offline.""" + """Config entry state is SETUP_RETRY when abode is offline.""" with patch( "homeassistant.components.abode.Abode", side_effect=AbodeException("any"), @@ -87,6 +87,6 @@ async def test_raise_config_entry_not_ready_when_offline(hass): config_entry = await setup_platform(hass, ALARM_DOMAIN) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert hass.config_entries.flow.async_progress() == [] diff --git a/tests/components/accuweather/__init__.py b/tests/components/accuweather/__init__.py index d78eac4269b..3e0c6c2b875 100644 --- a/tests/components/accuweather/__init__.py +++ b/tests/components/accuweather/__init__.py @@ -1,6 +1,6 @@ """Tests for AccuWeather.""" import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import DOMAIN @@ -40,6 +40,10 @@ async def init_integration( ), patch( "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) diff --git a/tests/components/accuweather/test_config_flow.py b/tests/components/accuweather/test_config_flow.py index 1d9feecda3c..c8f2d3c8c89 100644 --- a/tests/components/accuweather/test_config_flow.py +++ b/tests/components/accuweather/test_config_flow.py @@ -1,6 +1,6 @@ """Define tests for the AccuWeather config flow.""" import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from accuweather import ApiError, InvalidApiKeyError, RequestsExceededError @@ -50,7 +50,7 @@ async def test_api_key_too_short(hass): async def test_invalid_api_key(hass): """Test that errors are shown when API key is invalid.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=InvalidApiKeyError("Invalid API key"), ): @@ -66,7 +66,7 @@ async def test_invalid_api_key(hass): async def test_api_error(hass): """Test API error.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=ApiError("Invalid response from AccuWeather API"), ): @@ -82,7 +82,7 @@ async def test_api_error(hass): async def test_requests_exceeded_error(hass): """Test requests exceeded error.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", side_effect=RequestsExceededError( "The allowed number of requests has been exceeded" ), @@ -100,7 +100,7 @@ async def test_requests_exceeded_error(hass): async def test_integration_already_exists(hass): """Test we only allow a single config flow.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", return_value=json.loads(load_fixture("accuweather/location_data.json")), ): MockConfigEntry( @@ -122,7 +122,7 @@ async def test_integration_already_exists(hass): async def test_create_entry(hass): """Test that the user step works.""" with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", return_value=json.loads(load_fixture("accuweather/location_data.json")), ), patch( "homeassistant.components.accuweather.async_setup_entry", return_value=True @@ -152,15 +152,19 @@ async def test_options_flow(hass): config_entry.add_to_hass(hass) with patch( - "accuweather.AccuWeather._async_get_data", + "homeassistant.components.accuweather.AccuWeather._async_get_data", return_value=json.loads(load_fixture("accuweather/location_data.json")), ), patch( - "accuweather.AccuWeather.async_get_current_conditions", + "homeassistant.components.accuweather.AccuWeather.async_get_current_conditions", return_value=json.loads( load_fixture("accuweather/current_conditions_data.json") ), ), patch( - "accuweather.AccuWeather.async_get_forecast" + "homeassistant.components.accuweather.AccuWeather.async_get_forecast" + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/accuweather/test_init.py b/tests/components/accuweather/test_init.py index bb45e894e74..d6f76f113b3 100644 --- a/tests/components/accuweather/test_init.py +++ b/tests/components/accuweather/test_init.py @@ -6,11 +6,7 @@ from unittest.mock import patch from accuweather import ApiError from homeassistant.components.accuweather.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.util.dt import utcnow @@ -48,7 +44,7 @@ async def test_config_not_ready(hass): ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -56,12 +52,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -69,7 +65,7 @@ async def test_update_interval(hass): """Test correct update interval.""" entry = await init_integration(hass) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED current = json.loads(load_fixture("accuweather/current_conditions_data.json")) future = utcnow() + timedelta(minutes=40) @@ -91,7 +87,7 @@ async def test_update_interval_forecast(hass): """Test correct update interval when forecast is True.""" entry = await init_integration(hass, forecast=True) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED current = json.loads(load_fixture("accuweather/current_conditions_data.json")) forecast = json.loads(load_fixture("accuweather/forecast_data.json")) diff --git a/tests/components/accuweather/test_sensor.py b/tests/components/accuweather/test_sensor.py index a4436445340..482fae696c0 100644 --- a/tests/components/accuweather/test_sensor.py +++ b/tests/components/accuweather/test_sensor.py @@ -1,7 +1,7 @@ """Test sensor of AccuWeather integration.""" from datetime import timedelta import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import ATTRIBUTION, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -13,6 +13,7 @@ from homeassistant.const import ( ATTR_UNIT_OF_MEASUREMENT, CONCENTRATION_PARTS_PER_CUBIC_METER, DEVICE_CLASS_TEMPERATURE, + LENGTH_FEET, LENGTH_METERS, LENGTH_MILLIMETERS, PERCENTAGE, @@ -25,6 +26,7 @@ from homeassistant.const import ( from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from homeassistant.util.unit_system import IMPERIAL_SYSTEM from tests.common import async_fire_time_changed, load_fixture from tests.components.accuweather import init_integration @@ -616,6 +618,10 @@ async def test_availability(hass): return_value=json.loads( load_fixture("accuweather/current_conditions_data.json") ), + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -641,7 +647,11 @@ async def test_manual_update_entity(hass): ) as mock_current, patch( "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, - ) as mock_forecast: + ) as mock_forecast, patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): await hass.services.async_call( "homeassistant", "update_entity", @@ -650,3 +660,16 @@ async def test_manual_update_entity(hass): ) assert mock_current.call_count == 1 assert mock_forecast.call_count == 1 + + +async def test_sensor_imperial_units(hass): + """Test states of the sensor without forecast.""" + hass.config.units = IMPERIAL_SYSTEM + await init_integration(hass) + + state = hass.states.get("sensor.home_cloud_ceiling") + assert state + assert state.state == "10500" + assert state.attributes.get(ATTR_ATTRIBUTION) == ATTRIBUTION + assert state.attributes.get(ATTR_ICON) == "mdi:weather-fog" + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == LENGTH_FEET diff --git a/tests/components/accuweather/test_weather.py b/tests/components/accuweather/test_weather.py index 8190d96e634..b1c87c7d404 100644 --- a/tests/components/accuweather/test_weather.py +++ b/tests/components/accuweather/test_weather.py @@ -1,7 +1,7 @@ """Test weather of AccuWeather integration.""" from datetime import timedelta import json -from unittest.mock import patch +from unittest.mock import PropertyMock, patch from homeassistant.components.accuweather.const import ATTRIBUTION from homeassistant.components.weather import ( @@ -112,6 +112,10 @@ async def test_availability(hass): return_value=json.loads( load_fixture("accuweather/current_conditions_data.json") ), + ), patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, ): async_fire_time_changed(hass, future) await hass.async_block_till_done() @@ -137,7 +141,11 @@ async def test_manual_update_entity(hass): ) as mock_current, patch( "homeassistant.components.accuweather.AccuWeather.async_get_forecast", return_value=forecast, - ) as mock_forecast: + ) as mock_forecast, patch( + "homeassistant.components.accuweather.AccuWeather.requests_remaining", + new_callable=PropertyMock, + return_value=10, + ): await hass.services.async_call( "homeassistant", "update_entity", diff --git a/tests/components/adguard/test_config_flow.py b/tests/components/adguard/test_config_flow.py index 872f9e5807e..75d138f400c 100644 --- a/tests/components/adguard/test_config_flow.py +++ b/tests/components/adguard/test_config_flow.py @@ -2,8 +2,8 @@ import aiohttp from homeassistant import config_entries, data_entry_flow -from homeassistant.components.adguard import config_flow from homeassistant.components.adguard.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -30,9 +30,9 @@ FIXTURE_USER_INPUT = { async def test_show_authenticate_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -49,13 +49,14 @@ async def test_connection_error( exc=aiohttp.ClientError, ) - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - assert result["errors"] == {"base": "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} async def test_full_flow_implementation( @@ -70,21 +71,30 @@ async def test_full_flow_implementation( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.AdGuardHomeFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == FIXTURE_USER_INPUT[CONF_HOST] - assert result["data"][CONF_HOST] == FIXTURE_USER_INPUT[CONF_HOST] - assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] - assert result["data"][CONF_PORT] == FIXTURE_USER_INPUT[CONF_PORT] - assert result["data"][CONF_SSL] == FIXTURE_USER_INPUT[CONF_SSL] - assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] - assert result["data"][CONF_VERIFY_SSL] == FIXTURE_USER_INPUT[CONF_VERIFY_SSL] + assert result + assert result.get("flow_id") + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input=FIXTURE_USER_INPUT + ) + assert result2 + assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == FIXTURE_USER_INPUT[CONF_HOST] + + data = result2.get("data") + assert data + assert data[CONF_HOST] == FIXTURE_USER_INPUT[CONF_HOST] + assert data[CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert data[CONF_PORT] == FIXTURE_USER_INPUT[CONF_PORT] + assert data[CONF_SSL] == FIXTURE_USER_INPUT[CONF_SSL] + assert data[CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert data[CONF_VERIFY_SSL] == FIXTURE_USER_INPUT[CONF_VERIFY_SSL] async def test_integration_already_exists(hass: HomeAssistant) -> None: @@ -98,8 +108,9 @@ async def test_integration_already_exists(hass: HomeAssistant) -> None: data={"host": "mock-adguard", "port": "3000"}, context={"source": config_entries.SOURCE_USER}, ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == "abort" + assert result.get("reason") == "already_configured" async def test_hassio_already_configured(hass: HomeAssistant) -> None: @@ -113,8 +124,9 @@ async def test_hassio_already_configured(hass: HomeAssistant) -> None: data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" async def test_hassio_ignored(hass: HomeAssistant) -> None: @@ -128,11 +140,9 @@ async def test_hassio_ignored(hass: HomeAssistant) -> None: data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": "3000"}, context={"source": config_entries.SOURCE_HASSIO}, ) - - assert "type" in result - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert "reason" in result - assert result["reason"] == "already_configured" + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" async def test_hassio_confirm( @@ -150,19 +160,25 @@ async def test_hassio_confirm( data={"addon": "AdGuard Home Addon", "host": "mock-adguard", "port": 3000}, context={"source": config_entries.SOURCE_HASSIO}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "hassio_confirm" - assert result["description_placeholders"] == {"addon": "AdGuard Home Addon"} + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "AdGuard Home Addon"} - result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "AdGuard Home Addon" - assert result["data"][CONF_HOST] == "mock-adguard" - assert result["data"][CONF_PASSWORD] is None - assert result["data"][CONF_PORT] == 3000 - assert result["data"][CONF_SSL] is False - assert result["data"][CONF_USERNAME] is None - assert result["data"][CONF_VERIFY_SSL] + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + + assert result2 + assert result2.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "AdGuard Home Addon" + + data = result2.get("data") + assert data + assert data[CONF_HOST] == "mock-adguard" + assert data[CONF_PASSWORD] is None + assert data[CONF_PORT] == 3000 + assert data[CONF_SSL] is False + assert data[CONF_USERNAME] is None + assert data[CONF_VERIFY_SSL] async def test_hassio_connection_error( @@ -181,6 +197,7 @@ async def test_hassio_connection_error( result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "hassio_confirm" - assert result["errors"] == {"base": "cannot_connect"} + assert result + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("errors") == {"base": "cannot_connect"} diff --git a/tests/components/advantage_air/test_init.py b/tests/components/advantage_air/test_init.py index 1567b9ee8ad..ca8ecff359e 100644 --- a/tests/components/advantage_air/test_init.py +++ b/tests/components/advantage_air/test_init.py @@ -1,10 +1,6 @@ """Test the Advantage Air Initialization.""" -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from tests.components.advantage_air import ( TEST_SYSTEM_DATA, @@ -22,11 +18,11 @@ async def test_async_setup_entry(hass, aioclient_mock): ) entry = await add_mock_config(hass) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_async_setup_entry_failure(hass, aioclient_mock): @@ -38,4 +34,4 @@ async def test_async_setup_entry_failure(hass, aioclient_mock): ) entry = await add_mock_config(hass) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/aemet/test_config_flow.py b/tests/components/aemet/test_config_flow.py index be01ac9de07..af14c170f40 100644 --- a/tests/components/aemet/test_config_flow.py +++ b/tests/components/aemet/test_config_flow.py @@ -5,8 +5,8 @@ from unittest.mock import MagicMock, patch import requests_mock from homeassistant import data_entry_flow -from homeassistant.components.aemet.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, SOURCE_USER +from homeassistant.components.aemet.const import CONF_STATION_UPDATES, DOMAIN +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.util.dt as dt_util @@ -47,7 +47,7 @@ async def test_form(hass): conf_entries = hass.config_entries.async_entries(DOMAIN) entry = conf_entries[0] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] @@ -58,8 +58,64 @@ async def test_form(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_options(hass): + """Test the form options.""" + + now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") + with patch("homeassistant.util.dt.now", return_value=now), patch( + "homeassistant.util.dt.utcnow", return_value=now + ), requests_mock.mock() as _m: + aemet_requests_mock(_m) + + entry = MockConfigEntry( + domain=DOMAIN, unique_id="40.30403754--3.72935236", data=CONFIG + ) + entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_STATION_UPDATES: False} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + CONF_STATION_UPDATES: False, + } + + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], user_input={CONF_STATION_UPDATES: True} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert entry.options == { + CONF_STATION_UPDATES: True, + } + + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + async def test_form_duplicated_id(hass): - """Test that the options form.""" + """Test setting up duplicated entry.""" now = dt_util.parse_datetime("2021-01-09 12:00:00+00:00") with patch("homeassistant.util.dt.now", return_value=now), patch( diff --git a/tests/components/aemet/test_init.py b/tests/components/aemet/test_init.py index f1c6c48f3f3..b1f452c1b46 100644 --- a/tests/components/aemet/test_init.py +++ b/tests/components/aemet/test_init.py @@ -5,7 +5,7 @@ from unittest.mock import patch import requests_mock from homeassistant.components.aemet.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, CONF_NAME import homeassistant.util.dt as dt_util @@ -37,8 +37,8 @@ async def test_unload_entry(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/airly/test_init.py b/tests/components/airly/test_init.py index c2785d6f3e7..a20ae6ddd1a 100644 --- a/tests/components/airly/test_init.py +++ b/tests/components/airly/test_init.py @@ -1,19 +1,22 @@ """Test init of Airly integration.""" from unittest.mock import patch +import pytest + from homeassistant.components.airly import set_update_interval from homeassistant.components.airly.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.util.dt import utcnow from . import API_POINT_URL -from tests.common import MockConfigEntry, async_fire_time_changed, load_fixture +from tests.common import ( + MockConfigEntry, + async_fire_time_changed, + load_fixture, + mock_device_registry, +) from tests.components.airly import init_integration @@ -45,7 +48,7 @@ async def test_config_not_ready(hass, aioclient_mock): aioclient_mock.get(API_POINT_URL, exc=ConnectionError()) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_config_without_unique_id(hass, aioclient_mock): @@ -64,7 +67,7 @@ async def test_config_without_unique_id(hass, aioclient_mock): aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.unique_id == "123-456" @@ -85,7 +88,7 @@ async def test_config_with_turned_off_station(hass, aioclient_mock): aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_no_station.json")) entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_update_interval(hass, aioclient_mock): @@ -120,7 +123,7 @@ async def test_update_interval(hass, aioclient_mock): assert aioclient_mock.call_count == 1 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED update_interval = set_update_interval(instances, REMAINING_RQUESTS) future = utcnow() + update_interval @@ -157,7 +160,7 @@ async def test_update_interval(hass, aioclient_mock): assert aioclient_mock.call_count == 3 assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED update_interval = set_update_interval(instances, REMAINING_RQUESTS) future = utcnow() + update_interval @@ -174,10 +177,42 @@ async def test_unload_entry(hass, aioclient_mock): entry = await init_integration(hass, aioclient_mock) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +@pytest.mark.parametrize("old_identifier", ((DOMAIN, 123, 456), (DOMAIN, "123", "456"))) +async def test_migrate_device_entry(hass, aioclient_mock, old_identifier): + """Test device_info identifiers migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id="123-456", + data={ + "api_key": "foo", + "latitude": 123, + "longitude": 456, + "name": "Home", + }, + ) + + aioclient_mock.get(API_POINT_URL, text=load_fixture("airly_valid_station.json")) + config_entry.add_to_hass(hass) + + device_reg = mock_device_registry(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={old_identifier} + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + migrated_device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123-456")} + ) + assert device_entry.id == migrated_device_entry.id diff --git a/tests/components/alarm_control_panel/test_device_action.py b/tests/components/alarm_control_panel/test_device_action.py index 2b50f83fd8d..a4ec802f3f5 100644 --- a/tests/components/alarm_control_panel/test_device_action.py +++ b/tests/components/alarm_control_panel/test_device_action.py @@ -116,7 +116,9 @@ async def test_get_actions_arm_night_only(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_action_capabilities(hass, device_reg, entity_reg): +async def test_get_action_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -154,7 +156,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities[action["type"]] -async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): +async def test_get_action_capabilities_arm_code( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -198,7 +202,7 @@ async def test_get_action_capabilities_arm_code(hass, device_reg, entity_reg): assert capabilities == expected_capabilities[action["type"]] -async def test_action(hass): +async def test_action(hass, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/alexa/test_smart_home.py b/tests/components/alexa/test_smart_home.py index ab884745e95..83abe2326d7 100644 --- a/tests/components/alexa/test_smart_home.py +++ b/tests/components/alexa/test_smart_home.py @@ -3264,6 +3264,7 @@ async def test_media_player_eq_modes(hass): eq_capability = get_capability(capabilities, "Alexa.EqualizerController") assert eq_capability is not None + assert eq_capability["properties"]["retrievable"] assert "modes" in eq_capability["configurations"] eq_modes = eq_capability["configurations"]["modes"] diff --git a/tests/components/almond/test_init.py b/tests/components/almond/test_init.py index fb74bdfa7f5..64537aa9465 100644 --- a/tests/components/almond/test_init.py +++ b/tests/components/almond/test_init.py @@ -38,7 +38,7 @@ async def test_set_up_oauth_remote_url(hass, aioclient_mock): ): assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED hass.config.components.add("cloud") with patch("homeassistant.components.almond.ALMOND_SETUP_DELAY", 0), patch( @@ -71,7 +71,7 @@ async def test_set_up_oauth_no_external_url(hass, aioclient_mock): ), patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(mock_create_device.mock_calls) == 0 @@ -90,7 +90,7 @@ async def test_set_up_hassio(hass, aioclient_mock): with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(mock_create_device.mock_calls) == 0 @@ -112,5 +112,5 @@ async def test_set_up_local(hass, aioclient_mock): with patch("pyalmond.WebAlmondAPI.async_create_device") as mock_create_device: assert await async_setup_component(hass, "almond", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(mock_create_device.mock_calls) == 1 diff --git a/tests/components/ambiclimate/test_config_flow.py b/tests/components/ambiclimate/test_config_flow.py index 3a325490064..2da550afd42 100644 --- a/tests/components/ambiclimate/test_config_flow.py +++ b/tests/components/ambiclimate/test_config_flow.py @@ -2,14 +2,17 @@ from unittest.mock import AsyncMock, patch import ambiclimate +import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.ambiclimate import config_flow from homeassistant.config import async_process_ha_core_config from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.setup import async_setup_component from homeassistant.util import aiohttp +from tests.common import MockConfigEntry + async def init_config_flow(hass): """Init a configuration flow.""" @@ -40,12 +43,15 @@ async def test_abort_if_already_setup(hass): """Test we abort if Ambiclimate is already setup.""" flow = await init_config_flow(hass) - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_user() + MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -103,11 +109,11 @@ async def test_abort_invalid_code(hass): async def test_already_setup(hass): """Test when already setup.""" - config_flow.register_flow_implementation(hass, None, None) - flow = await init_config_flow(hass) - - with patch.object(hass.config_entries, "async_entries", return_value=True): - result = await flow.async_step_user() + MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" diff --git a/tests/components/analytics/test_analytics.py b/tests/components/analytics/test_analytics.py index ea871081f02..ee67a7e3935 100644 --- a/tests/components/analytics/test_analytics.py +++ b/tests/components/analytics/test_analytics.py @@ -132,7 +132,9 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): with patch( "homeassistant.components.hassio.get_supervisor_info", - side_effect=Mock(return_value={"supported": True, "healthy": True}), + side_effect=Mock( + return_value={"supported": True, "healthy": True, "arch": "amd64"} + ), ), patch( "homeassistant.components.hassio.get_os_info", side_effect=Mock(return_value={"board": "blue", "version": "123"}), @@ -157,7 +159,10 @@ async def test_send_base_with_supervisor(hass, caplog, aioclient_mock): assert f"'uuid': '{MOCK_UUID}'" in caplog.text assert f"'version': '{MOCK_VERSION}'" in caplog.text - assert "'supervisor': {'healthy': True, 'supported': True}" in caplog.text + assert ( + "'supervisor': {'healthy': True, 'supported': True, 'arch': 'amd64'}" + in caplog.text + ) assert "'operating_system': {'board': 'blue', 'version': '123'}" in caplog.text assert "'installation_type':" in caplog.text assert "'integration_count':" not in caplog.text @@ -197,6 +202,7 @@ async def test_send_usage_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), @@ -303,6 +309,7 @@ async def test_send_statistics_with_supervisor(hass, caplog, aioclient_mock): return_value={ "healthy": True, "supported": True, + "arch": "amd64", "addons": [{"slug": "test_addon"}], } ), @@ -354,7 +361,7 @@ async def test_reusing_uuid(hass, aioclient_mock): assert analytics.uuid == "NOT_MOCK_UUID" -async def test_custom_integrations(hass, aioclient_mock): +async def test_custom_integrations(hass, aioclient_mock, enable_custom_integrations): """Test sending custom integrations.""" aioclient_mock.post(ANALYTICS_ENDPOINT_URL, status=200) analytics = Analytics(hass) diff --git a/tests/components/asuswrt/test_sensor.py b/tests/components/asuswrt/test_sensor.py index e69af0cd322..87c3fadb978 100644 --- a/tests/components/asuswrt/test_sensor.py +++ b/tests/components/asuswrt/test_sensor.py @@ -36,23 +36,35 @@ CONFIG_DATA = { CONF_MODE: "router", } -MOCK_DEVICES = { - "a1:b1:c1:d1:e1:f1": Device("a1:b1:c1:d1:e1:f1", "192.168.1.2", "Test"), - "a2:b2:c2:d2:e2:f2": Device("a2:b2:c2:d2:e2:f2", "192.168.1.3", "TestTwo"), -} MOCK_BYTES_TOTAL = [60000000000, 50000000000] MOCK_CURRENT_TRANSFER_RATES = [20000000, 10000000] +@pytest.fixture(name="mock_devices") +def mock_devices_fixture(): + """Mock a list of devices.""" + return { + "a1:b1:c1:d1:e1:f1": Device("a1:b1:c1:d1:e1:f1", "192.168.1.2", "Test"), + "a2:b2:c2:d2:e2:f2": Device("a2:b2:c2:d2:e2:f2", "192.168.1.3", "TestTwo"), + } + + @pytest.fixture(name="connect") -def mock_controller_connect(): +def mock_controller_connect(mock_devices): """Mock a successful connection.""" with patch("homeassistant.components.asuswrt.router.AsusWrt") as service_mock: service_mock.return_value.connection.async_connect = AsyncMock() service_mock.return_value.is_connected = True service_mock.return_value.connection.disconnect = Mock() + service_mock.return_value.async_get_nvram = AsyncMock( + return_value={ + "model": "abcd", + "firmver": "efg", + "buildno": "123", + } + ) service_mock.return_value.async_get_connected_devices = AsyncMock( - return_value=MOCK_DEVICES + return_value=mock_devices ) service_mock.return_value.async_get_bytes_total = AsyncMock( return_value=MOCK_BYTES_TOTAL @@ -63,7 +75,7 @@ def mock_controller_connect(): yield service_mock -async def test_sensors(hass, connect): +async def test_sensors(hass, connect, mock_devices): """Test creating an AsusWRT sensor.""" entity_reg = er.async_get(hass) @@ -134,10 +146,11 @@ async def test_sensors(hass, connect): assert hass.states.get(f"{sensor_prefix}_devices_connected").state == "2" # add one device and remove another - MOCK_DEVICES.pop("a1:b1:c1:d1:e1:f1") - MOCK_DEVICES["a3:b3:c3:d3:e3:f3"] = Device( + mock_devices.pop("a1:b1:c1:d1:e1:f1") + mock_devices["a3:b3:c3:d3:e3:f3"] = Device( "a3:b3:c3:d3:e3:f3", "192.168.1.4", "TestThree" ) + async_fire_time_changed(hass, utcnow() + timedelta(seconds=30)) await hass.async_block_till_done() diff --git a/tests/components/atag/test_init.py b/tests/components/atag/test_init.py index 7b7f3c1e33a..59f38ae7bfe 100644 --- a/tests/components/atag/test_init.py +++ b/tests/components/atag/test_init.py @@ -1,7 +1,7 @@ """Tests for the ATAG integration.""" from homeassistant.components.atag import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from . import init_integration, mock_connection @@ -15,7 +15,7 @@ async def test_config_entry_not_ready( """Test configuration entry not ready on library error.""" mock_connection(aioclient_mock, conn_error=True) entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index bc9f0048738..44594239d74 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -9,11 +9,7 @@ from yalexs.exceptions import AugustApiAIOHTTPError from homeassistant import setup from homeassistant.components.august.const import DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_LOCK, @@ -36,7 +32,7 @@ from tests.components.august.mocks import ( async def test_august_is_offline(hass): - """Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline.""" + """Config entry state is SETUP_RETRY when august is offline.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -53,7 +49,7 @@ async def test_august_is_offline(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_unlock_throws_august_api_http_error(hass): @@ -141,7 +137,7 @@ async def test_lock_has_doorsense(hass): async def test_auth_fails(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when auth fails.""" + """Config entry state is SETUP_ERROR when auth fails.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -159,7 +155,7 @@ async def test_auth_fails(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -167,7 +163,7 @@ async def test_auth_fails(hass): async def test_bad_password(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when the password has been changed.""" + """Config entry state is SETUP_ERROR when the password has been changed.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -187,7 +183,7 @@ async def test_bad_password(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -195,7 +191,7 @@ async def test_bad_password(hass): async def test_http_failure(hass): - """Config entry state is ENTRY_STATE_SETUP_RETRY when august is offline.""" + """Config entry state is SETUP_RETRY when august is offline.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -213,13 +209,13 @@ async def test_http_failure(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY assert hass.config_entries.flow.async_progress() == [] async def test_unknown_auth_state(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when august is in an unknown auth state.""" + """Config entry state is SETUP_ERROR when august is in an unknown auth state.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -237,7 +233,7 @@ async def test_unknown_auth_state(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -245,7 +241,7 @@ async def test_unknown_auth_state(hass): async def test_requires_validation_state(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when august requires validation.""" + """Config entry state is SETUP_ERROR when august requires validation.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -265,14 +261,14 @@ async def test_requires_validation_state(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert len(hass.config_entries.flow.async_progress()) == 1 assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" async def test_unknown_auth_http_401(hass): - """Config entry state is ENTRY_STATE_SETUP_ERROR when august gets an http.""" + """Config entry state is SETUP_ERROR when august gets an http.""" config_entry = MockConfigEntry( domain=DOMAIN, @@ -290,7 +286,7 @@ async def test_unknown_auth_http_401(hass): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() @@ -306,7 +302,7 @@ async def test_load_unload(hass): hass, [august_operative_lock, august_inoperative_lock] ) - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index cb6e5b1a12b..a68fa50a093 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -8,7 +8,6 @@ from axis.event_stream import OPERATION_INITIALIZED import pytest import respx -from homeassistant import config_entries from homeassistant.components import axis from homeassistant.components.axis.const import ( CONF_EVENTS, @@ -283,7 +282,6 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION config_entry = MockConfigEntry( domain=AXIS_DOMAIN, data=deepcopy(config), - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, options=deepcopy(options), version=3, unique_id=FORMATTED_MAC, diff --git a/tests/components/binary_sensor/test_device_condition.py b/tests/components/binary_sensor/test_device_condition.py index d8c9e1ca894..5d8673825fc 100644 --- a/tests/components/binary_sensor/test_device_condition.py +++ b/tests/components/binary_sensor/test_device_condition.py @@ -41,7 +41,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected conditions from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -100,7 +100,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -173,7 +173,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/binary_sensor/test_device_trigger.py b/tests/components/binary_sensor/test_device_trigger.py index 9b50d52b785..0e5cbcc1d70 100644 --- a/tests/components/binary_sensor/test_device_trigger.py +++ b/tests/components/binary_sensor/test_device_trigger.py @@ -41,7 +41,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected triggers from a binary_sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -100,7 +100,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for on and off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -184,7 +184,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/blebox/test_config_flow.py b/tests/components/blebox/test_config_flow.py index 965c707d2af..03f5d0b4f2a 100644 --- a/tests/components/blebox/test_config_flow.py +++ b/tests/components/blebox/test_config_flow.py @@ -173,7 +173,7 @@ async def test_async_setup_entry(hass, valid_feature_mock): await hass.async_block_till_done() assert hass.config_entries.async_entries() == [config] - assert config.state == config_entries.ENTRY_STATE_LOADED + assert config.state is config_entries.ConfigEntryState.LOADED async def test_async_remove_entry(hass, valid_feature_mock): @@ -189,4 +189,4 @@ async def test_async_remove_entry(hass, valid_feature_mock): await hass.async_block_till_done() assert hass.config_entries.async_entries() == [] - assert config.state == config_entries.ENTRY_STATE_NOT_LOADED + assert config.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/blebox/test_init.py b/tests/components/blebox/test_init.py index 098c10f2cfc..c0add2696b5 100644 --- a/tests/components/blebox/test_init.py +++ b/tests/components/blebox/test_init.py @@ -5,7 +5,7 @@ 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 homeassistant.config_entries import ConfigEntryState from .conftest import mock_config, patch_product_identify @@ -23,7 +23,7 @@ async def test_setup_failure(hass, caplog): 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 + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_failure_on_connection(hass, caplog): @@ -39,7 +39,7 @@ async def test_setup_failure_on_connection(hass, caplog): 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 + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry(hass): @@ -57,4 +57,4 @@ async def test_unload_config_entry(hass): await hass.async_block_till_done() assert not hass.data.get(DOMAIN) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/blebox/test_light.py b/tests/components/blebox/test_light.py index 6c8c26fe938..a73bba96fba 100644 --- a/tests/components/blebox/test_light.py +++ b/tests/components/blebox/test_light.py @@ -7,21 +7,13 @@ import pytest from homeassistant.components.light import ( ATTR_BRIGHTNESS, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_WHITE_VALUE, -) -from homeassistant.const import ( - ATTR_SUPPORTED_FEATURES, - SERVICE_TURN_OFF, - SERVICE_TURN_ON, - STATE_OFF, - STATE_ON, + ATTR_RGBW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_RGBW, ) +from homeassistant.const import SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_OFF, STATE_ON from homeassistant.helpers import device_registry as dr -from homeassistant.util import color from .conftest import async_setup_entity, mock_feature @@ -59,8 +51,8 @@ async def test_dimmer_init(dimmer, hass, config): state = hass.states.get(entity_id) assert state.name == "dimmerBox-brightness" - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_BRIGHTNESS + color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] + assert color_modes == [COLOR_MODE_BRIGHTNESS] assert state.attributes[ATTR_BRIGHTNESS] == 65 assert state.state == STATE_ON @@ -230,8 +222,8 @@ async def test_wlightbox_s_init(wlightbox_s, hass, config): state = hass.states.get(entity_id) assert state.name == "wLightBoxS-color" - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_BRIGHTNESS + color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] + assert color_modes == [COLOR_MODE_BRIGHTNESS] assert ATTR_BRIGHTNESS not in state.attributes assert state.state == STATE_OFF @@ -330,13 +322,11 @@ async def test_wlightbox_init(wlightbox, hass, config): state = hass.states.get(entity_id) assert state.name == "wLightBox-color" - supported_features = state.attributes[ATTR_SUPPORTED_FEATURES] - assert supported_features & SUPPORT_WHITE_VALUE - assert supported_features & SUPPORT_COLOR + color_modes = state.attributes[ATTR_SUPPORTED_COLOR_MODES] + assert color_modes == [COLOR_MODE_RGBW] - assert ATTR_WHITE_VALUE not in state.attributes - assert ATTR_HS_COLOR not in state.attributes assert ATTR_BRIGHTNESS not in state.attributes + assert ATTR_RGBW_COLOR not in state.attributes assert state.state == STATE_OFF device_registry = dr.async_get(hass) @@ -363,12 +353,11 @@ async def test_wlightbox_update(wlightbox, hass, config): await async_setup_entity(hass, config, entity_id) state = hass.states.get(entity_id) - assert state.attributes[ATTR_HS_COLOR] == (352.32, 100.0) - assert state.attributes[ATTR_WHITE_VALUE] == 0x3A + assert state.attributes[ATTR_RGBW_COLOR] == (0xFA, 0x00, 0x20, 0x3A) assert state.state == STATE_ON -async def test_wlightbox_on_via_just_whiteness(wlightbox, hass, config): +async def test_wlightbox_on_rgbw(wlightbox, hass, config): """Test light on.""" feature_mock, entity_id = wlightbox @@ -385,125 +374,37 @@ async def test_wlightbox_on_via_just_whiteness(wlightbox, hass, config): def turn_on(value): feature_mock.is_on = True - assert value == "f1e2d3c7" + assert value == "c1d2f3c7" feature_mock.white_value = 0xC7 # on - feature_mock.rgbw_hex = "f1e2d3c7" + feature_mock.rgbw_hex = "c1d2f3c7" feature_mock.async_on = AsyncMock(side_effect=turn_on) def apply_white(value, white): - assert value == "f1e2d305" + assert value == "00010203" assert white == 0xC7 - return "f1e2d3c7" + return "000102c7" feature_mock.apply_white = apply_white - feature_mock.sensible_on_value = "f1e2d305" - - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id, ATTR_WHITE_VALUE: 0xC7}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_WHITE_VALUE] == 0xC7 - - assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) - - -async def test_wlightbox_on_via_reset_whiteness(wlightbox, hass, config): - """Test light on.""" - - feature_mock, entity_id = wlightbox - - def initial_update(): - feature_mock.is_on = False - - feature_mock.async_update = AsyncMock(side_effect=initial_update) - await async_setup_entity(hass, config, entity_id) - feature_mock.async_update = AsyncMock() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - def turn_on(value): - feature_mock.is_on = True - feature_mock.white_value = 0x0 - assert value == "f1e2d300" - feature_mock.rgbw_hex = "f1e2d300" - - feature_mock.async_on = AsyncMock(side_effect=turn_on) - - def apply_white(value, white): - assert value == "f1e2d305" - assert white == 0x0 - return "f1e2d300" - - feature_mock.apply_white = apply_white - - feature_mock.sensible_on_value = "f1e2d305" - - await hass.services.async_call( - "light", - SERVICE_TURN_ON, - {"entity_id": entity_id, ATTR_WHITE_VALUE: 0x0}, - blocking=True, - ) - - state = hass.states.get(entity_id) - assert state.state == STATE_ON - assert state.attributes[ATTR_WHITE_VALUE] == 0x0 - assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) - - -async def test_wlightbox_on_via_just_hsl_color(wlightbox, hass, config): - """Test light on.""" - - feature_mock, entity_id = wlightbox - - def initial_update(): - feature_mock.is_on = False - feature_mock.rgbw_hex = "00000000" - - feature_mock.async_update = AsyncMock(side_effect=initial_update) - await async_setup_entity(hass, config, entity_id) - feature_mock.async_update = AsyncMock() - - state = hass.states.get(entity_id) - assert state.state == STATE_OFF - - hs_color = color.color_RGB_to_hs(0xFF, 0xA1, 0xB2) - - def turn_on(value): - feature_mock.is_on = True - assert value == "ffa1b2e4" - feature_mock.white_value = 0xE4 - feature_mock.rgbw_hex = value - - feature_mock.async_on = AsyncMock(side_effect=turn_on) - def apply_color(value, color_value): - assert value == "c1a2e3e4" - assert color_value == "ffa0b1" - return "ffa1b2e4" + assert value == "000102c7" + assert color_value == "c1d2f3" + return "c1d2f3c7" feature_mock.apply_color = apply_color - feature_mock.sensible_on_value = "c1a2e3e4" + feature_mock.sensible_on_value = "00010203" await hass.services.async_call( "light", SERVICE_TURN_ON, - {"entity_id": entity_id, ATTR_HS_COLOR: hs_color}, + {"entity_id": entity_id, ATTR_RGBW_COLOR: (0xC1, 0xD2, 0xF3, 0xC7)}, blocking=True, ) state = hass.states.get(entity_id) - assert state.attributes[ATTR_HS_COLOR] == hs_color - assert state.attributes[ATTR_WHITE_VALUE] == 0xE4 assert state.state == STATE_ON + assert state.attributes[ATTR_RGBW_COLOR] == (0xC1, 0xD2, 0xF3, 0xC7) async def test_wlightbox_on_to_last_color(wlightbox, hass, config): @@ -538,8 +439,7 @@ async def test_wlightbox_on_to_last_color(wlightbox, hass, config): ) state = hass.states.get(entity_id) - assert state.attributes[ATTR_WHITE_VALUE] == 0xE4 - assert state.attributes[ATTR_HS_COLOR] == color.color_RGB_to_hs(0xF1, 0xE2, 0xD3) + assert state.attributes[ATTR_RGBW_COLOR] == (0xF1, 0xE2, 0xD3, 0xE4) assert state.state == STATE_ON @@ -573,8 +473,7 @@ async def test_wlightbox_off(wlightbox, hass, config): ) state = hass.states.get(entity_id) - assert ATTR_WHITE_VALUE not in state.attributes - assert ATTR_HS_COLOR not in state.attributes + assert ATTR_RGBW_COLOR not in state.attributes assert state.state == STATE_OFF diff --git a/tests/components/bmw_connected_drive/test_config_flow.py b/tests/components/bmw_connected_drive/test_config_flow.py index ba3c69fe9c5..6a0bd210387 100644 --- a/tests/components/bmw_connected_drive/test_config_flow.py +++ b/tests/components/bmw_connected_drive/test_config_flow.py @@ -29,9 +29,7 @@ FIXTURE_CONFIG_ENTRY = { CONF_REGION: FIXTURE_USER_INPUT[CONF_REGION], }, "options": {CONF_READ_ONLY: False, CONF_USE_LOCATION: False}, - "system_options": {"disable_new_entities": False}, "source": config_entries.SOURCE_USER, - "connection_class": config_entries.CONN_CLASS_CLOUD_POLL, "unique_id": f"{FIXTURE_USER_INPUT[CONF_REGION]}-{FIXTURE_USER_INPUT[CONF_REGION]}", } diff --git a/tests/components/bond/test_init.py b/tests/components/bond/test_init.py index 0bba04b4d97..4ba105248df 100644 --- a/tests/components/bond/test_init.py +++ b/tests/components/bond/test_init.py @@ -5,11 +5,7 @@ from aiohttp import ClientConnectionError, ClientResponseError from bond_api import DeviceType from homeassistant.components.bond.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -47,7 +43,7 @@ async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): with patch_bond_version(side_effect=ClientConnectionError()): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAssistant): @@ -75,7 +71,7 @@ async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAss await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" # verify hub device is registered correctly @@ -115,7 +111,7 @@ async def test_unload_config_entry(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id not in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_old_identifiers_are_removed(hass: HomeAssistant): @@ -159,7 +155,7 @@ async def test_old_identifiers_are_removed(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" # verify the device info is cleaned up @@ -201,7 +197,7 @@ async def test_smart_by_bond_device_suggested_area(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" device_registry = dr.async_get(hass) @@ -247,7 +243,7 @@ async def test_bridge_device_suggested_area(hass: HomeAssistant): await hass.async_block_till_done() assert config_entry.entry_id in hass.data[DOMAIN] - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert config_entry.unique_id == "test-bond-id" device_registry = dr.async_get(hass) diff --git a/tests/components/bosch_shc/__init__.py b/tests/components/bosch_shc/__init__.py new file mode 100644 index 00000000000..a7ad288fadb --- /dev/null +++ b/tests/components/bosch_shc/__init__.py @@ -0,0 +1 @@ +"""Tests for the Bosch SHC integration.""" diff --git a/tests/components/bosch_shc/test_config_flow.py b/tests/components/bosch_shc/test_config_flow.py new file mode 100644 index 00000000000..c75814aabc3 --- /dev/null +++ b/tests/components/bosch_shc/test_config_flow.py @@ -0,0 +1,625 @@ +"""Test the Bosch SHC config flow.""" +from unittest.mock import PropertyMock, mock_open, patch + +from boschshcpy.exceptions import ( + SHCAuthenticationError, + SHCConnectionError, + SHCRegistrationError, + SHCSessionError, +) +from boschshcpy.information import SHCInformation + +from homeassistant import config_entries, setup +from homeassistant.components.bosch_shc.config_flow import write_tls_asset +from homeassistant.components.bosch_shc.const import CONF_SHC_CERT, CONF_SHC_KEY, DOMAIN + +from tests.common import MockConfigEntry + +MOCK_SETTINGS = { + "name": "Test name", + "device": {"mac": "test-mac", "hostname": "test-host"}, +} +DISCOVERY_INFO = { + "host": "1.1.1.1", + "port": 0, + "hostname": "shc012345.local.", + "type": "_http._tcp.local.", + "name": "Bosch SHC [test-mac]._http._tcp.local.", +} + + +async def test_form_user(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate" + ) as mock_authenticate, patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "shc012345" + assert result3["data"] == { + "host": "1.1.1.1", + "ssl_certificate": hass.config.path(DOMAIN, CONF_SHC_CERT), + "ssl_key": hass.config.path(DOMAIN, CONF_SHC_KEY), + "token": "abc:123", + "hostname": "123", + } + + assert len(mock_authenticate.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_get_info_connection_error(hass): + """Test we handle connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + side_effect=SHCConnectionError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_get_info_exception(hass): + """Test we handle exceptions.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "host": "1.1.1.1", + }, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_form_pairing_error(hass): + """Test we handle pairing error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + side_effect=SHCRegistrationError(""), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "pairing_failed"} + + +async def test_form_user_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCAuthenticationError, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "invalid_auth"} + + +async def test_form_validate_connection_error(hass): + """Test we handle connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCConnectionError, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "cannot_connect"} + + +async def test_form_validate_session_error(hass): + """Test we handle session error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=SHCSessionError(""), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "session_error"} + + +async def test_form_validate_exception(hass): + """Test we handle exception.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + side_effect=Exception, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "form" + assert result3["step_id"] == "credentials" + assert result3["errors"] == {"base": "unknown"} + + +async def test_form_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="bosch_shc", unique_id="test-mac", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "form" + assert result["step_id"] == "confirm_discovery" + assert result["errors"] == {} + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + assert context["title_placeholders"]["name"] == "shc012345" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate", + ), patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "create_entry" + assert result3["title"] == "shc012345" + assert result3["data"] == { + "host": "1.1.1.1", + "ssl_certificate": hass.config.path(DOMAIN, CONF_SHC_CERT), + "ssl_key": hass.config.path(DOMAIN, CONF_SHC_KEY), + "token": "abc:123", + "hostname": "123", + } + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_already_configured(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry( + domain="bosch_shc", unique_id="test-mac", data={"host": "0.0.0.0"} + ) + entry.add_to_hass(hass) + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf_cannot_connect(hass): + """Test we get the form.""" + with patch( + "boschshcpy.session.SHCSession.mdns_info", side_effect=SHCConnectionError + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +async def test_zeroconf_not_bosch_shc(hass): + """Test we filter out non-bosch_shc devices.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"host": "1.1.1.1", "name": "notboschshc"}, + context={"source": config_entries.SOURCE_ZEROCONF}, + ) + assert result["type"] == "abort" + assert result["reason"] == "not_bosch_shc" + + +async def test_reauth(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + mock_config = MockConfigEntry( + domain=DOMAIN, + unique_id="test-mac", + data={ + "host": "1.1.1.1", + "hostname": "test-mac", + "ssl_certificate": "test-cert.pem", + "ssl_key": "test-key.pem", + }, + title="shc012345", + ) + mock_config.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=mock_config.data, + ) + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + "boschshcpy.session.SHCSession.mdns_info", + return_value=SHCInformation, + ), patch( + "boschshcpy.information.SHCInformation.name", + new_callable=PropertyMock, + return_value="shc012345", + ), patch( + "boschshcpy.information.SHCInformation.unique_id", + new_callable=PropertyMock, + return_value="test-mac", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "2.2.2.2"}, + ) + + assert result2["type"] == "form" + assert result2["step_id"] == "credentials" + assert result2["errors"] == {} + + with patch( + "boschshcpy.register_client.SHCRegisterClient.register", + return_value={ + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + }, + ), patch("os.mkdir"), patch("builtins.open"), patch( + "boschshcpy.session.SHCSession.authenticate" + ), patch( + "homeassistant.components.bosch_shc.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + {"password": "test"}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "reauth_successful" + + assert mock_config.data["host"] == "2.2.2.2" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_tls_assets_writer(hass): + """Test we write tls assets to correct location.""" + assets = { + "token": "abc:123", + "cert": b"content_cert", + "key": b"content_key", + } + with patch("os.mkdir"), patch("builtins.open", mock_open()) as mocked_file: + write_tls_asset(hass, CONF_SHC_CERT, assets["cert"]) + mocked_file.assert_called_with(hass.config.path(DOMAIN, CONF_SHC_CERT), "w") + mocked_file().write.assert_called_with("content_cert") + + write_tls_asset(hass, CONF_SHC_KEY, assets["key"]) + mocked_file.assert_called_with(hass.config.path(DOMAIN, CONF_SHC_KEY), "w") + mocked_file().write.assert_called_with("content_key") diff --git a/tests/components/broadlink/test_device.py b/tests/components/broadlink/test_device.py index 8e53fd74c1c..2ee8f7a5218 100644 --- a/tests/components/broadlink/test_device.py +++ b/tests/components/broadlink/test_device.py @@ -5,12 +5,7 @@ import broadlink.exceptions as blke from homeassistant.components.broadlink.const import DOMAIN from homeassistant.components.broadlink.device import get_domains -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers.entity_registry import async_entries_for_device from . import get_device @@ -29,7 +24,7 @@ async def test_device_setup(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state == ConfigEntryState.LOADED assert mock_api.auth.call_count == 1 assert mock_api.get_fwversion.call_count == 1 forward_entries = {c[1][1] for c in mock_forward.mock_calls} @@ -52,7 +47,7 @@ async def test_device_setup_authentication_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_ERROR + assert mock_entry.state == ConfigEntryState.SETUP_ERROR assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 1 @@ -76,7 +71,7 @@ async def test_device_setup_network_timeout(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -95,7 +90,7 @@ async def test_device_setup_os_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -114,7 +109,7 @@ async def test_device_setup_broadlink_exception(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_ERROR + assert mock_entry.state is ConfigEntryState.SETUP_ERROR assert mock_api.auth.call_count == 1 assert mock_forward.call_count == 0 assert mock_init.call_count == 0 @@ -133,7 +128,7 @@ async def test_device_setup_update_network_timeout(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 @@ -156,7 +151,7 @@ async def test_device_setup_update_authorization_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state is ConfigEntryState.LOADED assert mock_api.auth.call_count == 2 assert mock_api.check_sensors.call_count == 2 forward_entries = {c[1][1] for c in mock_forward.mock_calls} @@ -180,7 +175,7 @@ async def test_device_setup_update_authentication_error(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 2 assert mock_api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 @@ -205,7 +200,7 @@ async def test_device_setup_update_broadlink_exception(hass): ) as mock_init: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_entry.state is ConfigEntryState.SETUP_RETRY assert mock_api.auth.call_count == 1 assert mock_api.check_sensors.call_count == 1 assert mock_forward.call_count == 0 @@ -221,7 +216,7 @@ async def test_device_setup_get_fwversion_broadlink_exception(hass): with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: mock_api, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state is ConfigEntryState.LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -237,7 +232,7 @@ async def test_device_setup_get_fwversion_os_error(hass): with patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: _, mock_entry = await device.setup_entry(hass, mock_api=mock_api) - assert mock_entry.state == ENTRY_STATE_LOADED + assert mock_entry.state is ConfigEntryState.LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -279,7 +274,7 @@ async def test_device_unload_works(hass): ) as mock_forward: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED forward_entries = {c[1][1] for c in mock_forward.mock_calls} domains = get_domains(mock_api.type) assert mock_forward.call_count == len(domains) @@ -302,7 +297,7 @@ async def test_device_unload_authentication_error(hass): ) as mock_forward: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED assert mock_forward.call_count == 0 @@ -320,7 +315,7 @@ async def test_device_unload_update_failed(hass): ) as mock_forward: await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED assert mock_forward.call_count == 0 diff --git a/tests/components/broadlink/test_heartbeat.py b/tests/components/broadlink/test_heartbeat.py new file mode 100644 index 00000000000..8e52a562425 --- /dev/null +++ b/tests/components/broadlink/test_heartbeat.py @@ -0,0 +1,108 @@ +"""Tests for Broadlink heartbeats.""" +from unittest.mock import call, patch + +from homeassistant.components.broadlink.heartbeat import BroadlinkHeartbeat +from homeassistant.util import dt + +from . import get_device + +from tests.common import async_fire_time_changed + +DEVICE_PING = "homeassistant.components.broadlink.heartbeat.blk.ping" + + +async def test_heartbeat_trigger_startup(hass): + """Test that the heartbeat is initialized with the first config entry.""" + device = get_device("Office") + + with patch(DEVICE_PING) as mock_ping: + await device.setup_entry(hass) + await hass.async_block_till_done() + + assert mock_ping.call_count == 1 + assert mock_ping.call_args == call(device.host) + + +async def test_heartbeat_ignore_oserror(hass, caplog): + """Test that an OSError is ignored.""" + device = get_device("Office") + + with patch(DEVICE_PING, side_effect=OSError()): + await device.setup_entry(hass) + await hass.async_block_till_done() + + assert "Failed to send heartbeat to" in caplog.text + + +async def test_heartbeat_trigger_right_time(hass): + """Test that the heartbeat is triggered at the right time.""" + device = get_device("Office") + + await device.setup_entry(hass) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL + ) + await hass.async_block_till_done() + + assert mock_ping.call_count == 1 + assert mock_ping.call_args == call(device.host) + + +async def test_heartbeat_do_not_trigger_before_time(hass): + """Test that the heartbeat is not triggered before the time.""" + device = get_device("Office") + + await device.setup_entry(hass) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, + dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL // 2, + ) + await hass.async_block_till_done() + + assert mock_ping.call_count == 0 + + +async def test_heartbeat_unload(hass): + """Test that the heartbeat is deactivated when the last config entry is removed.""" + device = get_device("Office") + + _, mock_entry = await device.setup_entry(hass) + await hass.async_block_till_done() + + await hass.config_entries.async_remove(mock_entry.entry_id) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL + ) + + assert mock_ping.call_count == 0 + + +async def test_heartbeat_do_not_unload(hass): + """Test that the heartbeat is not deactivated until the last config entry is removed.""" + device_a = get_device("Office") + device_b = get_device("Bedroom") + + _, mock_entry_a = await device_a.setup_entry(hass) + await device_b.setup_entry(hass) + await hass.async_block_till_done() + + await hass.config_entries.async_remove(mock_entry_a.entry_id) + await hass.async_block_till_done() + + with patch(DEVICE_PING) as mock_ping: + async_fire_time_changed( + hass, dt.utcnow() + BroadlinkHeartbeat.HEARTBEAT_INTERVAL + ) + await hass.async_block_till_done() + + assert mock_ping.call_count == 1 + assert mock_ping.call_args == call(device_b.host) diff --git a/tests/components/broadlink/test_sensors.py b/tests/components/broadlink/test_sensors.py index e5d31705a4f..5cc75c28a73 100644 --- a/tests/components/broadlink/test_sensors.py +++ b/tests/components/broadlink/test_sensors.py @@ -27,7 +27,7 @@ async def test_a1_sensor_setup(hass): assert mock_api.check_sensors_raw.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 sensors_and_states = { @@ -62,7 +62,7 @@ async def test_a1_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 5 mock_api.check_sensors_raw.return_value = { @@ -104,7 +104,7 @@ async def test_rm_pro_sensor_setup(hass): assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 sensors_and_states = { @@ -127,7 +127,7 @@ async def test_rm_pro_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 mock_api.check_sensors.return_value = {"temperature": 25.8} @@ -159,7 +159,7 @@ async def test_rm_pro_filter_crazy_temperature(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 1 mock_api.check_sensors.return_value = {"temperature": -7} @@ -189,7 +189,7 @@ async def test_rm_mini3_no_sensor(hass): assert mock_api.check_sensors.call_count <= 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 0 @@ -207,7 +207,7 @@ async def test_rm4_pro_hts2_sensor_setup(hass): assert mock_api.check_sensors.call_count == 1 device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 sensors_and_states = { @@ -233,7 +233,7 @@ async def test_rm4_pro_hts2_sensor_update(hass): device_entry = device_registry.async_get_device({(DOMAIN, mock_entry.unique_id)}) entries = async_entries_for_device(entity_registry, device_entry.id) - sensors = {entry for entry in entries if entry.domain == SENSOR_DOMAIN} + sensors = [entry for entry in entries if entry.domain == SENSOR_DOMAIN] assert len(sensors) == 2 mock_api.check_sensors.return_value = {"temperature": 16.8, "humidity": 34.0} diff --git a/tests/components/brother/test_config_flow.py b/tests/components/brother/test_config_flow.py index d681ac9c988..c518076615a 100644 --- a/tests/components/brother/test_config_flow.py +++ b/tests/components/brother/test_config_flow.py @@ -40,8 +40,8 @@ async def test_create_entry_with_hostname(hass): assert result["data"][CONF_TYPE] == CONFIG[CONF_TYPE] -async def test_create_entry_with_ip_address(hass): - """Test that the user step works with printer IP address.""" +async def test_create_entry_with_ipv4_address(hass): + """Test that the user step works with printer IPv4 address.""" with patch( "brother.Brother._get_data", return_value=json.loads(load_fixture("brother_printer_data.json")), @@ -58,6 +58,24 @@ async def test_create_entry_with_ip_address(hass): assert result["data"][CONF_TYPE] == "laser" +async def test_create_entry_with_ipv6_address(hass): + """Test that the user step works with printer IPv6 address.""" + with patch( + "brother.Brother._get_data", + return_value=json.loads(load_fixture("brother_printer_data.json")), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "2001:db8::1428:57ab", CONF_TYPE: "laser"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "HL-L2340DW 0123456789" + assert result["data"][CONF_HOST] == "2001:db8::1428:57ab" + assert result["data"][CONF_TYPE] == "laser" + + async def test_invalid_hostname(hass): """Test invalid hostname in user_input.""" result = await hass.config_entries.flow.async_init( @@ -118,33 +136,6 @@ async def test_device_exists_abort(hass): assert result["reason"] == "already_configured" -async def test_zeroconf_no_data(hass): - """Test we abort if zeroconf provides no data.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF} - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" - - -async def test_zeroconf_not_brother_printer_error(hass): - """Test we abort zeroconf flow if printer isn't Brother.""" - with patch( - "brother.Brother._get_data", - return_value=json.loads(load_fixture("brother_printer_data.json")), - ): - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={"hostname": "example.local.", "name": "Another Printer"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "not_brother_printer" - - async def test_zeroconf_snmp_error(hass): """Test we abort zeroconf flow on SNMP error.""" with patch("brother.Brother._get_data", side_effect=SnmpError("error")): diff --git a/tests/components/brother/test_init.py b/tests/components/brother/test_init.py index 7b85586ce28..76b999c3e54 100644 --- a/tests/components/brother/test_init.py +++ b/tests/components/brother/test_init.py @@ -2,11 +2,7 @@ from unittest.mock import patch from homeassistant.components.brother.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_TYPE, STATE_UNAVAILABLE from tests.common import MockConfigEntry @@ -35,7 +31,7 @@ async def test_config_not_ready(hass): with patch("brother.Brother._get_data", side_effect=ConnectionError()): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -43,10 +39,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/bsblan/test_init.py b/tests/components/bsblan/test_init.py index b6096ced0ac..c7937daf786 100644 --- a/tests/components/bsblan/test_init.py +++ b/tests/components/bsblan/test_init.py @@ -2,7 +2,7 @@ import aiohttp from homeassistant.components.bsblan.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.bsblan import init_integration, init_integration_without_auth @@ -19,7 +19,7 @@ async def test_config_entry_not_ready( ) entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -44,4 +44,4 @@ async def test_config_entry_no_authentication( ) entry = await init_integration_without_auth(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/buienradar/test_camera.py b/tests/components/buienradar/test_camera.py index c9c6d7b4793..3d0c63d972b 100644 --- a/tests/components/buienradar/test_camera.py +++ b/tests/components/buienradar/test_camera.py @@ -1,34 +1,63 @@ """The tests for generic camera component.""" import asyncio from contextlib import suppress +import copy from aiohttp.client_exceptions import ClientResponseError -from homeassistant.const import HTTP_INTERNAL_SERVER_ERROR -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import CONF_COUNTRY, CONF_DELTA, DOMAIN +from homeassistant.const import ( + CONF_LATITUDE, + CONF_LONGITUDE, + HTTP_INTERNAL_SERVER_ERROR, +) +from homeassistant.helpers.entity_registry import async_get from homeassistant.util import dt as dt_util +from tests.common import MockConfigEntry + # An infinitesimally small time-delta. EPSILON_DELTA = 0.0000000001 +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 -def radar_map_url(dim: int = 512, country_code: str = "NL") -> str: - """Build map url, defaulting to 512 wide (as in component).""" - return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w={dim}&h={dim}" +TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} + + +def radar_map_url(country_code: str = "NL") -> str: + """Build map URL.""" + return f"https://api.buienradar.nl/image/1.0/RadarMap{country_code}?w=700&h=700" + + +async def _setup_config_entry(hass, entry): + entity_registry = async_get(hass) + entity_registry.async_get_or_create( + domain="camera", + platform="buienradar", + unique_id=f"{TEST_LATITUDE:2.6f}{TEST_LONGITUDE:2.6f}", + config_entry=entry, + original_name="Buienradar", + ) + await hass.async_block_till_done() + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): """Test that it fetches the given url.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 @@ -38,7 +67,7 @@ async def test_fetching_url_and_caching(aioclient_mock, hass, hass_client): # default delta is 600s -> should be the same when calling immediately # afterwards. - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -46,22 +75,19 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): """Test that the cache expires after delta.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "delta": EPSILON_DELTA, - } - }, + options = {CONF_DELTA: EPSILON_DELTA} + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) - await hass.async_block_till_done() + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert resp.status == 200 assert aioclient_mock.call_count == 1 @@ -70,7 +96,7 @@ async def test_expire_delta(aioclient_mock, hass, hass_client): await asyncio.sleep(EPSILON_DELTA) # tiny delta has passed -> should immediately call again - resp = await client.get("/api/camera_proxy/camera.config_test") + resp = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 @@ -78,15 +104,16 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): """Test that it fetches with only one request at the same time.""" aioclient_mock.get(radar_map_url(), text="hello world") - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp_1 = client.get("/api/camera_proxy/camera.config_test") - resp_2 = client.get("/api/camera_proxy/camera.config_test") + resp_1 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") + resp_2 = client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") resp = await resp_1 resp_2 = await resp_2 @@ -96,44 +123,22 @@ async def test_only_one_fetch_at_a_time(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 1 -async def test_dimension(aioclient_mock, hass, hass_client): - """Test that it actually adheres to the dimension.""" - aioclient_mock.get(radar_map_url(700), text="hello world") - - await async_setup_component( - hass, - "camera", - {"camera": {"name": "config_test", "platform": "buienradar", "dimension": 700}}, - ) - await hass.async_block_till_done() - - client = await hass_client() - - await client.get("/api/camera_proxy/camera.config_test") - - assert aioclient_mock.call_count == 1 - - async def test_belgium_country(aioclient_mock, hass, hass_client): """Test that it actually adheres to another country like Belgium.""" aioclient_mock.get(radar_map_url(country_code="BE"), text="hello world") - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "country_code": "BE", - } - }, - ) - await hass.async_block_till_done() + data = copy.deepcopy(TEST_CFG_DATA) + data[CONF_COUNTRY] = "BE" + + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=data) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -142,15 +147,16 @@ async def test_failure_response_not_cached(aioclient_mock, hass, hass_client): """Test that it does not cache a failure response.""" aioclient_mock.get(radar_map_url(), text="hello world", status=401) - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - await client.get("/api/camera_proxy/camera.config_test") - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 2 @@ -168,22 +174,19 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): headers={"Last-Modified": last_modified}, ) - await async_setup_component( - hass, - "camera", - { - "camera": { - "name": "config_test", - "platform": "buienradar", - "delta": EPSILON_DELTA, - } - }, + options = {CONF_DELTA: EPSILON_DELTA} + + mock_entry = MockConfigEntry( + domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA, options=options ) - await hass.async_block_till_done() + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() - resp_1 = await client.get("/api/camera_proxy/camera.config_test") + resp_1 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") # It is not possible to check if header was sent. assert aioclient_mock.call_count == 1 @@ -197,7 +200,7 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): aioclient_mock.get(radar_map_url(), text=None, status=304) - resp_2 = await client.get("/api/camera_proxy/camera.config_test") + resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 assert (await resp_1.read()) == (await resp_2.read()) @@ -205,10 +208,11 @@ async def test_last_modified_updates(aioclient_mock, hass, hass_client): async def test_retries_after_error(aioclient_mock, hass, hass_client): """Test that it does retry after an error instead of caching.""" - await async_setup_component( - hass, "camera", {"camera": {"name": "config_test", "platform": "buienradar"}} - ) - await hass.async_block_till_done() + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await _setup_config_entry(hass, mock_entry) client = await hass_client() @@ -216,7 +220,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): # A 404 should not return data and throw: with suppress(ClientResponseError): - await client.get("/api/camera_proxy/camera.config_test") + await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 @@ -227,7 +231,7 @@ async def test_retries_after_error(aioclient_mock, hass, hass_client): assert aioclient_mock.call_count == 0 # http error should not be cached, immediate retry. - resp_2 = await client.get("/api/camera_proxy/camera.config_test") + resp_2 = await client.get("/api/camera_proxy/camera.buienradar_51_5288505_400216") assert aioclient_mock.call_count == 1 # Binary text can not be added as body to `aioclient_mock.get(text=...)`, diff --git a/tests/components/buienradar/test_config_flow.py b/tests/components/buienradar/test_config_flow.py new file mode 100644 index 00000000000..b8abefec70a --- /dev/null +++ b/tests/components/buienradar/test_config_flow.py @@ -0,0 +1,131 @@ +"""Test the buienradar2 config flow.""" +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE + +from tests.common import MockConfigEntry + +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 + + +async def test_config_flow_setup_(hass): + """Test setup of camera.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" + assert result["data"] == { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + +async def test_config_flow_already_configured_weather(hass): + """Test already configured.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=f"{TEST_LATITUDE}-{TEST_LONGITUDE}", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_import_camera(hass): + """Test import of camera.""" + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "create_entry" + assert result["title"] == f"{TEST_LATITUDE},{TEST_LONGITUDE}" + assert result["data"] == { + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options_flow(hass): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + result = await hass.config_entries.options.async_init(entry.entry_id) + + assert result["type"] == "form" + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={"country_code": "BE", "delta": 450, "timeframe": 30}, + ) + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ), patch( + "homeassistant.components.buienradar.async_unload_entry", return_value=True + ): + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + await hass.async_block_till_done() + + assert entry.options == {"country_code": "BE", "delta": 450, "timeframe": 30} diff --git a/tests/components/buienradar/test_init.py b/tests/components/buienradar/test_init.py new file mode 100644 index 00000000000..0c25fcc1886 --- /dev/null +++ b/tests/components/buienradar/test_init.py @@ -0,0 +1,121 @@ +"""Tests for the buienradar component.""" +from unittest.mock import patch + +from homeassistant import setup +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.entity_registry import async_get_registry + +from tests.common import MockConfigEntry + +TEST_LATITUDE = 51.5288504 +TEST_LONGITUDE = 5.4002156 + + +async def test_import_all(hass): + """Test import of all platforms.""" + config = { + "weather 1": [{"platform": "buienradar", "name": "test1"}], + "sensor 1": [{"platform": "buienradar", "timeframe": 30, "name": "test2"}], + "camera 1": [ + { + "platform": "buienradar", + "country_code": "BE", + "delta": 300, + "name": "test3", + } + ], + } + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + await setup.async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state is ConfigEntryState.LOADED + assert entry.data == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "timeframe": 30, + "country_code": "BE", + "delta": 300, + "name": "test2", + } + + +async def test_import_camera(hass): + """Test import of camera platform.""" + entity_registry = await async_get_registry(hass) + entity_registry.async_get_or_create( + domain="camera", + platform="buienradar", + unique_id="512_NL", + original_name="test_name", + ) + await hass.async_block_till_done() + + config = { + "camera 1": [{"platform": "buienradar", "country_code": "NL", "dimension": 512}] + } + + with patch( + "homeassistant.components.buienradar.async_setup_entry", return_value=True + ): + await setup.async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + conf_entries = hass.config_entries.async_entries(DOMAIN) + + assert len(conf_entries) == 1 + + entry = conf_entries[0] + + assert entry.state is ConfigEntryState.LOADED + assert entry.data == { + "latitude": hass.config.latitude, + "longitude": hass.config.longitude, + "timeframe": 60, + "country_code": "NL", + "delta": 600, + "name": "Buienradar", + } + + entity_id = entity_registry.async_get_entity_id( + "camera", + "buienradar", + f"{hass.config.latitude:2.6f}{hass.config.longitude:2.6f}", + ) + assert entity_id + entity = entity_registry.async_get(entity_id) + assert entity.original_name == "test_name" + + +async def test_load_unload(aioclient_mock, hass): + """Test options flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_LATITUDE: TEST_LATITUDE, + CONF_LONGITUDE: TEST_LONGITUDE, + }, + unique_id=DOMAIN, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/buienradar/test_sensor.py b/tests/components/buienradar/test_sensor.py index 801f5706a08..2b381f980d7 100644 --- a/tests/components/buienradar/test_sensor.py +++ b/tests/components/buienradar/test_sensor.py @@ -1,26 +1,37 @@ """The tests for the Buienradar sensor platform.""" -from homeassistant.components import sensor -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE +from homeassistant.helpers.entity_registry import async_get + +from tests.common import MockConfigEntry + +TEST_LONGITUDE = 51.5288504 +TEST_LATITUDE = 5.4002156 CONDITIONS = ["stationname", "temperature"] -BASE_CONFIG = { - "sensor": [ - { - "platform": "buienradar", - "name": "volkel", - "latitude": 51.65, - "longitude": 5.7, - "monitored_conditions": CONDITIONS, - } - ] -} +TEST_CFG_DATA = {CONF_LATITUDE: TEST_LATITUDE, CONF_LONGITUDE: TEST_LONGITUDE} -async def test_smoke_test_setup_component(hass): +async def test_smoke_test_setup_component(aioclient_mock, hass): """Smoke test for successfully set-up with default config.""" - assert await async_setup_component(hass, sensor.DOMAIN, BASE_CONFIG) + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + entity_registry = async_get(hass) + for cond in CONDITIONS: + entity_registry.async_get_or_create( + domain="sensor", + platform="buienradar", + unique_id=f"{TEST_LATITUDE:2.6f}{TEST_LONGITUDE:2.6f}{cond}", + config_entry=mock_entry, + original_name=f"Buienradar {cond}", + ) + await hass.async_block_till_done() + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() for cond in CONDITIONS: - state = hass.states.get(f"sensor.volkel_{cond}") + state = hass.states.get(f"sensor.buienradar_5_40021651_528850{cond}") assert state.state == "unknown" diff --git a/tests/components/buienradar/test_weather.py b/tests/components/buienradar/test_weather.py index db0a6ce3984..81fd3f4fcb4 100644 --- a/tests/components/buienradar/test_weather.py +++ b/tests/components/buienradar/test_weather.py @@ -1,25 +1,20 @@ """The tests for the buienradar weather component.""" -from homeassistant.components import weather -from homeassistant.setup import async_setup_component +from homeassistant.components.buienradar.const import DOMAIN +from homeassistant.const import CONF_LATITUDE, CONF_LONGITUDE -# Example config snippet from documentation. -BASE_CONFIG = { - "weather": [ - { - "platform": "buienradar", - "name": "volkel", - "latitude": 51.65, - "longitude": 5.7, - "forecast": True, - } - ] -} +from tests.common import MockConfigEntry + +TEST_CFG_DATA = {CONF_LATITUDE: 51.5288504, CONF_LONGITUDE: 5.4002156} -async def test_smoke_test_setup_component(hass): +async def test_smoke_test_setup_component(aioclient_mock, hass): """Smoke test for successfully set-up with default config.""" - assert await async_setup_component(hass, weather.DOMAIN, BASE_CONFIG) + mock_entry = MockConfigEntry(domain=DOMAIN, unique_id="TEST_ID", data=TEST_CFG_DATA) + + mock_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(mock_entry.entry_id) await hass.async_block_till_done() - state = hass.states.get("weather.volkel") + state = hass.states.get("weather.buienradar") assert state.state == "unknown" diff --git a/tests/components/caldav/test_calendar.py b/tests/components/caldav/test_calendar.py index d8c6a44a3ea..5a5b7adf0e3 100644 --- a/tests/components/caldav/test_calendar.py +++ b/tests/components/caldav/test_calendar.py @@ -401,8 +401,6 @@ async def test_ongoing_floating_event_returned(mock_now, hass, calendar): await hass.async_block_till_done() state = hass.states.get("calendar.private") - print(dt.DEFAULT_TIME_ZONE) - print(state) assert state.name == calendar.name assert state.state == STATE_ON assert dict(state.attributes) == { diff --git a/tests/components/camera/test_init.py b/tests/components/camera/test_init.py index 340a4b5d756..7c7890a3e5f 100644 --- a/tests/components/camera/test_init.py +++ b/tests/components/camera/test_init.py @@ -11,7 +11,12 @@ from homeassistant.components.camera.const import DOMAIN, PREF_PRELOAD_STREAM from homeassistant.components.camera.prefs import CameraEntityPreferences from homeassistant.components.websocket_api.const import TYPE_RESULT from homeassistant.config import async_process_ha_core_config -from homeassistant.const import ATTR_ENTITY_ID, EVENT_HOMEASSISTANT_START +from homeassistant.const import ( + ATTR_ENTITY_ID, + EVENT_HOMEASSISTANT_START, + HTTP_BAD_GATEWAY, + HTTP_OK, +) from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -354,3 +359,19 @@ async def test_record_service(hass, mock_camera, mock_stream): # So long as we call stream.record, the rest should be covered # by those tests. assert mock_record.called + + +async def test_camera_proxy_stream(hass, mock_camera, hass_client): + """Test record service.""" + + client = await hass_client() + + response = await client.get("/api/camera_proxy_stream/camera.demo_camera") + assert response.status == HTTP_OK + + with patch( + "homeassistant.components.demo.camera.DemoCamera.handle_async_mjpeg_stream", + return_value=None, + ): + response = await client.get("/api/camera_proxy_stream/camera.demo_camera") + assert response.status == HTTP_BAD_GATEWAY diff --git a/tests/components/canary/test_init.py b/tests/components/canary/test_init.py index a767eb0ec51..21b897509eb 100644 --- a/tests/components/canary/test_init.py +++ b/tests/components/canary/test_init.py @@ -5,11 +5,7 @@ from requests import ConnectTimeout from homeassistant.components.camera.const import DOMAIN as CAMERA_DOMAIN from homeassistant.components.canary.const import CONF_FFMPEG_ARGUMENTS, DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_PASSWORD, CONF_TIMEOUT, CONF_USERNAME from homeassistant.setup import async_setup_component @@ -64,12 +60,12 @@ async def test_unload_entry(hass, canary): assert entry assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -79,4 +75,4 @@ async def test_async_setup_raises_entry_not_ready(hass, canary): entry = await init_integration(hass) assert entry - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/cast/test_config_flow.py b/tests/components/cast/test_config_flow.py index cc67d585022..7c3fb774722 100644 --- a/tests/components/cast/test_config_flow.py +++ b/tests/components/cast/test_config_flow.py @@ -153,7 +153,8 @@ def get_suggested(schema, key): ) async def test_option_flow(hass, parameter_data): """Test config flow options.""" - all_parameters = ["ignore_cec", "known_hosts", "uuid"] + basic_parameters = ["known_hosts"] + advanced_parameters = ["ignore_cec", "uuid"] parameter, initial, suggested, user_input, updated = parameter_data data = { @@ -170,32 +171,61 @@ async def test_option_flow(hass, parameter_data): # Test ignore_cec and uuid options are hidden if advanced options are disabled result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "options" + assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema assert set(data_schema) == {"known_hosts"} orig_data = dict(config_entry.data) - # Reconfigure ignore_cec, known_hosts, uuid + # Reconfigure known_hosts context = {"source": config_entries.SOURCE_USER, "show_advanced_options": True} result = await hass.config_entries.options.async_init( config_entry.entry_id, context=context ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "options" + assert result["step_id"] == "basic_options" data_schema = result["data_schema"].schema - for other_param in all_parameters: + for other_param in basic_parameters: if other_param == parameter: continue assert get_suggested(data_schema, other_param) == "" - assert get_suggested(data_schema, parameter) == suggested + if parameter in basic_parameters: + assert get_suggested(data_schema, parameter) == suggested + user_input_dict = {} + if parameter in basic_parameters: + user_input_dict[parameter] = user_input result = await hass.config_entries.options.async_configure( result["flow_id"], - user_input={parameter: user_input}, + user_input=user_input_dict, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "advanced_options" + for other_param in basic_parameters: + if other_param == parameter: + continue + assert config_entry.data[other_param] == [] + # No update yet + assert config_entry.data[parameter] == initial + + # Reconfigure ignore_cec, uuid + data_schema = result["data_schema"].schema + for other_param in advanced_parameters: + if other_param == parameter: + continue + assert get_suggested(data_schema, other_param) == "" + if parameter in advanced_parameters: + assert get_suggested(data_schema, parameter) == suggested + + user_input_dict = {} + if parameter in advanced_parameters: + user_input_dict[parameter] = user_input + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input=user_input_dict, ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] is None - for other_param in all_parameters: + for other_param in advanced_parameters: if other_param == parameter: continue assert config_entry.data[other_param] == [] @@ -209,12 +239,10 @@ async def test_option_flow(hass, parameter_data): ) assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["data"] is None - assert config_entry.data == { - **orig_data, - "ignore_cec": [], - "known_hosts": [], - "uuid": [], - } + expected_data = {**orig_data, "known_hosts": []} + if parameter in advanced_parameters: + expected_data[parameter] = updated + assert dict(config_entry.data) == expected_data async def test_known_hosts(hass, castbrowser_mock, castbrowser_constructor_mock): diff --git a/tests/components/cast/test_home_assistant_cast.py b/tests/components/cast/test_home_assistant_cast.py index 6ac4f9c9d0c..1e0618b066e 100644 --- a/tests/components/cast/test_home_assistant_cast.py +++ b/tests/components/cast/test_home_assistant_cast.py @@ -2,7 +2,6 @@ from unittest.mock import patch -from homeassistant import config_entries from homeassistant.components.cast import home_assistant_cast from homeassistant.config import async_process_ha_core_config @@ -93,7 +92,6 @@ async def test_use_cloud_url(hass, mock_zeroconf): async def test_remove_entry(hass, mock_zeroconf): """Test removing config entry removes user.""" entry = MockConfigEntry( - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={}, domain="cast", title="Google Cast", diff --git a/tests/components/cert_expiry/test_init.py b/tests/components/cert_expiry/test_init.py index 1c62782107b..dbe5e74d891 100644 --- a/tests/components/cert_expiry/test_init.py +++ b/tests/components/cert_expiry/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_HOST, CONF_PORT, @@ -63,7 +63,7 @@ async def test_update_unique_id(hass): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.unique_id == f"{HOST}:{PORT}" @@ -89,7 +89,7 @@ async def test_unload_config_entry(mock_now, hass): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state.state == timestamp.isoformat() assert state.attributes.get("error") == "None" @@ -97,7 +97,7 @@ async def test_unload_config_entry(mock_now, hass): await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get("sensor.cert_expiry_timestamp_example_com") assert state.state == STATE_UNAVAILABLE diff --git a/tests/components/cert_expiry/test_sensors.py b/tests/components/cert_expiry/test_sensors.py index 375b676eaf8..099fe78ca39 100644 --- a/tests/components/cert_expiry/test_sensors.py +++ b/tests/components/cert_expiry/test_sensors.py @@ -5,7 +5,7 @@ import ssl from unittest.mock import patch from homeassistant.components.cert_expiry.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE, STATE_UNKNOWN from homeassistant.util.dt import utcnow @@ -81,7 +81,7 @@ async def test_async_setup_entry_host_unavailable(hass): assert await hass.config_entries.async_setup(entry.entry_id) is False await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY next_update = utcnow() + timedelta(seconds=45) async_fire_time_changed(hass, next_update) diff --git a/tests/components/climacell/test_sensor.py b/tests/components/climacell/test_sensor.py index 44fc163848b..d06742ba209 100644 --- a/tests/components/climacell/test_sensor.py +++ b/tests/components/climacell/test_sensor.py @@ -7,7 +7,6 @@ from typing import Any from unittest.mock import patch import pytest -import pytz from homeassistant.components.climacell.config_flow import ( _get_config_schema, @@ -18,6 +17,7 @@ from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ATTR_ATTRIBUTION from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt as dt_util from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA @@ -59,7 +59,7 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", - return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC), + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( diff --git a/tests/components/climacell/test_weather.py b/tests/components/climacell/test_weather.py index 02aa65a350e..fa1ef9dc490 100644 --- a/tests/components/climacell/test_weather.py +++ b/tests/components/climacell/test_weather.py @@ -7,7 +7,6 @@ from typing import Any from unittest.mock import patch import pytest -import pytz from homeassistant.components.climacell.config_flow import ( _get_config_schema, @@ -46,6 +45,7 @@ from homeassistant.components.weather import ( from homeassistant.const import ATTR_ATTRIBUTION, ATTR_FRIENDLY_NAME from homeassistant.core import HomeAssistant, State, callback from homeassistant.helpers.entity_registry import async_get +from homeassistant.util import dt as dt_util from .const import API_V3_ENTRY_DATA, API_V4_ENTRY_DATA @@ -70,7 +70,7 @@ async def _setup(hass: HomeAssistant, config: dict[str, Any]) -> State: """Set up entry and return entity state.""" with patch( "homeassistant.util.dt.utcnow", - return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=pytz.UTC), + return_value=datetime(2021, 3, 6, 23, 59, 59, tzinfo=dt_util.UTC), ): data = _get_config_schema(hass)(config) config_entry = MockConfigEntry( diff --git a/tests/components/cloud/test_google_config.py b/tests/components/cloud/test_google_config.py index bc430347e08..64d50250259 100644 --- a/tests/components/cloud/test_google_config.py +++ b/tests/components/cloud/test_google_config.py @@ -234,6 +234,7 @@ async def test_setup_integration(hass, mock_conf, cloud_prefs): assert "google_assistant" not in hass.config.components await mock_conf.async_initialize() + await hass.async_block_till_done() assert "google_assistant" in hass.config.components hass.config.components.remove("google_assistant") diff --git a/tests/components/cloudflare/__init__.py b/tests/components/cloudflare/__init__.py index 60ce0f055d5..0e4e07b91cc 100644 --- a/tests/components/cloudflare/__init__.py +++ b/tests/components/cloudflare/__init__.py @@ -88,13 +88,6 @@ def _get_mock_cfupdate( return client -def _patch_async_setup(return_value=True): - return patch( - "homeassistant.components.cloudflare.async_setup", - return_value=return_value, - ) - - def _patch_async_setup_entry(return_value=True): return patch( "homeassistant.components.cloudflare.async_setup_entry", diff --git a/tests/components/cloudflare/test_config_flow.py b/tests/components/cloudflare/test_config_flow.py index abdd69269f5..00dbb5e47df 100644 --- a/tests/components/cloudflare/test_config_flow.py +++ b/tests/components/cloudflare/test_config_flow.py @@ -20,7 +20,6 @@ from . import ( USER_INPUT, USER_INPUT_RECORDS, USER_INPUT_ZONE, - _patch_async_setup, _patch_async_setup_entry, ) @@ -58,7 +57,7 @@ async def test_user_form(hass, cfupdate_flow): assert result["step_id"] == "records" assert result["errors"] == {} - with _patch_async_setup() as mock_setup, _patch_async_setup_entry() as mock_setup_entry: + with _patch_async_setup_entry() as mock_setup_entry: result = await hass.config_entries.flow.async_configure( result["flow_id"], USER_INPUT_RECORDS, @@ -76,7 +75,6 @@ async def test_user_form(hass, cfupdate_flow): assert result["result"] assert result["result"].unique_id == USER_INPUT_ZONE[CONF_ZONE] - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/cloudflare/test_init.py b/tests/components/cloudflare/test_init.py index 1fb4af7f9aa..5a42ca9f09c 100644 --- a/tests/components/cloudflare/test_init.py +++ b/tests/components/cloudflare/test_init.py @@ -2,11 +2,7 @@ from pycfdns.exceptions import CloudflareConnectionException from homeassistant.components.cloudflare.const import DOMAIN, SERVICE_UPDATE_RECORDS -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from . import ENTRY_CONFIG, init_integration @@ -18,12 +14,12 @@ async def test_unload_entry(hass, cfupdate): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -37,7 +33,7 @@ async def test_async_setup_raises_entry_not_ready(hass, cfupdate): instance.get_zone_id.side_effect = CloudflareConnectionException() await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_integration_services(hass, cfupdate): @@ -45,7 +41,7 @@ async def test_integration_services(hass, cfupdate): instance = cfupdate.return_value entry = await init_integration(hass) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( DOMAIN, diff --git a/tests/components/config/test_config_entries.py b/tests/components/config/test_config_entries.py index 2f7815c99bf..0e1b471cbd5 100644 --- a/tests/components/config/test_config_entries.py +++ b/tests/components/config/test_config_entries.py @@ -50,14 +50,13 @@ async def test_get_entries(hass, client): pass hass.helpers.config_entry_flow.register_discovery_flow( - "comp2", "Comp 2", lambda: None, core_ce.CONN_CLASS_ASSUMED + "comp2", "Comp 2", lambda: None ) entry = MockConfigEntry( domain="comp1", title="Test 1", source="bla", - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ) entry.supports_unload = True entry.add_to_hass(hass) @@ -65,9 +64,8 @@ async def test_get_entries(hass, client): domain="comp2", title="Test 2", source="bla2", - state=core_ce.ENTRY_STATE_SETUP_ERROR, + state=core_ce.ConfigEntryState.SETUP_ERROR, reason="Unsupported API", - connection_class=core_ce.CONN_CLASS_ASSUMED, ).add_to_hass(hass) MockConfigEntry( domain="comp3", @@ -86,10 +84,11 @@ async def test_get_entries(hass, client): "domain": "comp1", "title": "Test 1", "source": "bla", - "state": "not_loaded", - "connection_class": "local_poll", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": True, "supports_unload": True, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": None, }, @@ -97,10 +96,11 @@ async def test_get_entries(hass, client): "domain": "comp2", "title": "Test 2", "source": "bla2", - "state": "setup_error", - "connection_class": "assumed", + "state": core_ce.ConfigEntryState.SETUP_ERROR.value, "supports_options": False, "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": None, "reason": "Unsupported API", }, @@ -108,10 +108,11 @@ async def test_get_entries(hass, client): "domain": "comp3", "title": "Test 3", "source": "bla3", - "state": "not_loaded", - "connection_class": "unknown", + "state": core_ce.ConfigEntryState.NOT_LOADED.value, "supports_options": False, "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "disabled_by": core_ce.DISABLED_USER, "reason": None, }, @@ -120,7 +121,7 @@ async def test_get_entries(hass, client): async def test_remove_entry(hass, client): """Test removing an entry via the API.""" - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.delete(f"/api/config/config_entries/entry/{entry.entry_id}") assert resp.status == 200 @@ -131,7 +132,7 @@ async def test_remove_entry(hass, client): async def test_reload_entry(hass, client): """Test reloading an entry via the API.""" - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" @@ -151,7 +152,7 @@ async def test_reload_invalid_entry(hass, client): async def test_remove_entry_unauth(hass, client, hass_admin_user): """Test removing an entry via the API.""" hass_admin_user.groups = [] - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.delete(f"/api/config/config_entries/entry/{entry.entry_id}") assert resp.status == 401 @@ -161,7 +162,7 @@ async def test_remove_entry_unauth(hass, client, hass_admin_user): async def test_reload_entry_unauth(hass, client, hass_admin_user): """Test reloading an entry via the API.""" hass_admin_user.groups = [] - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" @@ -172,7 +173,7 @@ async def test_reload_entry_unauth(hass, client, hass_admin_user): async def test_reload_entry_in_failed_state(hass, client, hass_admin_user): """Test reloading an entry via the API that has already failed to unload.""" - entry = MockConfigEntry(domain="demo", state=core_ce.ENTRY_STATE_FAILED_UNLOAD) + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.FAILED_UNLOAD) entry.add_to_hass(hass) resp = await client.post( f"/api/config/config_entries/entry/{entry.entry_id}/reload" @@ -292,7 +293,7 @@ async def test_abort(hass, client): } -async def test_create_account(hass, client): +async def test_create_account(hass, client, enable_custom_integrations): """Test a flow that creates an account.""" mock_entity_platform(hass, "config_flow.test", None) @@ -326,23 +327,25 @@ async def test_create_account(hass, client): "type": "create_entry", "version": 1, "result": { - "connection_class": "unknown", "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, "source": core_ce.SOURCE_USER, - "state": "loaded", + "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "Test Entry", "reason": None, }, "description": None, "description_placeholders": None, + "options": {}, } -async def test_two_step_flow(hass, client): +async def test_two_step_flow(hass, client, enable_custom_integrations): """Test we can finish a two step flow.""" mock_integration( hass, MockModule("test", async_setup_entry=AsyncMock(return_value=True)) @@ -397,19 +400,21 @@ async def test_two_step_flow(hass, client): "title": "user-title", "version": 1, "result": { - "connection_class": "unknown", "disabled_by": None, "domain": "test", "entry_id": entries[0].entry_id, "source": core_ce.SOURCE_USER, - "state": "loaded", + "state": core_ce.ConfigEntryState.LOADED.value, "supports_options": False, "supports_unload": False, + "pref_disable_new_entities": False, + "pref_disable_polling": False, "title": "user-title", "reason": None, }, "description": None, "description_placeholders": None, + "options": {}, } @@ -596,7 +601,6 @@ async def test_options_flow(hass, client): domain="test", entry_id="test1", source="bla", - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) entry = hass.config_entries.async_entries()[0] @@ -646,7 +650,6 @@ async def test_two_step_options_flow(hass, client): domain="test", entry_id="test1", source="bla", - connection_class=core_ce.CONN_CLASS_LOCAL_POLL, ).add_to_hass(hass) entry = hass.config_entries.async_entries()[0] @@ -685,67 +688,53 @@ async def test_two_step_options_flow(hass, client): } -async def test_list_system_options(hass, hass_ws_client): - """Test that we can list an entries system options.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) - - entry = MockConfigEntry(domain="demo") - entry.add_to_hass(hass) - - await ws_client.send_json( - { - "id": 5, - "type": "config_entries/system_options/list", - "entry_id": entry.entry_id, - } - ) - response = await ws_client.receive_json() - - assert response["success"] - assert response["result"] == {"disable_new_entities": False} - - -async def test_update_system_options(hass, hass_ws_client): +async def test_update_prefrences(hass, hass_ws_client): """Test that we can update system options.""" assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is False + await ws_client.send_json( { - "id": 5, - "type": "config_entries/system_options/update", + "id": 6, + "type": "config_entries/update", "entry_id": entry.entry_id, - "disable_new_entities": True, + "pref_disable_new_entities": True, } ) response = await ws_client.receive_json() assert response["success"] - assert response["result"]["disable_new_entities"] - assert entry.system_options.disable_new_entities + assert response["result"]["require_restart"] is False + assert response["result"]["config_entry"]["pref_disable_new_entities"] is True + assert response["result"]["config_entry"]["pref_disable_polling"] is False - -async def test_update_system_options_nonexisting(hass, hass_ws_client): - """Test that we can update entry.""" - assert await async_setup_component(hass, "config", {}) - ws_client = await hass_ws_client(hass) + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False await ws_client.send_json( { - "id": 5, - "type": "config_entries/system_options/update", - "entry_id": "non_existing", - "disable_new_entities": True, + "id": 7, + "type": "config_entries/update", + "entry_id": entry.entry_id, + "pref_disable_new_entities": False, + "pref_disable_polling": True, } ) response = await ws_client.receive_json() - assert not response["success"] - assert response["error"]["code"] == "not_found" + assert response["success"] + assert response["result"]["require_restart"] is True + assert response["result"]["config_entry"]["pref_disable_new_entities"] is False + assert response["result"]["config_entry"]["pref_disable_polling"] is True + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry(hass, hass_ws_client): @@ -767,7 +756,7 @@ async def test_update_entry(hass, hass_ws_client): response = await ws_client.receive_json() assert response["success"] - assert response["result"]["title"] == "Updated Title" + assert response["result"]["config_entry"]["title"] == "Updated Title" assert entry.title == "Updated Title" @@ -795,7 +784,7 @@ async def test_disable_entry(hass, hass_ws_client): assert await async_setup_component(hass, "config", {}) ws_client = await hass_ws_client(hass) - entry = MockConfigEntry(domain="demo", state="loaded") + entry = MockConfigEntry(domain="demo", state=core_ce.ConfigEntryState.LOADED) entry.add_to_hass(hass) assert entry.disabled_by is None @@ -813,7 +802,7 @@ async def test_disable_entry(hass, hass_ws_client): assert response["success"] assert response["result"] == {"require_restart": True} assert entry.disabled_by == core_ce.DISABLED_USER - assert entry.state == "failed_unload" + assert entry.state is core_ce.ConfigEntryState.FAILED_UNLOAD # Enable await ws_client.send_json( @@ -829,7 +818,7 @@ async def test_disable_entry(hass, hass_ws_client): assert response["success"] assert response["result"] == {"require_restart": True} assert entry.disabled_by is None - assert entry.state == "failed_unload" + assert entry.state == core_ce.ConfigEntryState.FAILED_UNLOAD # Enable again -> no op await ws_client.send_json( @@ -845,7 +834,7 @@ async def test_disable_entry(hass, hass_ws_client): assert response["success"] assert response["result"] == {"require_restart": False} assert entry.disabled_by is None - assert entry.state == "failed_unload" + assert entry.state == core_ce.ConfigEntryState.FAILED_UNLOAD async def test_disable_entry_nonexisting(hass, hass_ws_client): diff --git a/tests/components/config/test_core.py b/tests/components/config/test_core.py index 361fceab565..738b1183c14 100644 --- a/tests/components/config/test_core.py +++ b/tests/components/config/test_core.py @@ -57,7 +57,7 @@ async def test_websocket_core_update(hass, client): assert hass.config.elevation != 25 assert hass.config.location_name != "Huis" assert hass.config.units.name != CONF_UNIT_SYSTEM_IMPERIAL - assert hass.config.time_zone.zone != "America/New_York" + assert hass.config.time_zone != "America/New_York" assert hass.config.external_url != "https://www.example.com" assert hass.config.internal_url != "http://example.com" @@ -91,7 +91,7 @@ async def test_websocket_core_update(hass, client): assert hass.config.internal_url == "http://example.local" assert len(mock_set_tz.mock_calls) == 1 - assert mock_set_tz.mock_calls[0][1][0].zone == "America/New_York" + assert mock_set_tz.mock_calls[0][1][0] == dt_util.get_time_zone("America/New_York") async def test_websocket_core_update_not_admin(hass, hass_ws_client, hass_admin_user): @@ -144,7 +144,6 @@ async def test_detect_config_fail(hass, client): return_value=location.LocationInfo( ip=None, country_code=None, - country_name=None, region_code=None, region_name=None, city=None, diff --git a/tests/components/coronavirus/test_init.py b/tests/components/coronavirus/test_init.py index c36255db9d1..eeb91e77239 100644 --- a/tests/components/coronavirus/test_init.py +++ b/tests/components/coronavirus/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from aiohttp import ClientError from homeassistant.components.coronavirus.const import DOMAIN, OPTION_WORLDWIDE -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -69,4 +69,4 @@ async def test_config_entry_not_ready( assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/cover/test_device_action.py b/tests/components/cover/test_device_action.py index 5cec3d901e1..60bb4e5401b 100644 --- a/tests/components/cover/test_device_action.py +++ b/tests/components/cover/test_device_action.py @@ -31,7 +31,7 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions(hass, device_reg, entity_reg): +async def test_get_actions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -73,7 +73,9 @@ async def test_get_actions(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_tilt(hass, device_reg, entity_reg): +async def test_get_actions_tilt( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -127,7 +129,9 @@ async def test_get_actions_tilt(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_set_pos(hass, device_reg, entity_reg): +async def test_get_actions_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -157,7 +161,9 @@ async def test_get_actions_set_pos(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_actions_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -205,7 +211,9 @@ async def test_get_actions_set_tilt_pos(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_action_capabilities(hass, device_reg, entity_reg): +async def test_get_action_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -233,7 +241,9 @@ async def test_get_action_capabilities(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg): +async def test_get_action_capabilities_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -276,7 +286,9 @@ async def test_get_action_capabilities_set_pos(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_action_capabilities_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover action.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -319,7 +331,7 @@ async def test_get_action_capabilities_set_tilt_pos(hass, device_reg, entity_reg assert capabilities == {"extra_fields": []} -async def test_action(hass): +async def test_action(hass, enable_custom_integrations): """Test for cover actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -385,7 +397,7 @@ async def test_action(hass): assert len(stop_calls) == 1 -async def test_action_tilt(hass): +async def test_action_tilt(hass, enable_custom_integrations): """Test for cover tilt actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -438,7 +450,7 @@ async def test_action_tilt(hass): assert len(close_calls) == 1 -async def test_action_set_position(hass): +async def test_action_set_position(hass, enable_custom_integrations): """Test for cover set position actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/cover/test_device_condition.py b/tests/components/cover/test_device_condition.py index 04415661515..8e8d92e5a1c 100644 --- a/tests/components/cover/test_device_condition.py +++ b/tests/components/cover/test_device_condition.py @@ -43,7 +43,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected conditions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -94,7 +94,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) -async def test_get_conditions_set_pos(hass, device_reg, entity_reg): +async def test_get_conditions_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected conditions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -152,7 +154,9 @@ async def test_get_conditions_set_pos(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) -async def test_get_conditions_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_conditions_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected conditions from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -210,7 +214,9 @@ async def test_get_conditions_set_tilt_pos(hass, device_reg, entity_reg): assert_lists_same(conditions, expected_conditions) -async def test_get_condition_capabilities(hass, device_reg, entity_reg): +async def test_get_condition_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -237,7 +243,9 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_condition_capabilities_set_pos(hass, device_reg, entity_reg): +async def test_get_condition_capabilities_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -287,7 +295,9 @@ async def test_get_condition_capabilities_set_pos(hass, device_reg, entity_reg): assert capabilities == {"extra_fields": []} -async def test_get_condition_capabilities_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_condition_capabilities_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -449,7 +459,7 @@ async def test_if_state(hass, calls): assert calls[3].data["some"] == "is_closing - event - test_event4" -async def test_if_position(hass, calls): +async def test_if_position(hass, calls, enable_custom_integrations): """Test for position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -553,7 +563,7 @@ async def test_if_position(hass, calls): assert calls[4].data["some"] == "is_pos_gt_45 - event - test_event1" -async def test_if_tilt_position(hass, calls): +async def test_if_tilt_position(hass, calls, enable_custom_integrations): """Test for tilt position conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/cover/test_device_trigger.py b/tests/components/cover/test_device_trigger.py index 7ff5a434e5b..8732fcc8020 100644 --- a/tests/components/cover/test_device_trigger.py +++ b/tests/components/cover/test_device_trigger.py @@ -47,7 +47,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected triggers from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -98,7 +98,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_set_pos(hass, device_reg, entity_reg): +async def test_get_triggers_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected triggers from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -156,7 +158,9 @@ async def test_get_triggers_set_pos(hass, device_reg, entity_reg): assert_lists_same(triggers, expected_triggers) -async def test_get_triggers_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_triggers_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected triggers from a cover.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -214,7 +218,9 @@ async def test_get_triggers_set_tilt_pos(hass, device_reg, entity_reg): assert_lists_same(triggers, expected_triggers) -async def test_get_trigger_capabilities(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -245,7 +251,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): } -async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities_set_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -303,7 +311,9 @@ async def test_get_trigger_capabilities_set_pos(hass, device_reg, entity_reg): } -async def test_get_trigger_capabilities_set_tilt_pos(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities_set_tilt_pos( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a cover trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -538,7 +548,7 @@ async def test_if_fires_on_state_change_with_for(hass, calls): ) -async def test_if_fires_on_position(hass, calls): +async def test_if_fires_on_position(hass, calls, enable_custom_integrations): """Test for position triggers.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -665,7 +675,7 @@ async def test_if_fires_on_position(hass, calls): ) -async def test_if_fires_on_tilt_position(hass, calls): +async def test_if_fires_on_tilt_position(hass, calls, enable_custom_integrations): """Test for tilt position triggers.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/daikin/test_config_flow.py b/tests/components/daikin/test_config_flow.py index 076f9f54878..624268d7ee3 100644 --- a/tests/components/daikin/test_config_flow.py +++ b/tests/components/daikin/test_config_flow.py @@ -8,7 +8,7 @@ from aiohttp.web_exceptions import HTTPForbidden import pytest from homeassistant.components.daikin.const import KEY_MAC -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, @@ -80,27 +80,6 @@ async def test_abort_if_already_setup(hass, mock_daikin): assert result["reason"] == "already_configured" -async def test_import(hass, mock_daikin): - """Test import step.""" - result = await hass.config_entries.flow.async_init( - "daikin", - context={"source": SOURCE_IMPORT}, - data={}, - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "user" - - result = await hass.config_entries.flow.async_init( - "daikin", - context={"source": SOURCE_IMPORT}, - data={CONF_HOST: HOST}, - ) - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == HOST - assert result["data"][CONF_HOST] == HOST - assert result["data"][KEY_MAC] == MAC - - @pytest.mark.parametrize( "s_effect,reason", [ diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py index 1712faaf080..8a160b7ef19 100644 --- a/tests/components/deconz/test_gateway.py +++ b/tests/components/deconz/test_gateway.py @@ -32,7 +32,7 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_UDN, ) from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_SSDP, SOURCE_USER +from homeassistant.config_entries import SOURCE_SSDP, SOURCE_USER from homeassistant.const import ( CONF_API_KEY, CONF_HOST, @@ -116,7 +116,6 @@ async def setup_deconz_integration( domain=DECONZ_DOMAIN, source=source, data=deepcopy(config), - connection_class=CONN_CLASS_LOCAL_PUSH, options=deepcopy(options), entry_id=entry_id, unique_id=unique_id, diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 84c7a9b1078..a5e27709ebf 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -179,7 +179,6 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): blocking=True, ) assert aioclient_mock.mock_calls[1][2] == { - "ct": 2500, "bri": 200, "transitiontime": 50, "alert": "select", diff --git a/tests/components/deconz/test_services.py b/tests/components/deconz/test_services.py index 249f4dbbb57..7ad9c82b08c 100644 --- a/tests/components/deconz/test_services.py +++ b/tests/components/deconz/test_services.py @@ -322,7 +322,8 @@ async def test_remove_orphaned_entries_service(hass, aioclient_mock): device_registry = dr.async_get(hass) device = device_registry.async_get_or_create( - config_entry_id=config_entry.entry_id, identifiers={("mac", "123")} + config_entry_id=config_entry.entry_id, + connections={(dr.CONNECTION_NETWORK_MAC, "123")}, ) assert ( diff --git a/tests/components/demo/test_media_player.py b/tests/components/demo/test_media_player.py index 4ed41ce9c83..9cc01d18b03 100644 --- a/tests/components/demo/test_media_player.py +++ b/tests/components/demo/test_media_player.py @@ -400,6 +400,26 @@ async def test_seek(hass, mock_media_seek): assert mock_media_seek.called +async def test_stop(hass): + """Test stop.""" + assert await async_setup_component( + hass, mp.DOMAIN, {"media_player": {"platform": "demo"}} + ) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_PLAYING + + await hass.services.async_call( + mp.DOMAIN, + mp.SERVICE_MEDIA_STOP, + {ATTR_ENTITY_ID: TEST_ENTITY_ID}, + blocking=True, + ) + state = hass.states.get(TEST_ENTITY_ID) + assert state.state == STATE_OFF + + async def test_media_image_proxy(hass, hass_client): """Test the media server image proxy server .""" assert await async_setup_component( diff --git a/tests/components/device_automation/__init__.py b/tests/components/device_automation/__init__.py new file mode 100644 index 00000000000..8663047716d --- /dev/null +++ b/tests/components/device_automation/__init__.py @@ -0,0 +1 @@ +"""device_automation tests.""" diff --git a/tests/components/device_automation/test_init.py b/tests/components/device_automation/test_init.py index a2f042bcd73..7c16d067eff 100644 --- a/tests/components/device_automation/test_init.py +++ b/tests/components/device_automation/test_init.py @@ -490,7 +490,9 @@ async def test_automation_with_non_existing_integration(hass, caplog): assert "Integration 'beer' not found" in caplog.text -async def test_automation_with_integration_without_device_action(hass, caplog): +async def test_automation_with_integration_without_device_action( + hass, caplog, enable_custom_integrations +): """Test automation with integration without device action support.""" assert await async_setup_component( hass, @@ -509,7 +511,9 @@ async def test_automation_with_integration_without_device_action(hass, caplog): ) -async def test_automation_with_integration_without_device_condition(hass, caplog): +async def test_automation_with_integration_without_device_condition( + hass, caplog, enable_custom_integrations +): """Test automation with integration without device condition support.""" assert await async_setup_component( hass, @@ -534,7 +538,9 @@ async def test_automation_with_integration_without_device_condition(hass, caplog ) -async def test_automation_with_integration_without_device_trigger(hass, caplog): +async def test_automation_with_integration_without_device_trigger( + hass, caplog, enable_custom_integrations +): """Test automation with integration without device trigger support.""" assert await async_setup_component( hass, @@ -615,7 +621,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_automation_with_sub_condition(hass, calls): +async def test_automation_with_sub_condition(hass, calls, enable_custom_integrations): """Test automation with device condition under and/or conditions.""" DOMAIN = "light" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/device_sun_light_trigger/test_init.py b/tests/components/device_sun_light_trigger/test_init.py index 7d478e7d8d1..f7d835427a1 100644 --- a/tests/components/device_sun_light_trigger/test_init.py +++ b/tests/components/device_sun_light_trigger/test_init.py @@ -29,7 +29,7 @@ from tests.common import async_fire_time_changed @pytest.fixture -def scanner(hass): +def scanner(hass, enable_custom_integrations): """Initialize components.""" scanner = getattr(hass.components, "test.device_tracker").get_scanner(None, None) @@ -100,7 +100,7 @@ async def test_lights_on_when_sun_sets(hass, scanner): ) -async def test_lights_turn_off_when_everyone_leaves(hass): +async def test_lights_turn_off_when_everyone_leaves(hass, enable_custom_integrations): """Test lights turn off when everyone leaves the house.""" assert await async_setup_component( hass, "light", {light.DOMAIN: {CONF_PLATFORM: "test"}} diff --git a/tests/components/device_tracker/test_entities.py b/tests/components/device_tracker/test_entities.py index 1bc058a1449..88e1dccdb34 100644 --- a/tests/components/device_tracker/test_entities.py +++ b/tests/components/device_tracker/test_entities.py @@ -18,7 +18,7 @@ from homeassistant.const import ATTR_BATTERY_LEVEL, STATE_HOME, STATE_NOT_HOME from tests.common import MockConfigEntry -async def test_scanner_entity_device_tracker(hass): +async def test_scanner_entity_device_tracker(hass, enable_custom_integrations): """Test ScannerEntity based device tracker.""" config_entry = MockConfigEntry(domain="test") config_entry.add_to_hass(hass) diff --git a/tests/components/device_tracker/test_init.py b/tests/components/device_tracker/test_init.py index 6155ed7d1db..bf72cb34119 100644 --- a/tests/components/device_tracker/test_init.py +++ b/tests/components/device_tracker/test_init.py @@ -94,7 +94,7 @@ async def test_reading_broken_yaml_config(hass): assert res[0].dev_id == "my_device" -async def test_reading_yaml_config(hass, yaml_devices): +async def test_reading_yaml_config(hass, yaml_devices, enable_custom_integrations): """Test the rendering of the YAML configuration.""" dev_id = "test" device = legacy.Device( @@ -161,7 +161,7 @@ async def test_duplicate_mac_dev_id(mock_warning, hass): assert "Duplicate device IDs" in args[0], "Duplicate device IDs warning expected" -async def test_setup_without_yaml_file(hass): +async def test_setup_without_yaml_file(hass, enable_custom_integrations): """Test with no YAML file.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -224,7 +224,7 @@ async def test_discover_platform(mock_demo_setup_scanner, mock_see, hass): ) -async def test_update_stale(hass, mock_device_tracker_conf): +async def test_update_stale(hass, mock_device_tracker_conf, enable_custom_integrations): """Test stalled update.""" scanner = getattr(hass.components, "test.device_tracker").SCANNER @@ -265,7 +265,9 @@ async def test_update_stale(hass, mock_device_tracker_conf): assert hass.states.get("device_tracker.dev1").state == STATE_NOT_HOME -async def test_entity_attributes(hass, mock_device_tracker_conf): +async def test_entity_attributes( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test the entity attributes.""" devices = mock_device_tracker_conf dev_id = "test_entity" @@ -297,7 +299,7 @@ async def test_entity_attributes(hass, mock_device_tracker_conf): @patch("homeassistant.components.device_tracker.legacy." "DeviceTracker.async_see") -async def test_see_service(mock_see, hass): +async def test_see_service(mock_see, hass, enable_custom_integrations): """Test the see service with a unicode dev_id and NO MAC.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -324,7 +326,9 @@ async def test_see_service(mock_see, hass): assert mock_see.call_args == call(**params) -async def test_see_service_guard_config_entry(hass, mock_device_tracker_conf): +async def test_see_service_guard_config_entry( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test the guard if the device is registered in the entity registry.""" mock_entry = Mock() dev_id = "test" @@ -340,7 +344,9 @@ async def test_see_service_guard_config_entry(hass, mock_device_tracker_conf): assert not devices -async def test_new_device_event_fired(hass, mock_device_tracker_conf): +async def test_new_device_event_fired( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker will fire an event.""" with assert_setup_component(1, device_tracker.DOMAIN): assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -370,7 +376,9 @@ async def test_new_device_event_fired(hass, mock_device_tracker_conf): } -async def test_duplicate_yaml_keys(hass, mock_device_tracker_conf): +async def test_duplicate_yaml_keys( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker will not generate invalid YAML.""" devices = mock_device_tracker_conf with assert_setup_component(1, device_tracker.DOMAIN): @@ -385,7 +393,9 @@ async def test_duplicate_yaml_keys(hass, mock_device_tracker_conf): assert devices[0].dev_id != devices[1].dev_id -async def test_invalid_dev_id(hass, mock_device_tracker_conf): +async def test_invalid_dev_id( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker will not allow invalid dev ids.""" devices = mock_device_tracker_conf with assert_setup_component(1, device_tracker.DOMAIN): @@ -397,7 +407,7 @@ async def test_invalid_dev_id(hass, mock_device_tracker_conf): assert not devices -async def test_see_state(hass, yaml_devices): +async def test_see_state(hass, yaml_devices, enable_custom_integrations): """Test device tracker see records state correctly.""" assert await async_setup_component(hass, device_tracker.DOMAIN, TEST_PLATFORM) @@ -433,7 +443,9 @@ async def test_see_state(hass, yaml_devices): assert attrs["number"] == 1 -async def test_see_passive_zone_state(hass, mock_device_tracker_conf): +async def test_see_passive_zone_state( + hass, mock_device_tracker_conf, enable_custom_integrations +): """Test that the device tracker sets gps for passive trackers.""" now = dt_util.utcnow() @@ -562,7 +574,9 @@ async def test_bad_platform(hass): assert f"{device_tracker.DOMAIN}.bad_platform" not in hass.config.components -async def test_adding_unknown_device_to_config(mock_device_tracker_conf, hass): +async def test_adding_unknown_device_to_config( + mock_device_tracker_conf, hass, enable_custom_integrations +): """Test the adding of unknown devices to configuration file.""" scanner = getattr(hass.components, "test.device_tracker").SCANNER scanner.reset() diff --git a/tests/components/devolo_home_control/test_init.py b/tests/components/devolo_home_control/test_init.py index 3f124ee2098..657836f9d16 100644 --- a/tests/components/devolo_home_control/test_init.py +++ b/tests/components/devolo_home_control/test_init.py @@ -4,12 +4,7 @@ from unittest.mock import patch from devolo_home_control_api.exceptions.gateway import GatewayOfflineError import pytest -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.devolo_home_control import configure_integration @@ -20,7 +15,7 @@ async def test_setup_entry(hass: HomeAssistant): entry = configure_integration(hass) with patch("homeassistant.components.devolo_home_control.HomeControl"): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED @pytest.mark.credentials_invalid @@ -28,7 +23,7 @@ async def test_setup_entry_credentials_invalid(hass: HomeAssistant): """Test setup entry fails if credentials are invalid.""" entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR @pytest.mark.maintenance @@ -36,7 +31,7 @@ async def test_setup_entry_maintenance(hass: HomeAssistant): """Test setup entry fails if mydevolo is in maintenance mode.""" entry = configure_integration(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_gateway_offline(hass: HomeAssistant): @@ -47,7 +42,7 @@ async def test_setup_gateway_offline(hass: HomeAssistant): side_effect=GatewayOfflineError, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass: HomeAssistant): @@ -57,4 +52,4 @@ async def test_unload_entry(hass: HomeAssistant): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/dexcom/test_init.py b/tests/components/dexcom/test_init.py index a155450bf26..2509ba25f33 100644 --- a/tests/components/dexcom/test_init.py +++ b/tests/components/dexcom/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import patch from pydexcom import AccountError, SessionError from homeassistant.components.dexcom.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.common import MockConfigEntry from tests.components.dexcom import CONFIG, init_integration @@ -55,10 +55,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/directv/test_init.py b/tests/components/directv/test_init.py index 96fd27a30eb..3ef151f4257 100644 --- a/tests/components/directv/test_init.py +++ b/tests/components/directv/test_init.py @@ -1,10 +1,6 @@ """Tests for the DirecTV integration.""" from homeassistant.components.directv.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.directv import setup_integration @@ -19,7 +15,7 @@ async def test_config_entry_not_ready( """Test the DirecTV configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, setup_error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -29,10 +25,10 @@ async def test_unload_config_entry( entry = await setup_integration(hass, aioclient_mock) assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 9fa9752dc65..915955da652 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -5,7 +5,6 @@ import pytest import requests from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.doorbird import CONF_CUSTOM_URL, CONF_TOKEN from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME @@ -76,124 +75,6 @@ async def test_user_form(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - import_config = VALID_CONFIG.copy() - import_config[CONF_EVENTS] = ["event1", "event2", "event3"] - import_config[CONF_TOKEN] = "imported_token" - import_config[ - CONF_CUSTOM_URL - ] = "http://legacy.custom.url/should/only/come/in/from/yaml" - - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "macaddr"} - ) - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( - "homeassistant.components.doorbird.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=import_config, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "1.2.3.4" - assert result["data"] == { - "host": "1.2.3.4", - "name": "mydoorbird", - "password": "password", - "username": "friend", - "events": ["event1", "event2", "event3"], - "token": "imported_token", - # This will go away once we convert to cloud hooks - "hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml", - } - # It is not possible to import options at this time - # so they end up in the config entry data and are - # used a fallback when they are not in options - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_with_zeroconf_already_discovered(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - doorbirdapi = _get_mock_doorbirdapi_return_values( - ready=[True], info={"WIFI_MAC_ADDR": "1CCAE3DOORBIRD"} - ) - # Running the zeroconf init will make the unique id - # in progress - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ): - zero_conf = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data={ - "properties": {"macaddress": "1CCAE3DOORBIRD"}, - "name": "Doorstation - abc123._axis-video._tcp.local.", - "host": "192.168.1.5", - }, - ) - await hass.async_block_till_done() - assert zero_conf["type"] == data_entry_flow.RESULT_TYPE_FORM - assert zero_conf["step_id"] == "user" - assert zero_conf["errors"] == {} - - import_config = VALID_CONFIG.copy() - import_config[CONF_EVENTS] = ["event1", "event2", "event3"] - import_config[CONF_TOKEN] = "imported_token" - import_config[ - CONF_CUSTOM_URL - ] = "http://legacy.custom.url/should/only/come/in/from/yaml" - - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( - "homeassistant.components.doorbird.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.doorbird.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=import_config, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "1.2.3.4" - assert result["data"] == { - "host": "1.2.3.4", - "name": "mydoorbird", - "password": "password", - "username": "friend", - "events": ["event1", "event2", "event3"], - "token": "imported_token", - # This will go away once we convert to cloud hooks - "hass_url_override": "http://legacy.custom.url/should/only/come/in/from/yaml", - } - # It is not possible to import options at this time - # so they end up in the config entry data and are - # used a fallback when they are not in options - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - async def test_form_zeroconf_wrong_oui(hass): """Test we abort when we get the wrong OUI via zeroconf.""" await setup.async_setup_component(hass, "persistent_notification", {}) diff --git a/tests/components/dsmr/test_sensor.py b/tests/components/dsmr/test_sensor.py index e037585f8ff..29ab29a0af6 100644 --- a/tests/components/dsmr/test_sensor.py +++ b/tests/components/dsmr/test_sensor.py @@ -11,6 +11,7 @@ from decimal import Decimal from itertools import chain, repeat from unittest.mock import DEFAULT, MagicMock +from homeassistant import config_entries from homeassistant.components.dsmr.const import DOMAIN from homeassistant.components.dsmr.sensor import DerivativeDSMREntity from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN @@ -59,7 +60,7 @@ async def test_setup_platform(hass, dsmr_connection_fixture): entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state == config_entries.ConfigEntryState.LOADED assert entry.data == {**entry_data, **serial_data} @@ -625,4 +626,4 @@ async def test_reconnect(hass, dsmr_connection_fixture): await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == "not_loaded" + assert mock_entry.state == config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/dunehd/test_config_flow.py b/tests/components/dunehd/test_config_flow.py index 278c94864b8..c8e7b0f7f3e 100644 --- a/tests/components/dunehd/test_config_flow.py +++ b/tests/components/dunehd/test_config_flow.py @@ -66,7 +66,12 @@ async def test_user_invalid_host(hass): async def test_user_very_long_host(hass): """Test that errors are shown when the host is longer than 253 chars.""" - long_host = "very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_host" + long_host = ( + "very_long_host_very_long_host_very_long_host_very_long_host_very_long_" + "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_" + "host_very_long_host_very_long_host_very_long_host_very_long_host_very_long_" + "host_very_long_host_very_long_host" + ) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data={CONF_HOST: long_host} ) diff --git a/tests/components/dynalite/test_config_flow.py b/tests/components/dynalite/test_config_flow.py index e21c82d7c20..a513fe27567 100644 --- a/tests/components/dynalite/test_config_flow.py +++ b/tests/components/dynalite/test_config_flow.py @@ -13,9 +13,9 @@ from tests.common import MockConfigEntry @pytest.mark.parametrize( "first_con, second_con,exp_type, exp_result, exp_reason", [ - (True, True, "create_entry", "loaded", ""), - (False, False, "abort", "", "no_connection"), - (True, False, "create_entry", "setup_retry", ""), + (True, True, "create_entry", config_entries.ConfigEntryState.LOADED, ""), + (False, False, "abort", None, "no_connection"), + (True, False, "create_entry", config_entries.ConfigEntryState.SETUP_RETRY, ""), ], ) async def test_flow(hass, first_con, second_con, exp_type, exp_result, exp_reason): @@ -104,4 +104,4 @@ async def test_two_entries(hass): data={dynalite.CONF_HOST: host2}, ) assert result["type"] == "create_entry" - assert result["result"].state == "loaded" + assert result["result"].state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/eafm/test_sensor.py b/tests/components/eafm/test_sensor.py index 3f2eb72a8e3..d8d48367a64 100644 --- a/tests/components/eafm/test_sensor.py +++ b/tests/components/eafm/test_sensor.py @@ -31,12 +31,11 @@ async def async_setup_test_fixture(hass, mock_get_station, initial_value): entry_id="VikingRecorder1234", data={"station": "L1234"}, title="Viking Recorder", - connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, ) entry.add_to_hass(hass) assert await async_setup_component(hass, "eafm", {}) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED await hass.async_block_till_done() async def poll(value): diff --git a/tests/components/elgato/__init__.py b/tests/components/elgato/__init__.py index ea63bc0c4d0..12df481d182 100644 --- a/tests/components/elgato/__init__.py +++ b/tests/components/elgato/__init__.py @@ -12,6 +12,8 @@ async def init_integration( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, skip_setup: bool = False, + color: bool = False, + mode_color: bool = False, ) -> MockConfigEntry: """Set up the Elgato Key Light integration in Home Assistant.""" aioclient_mock.get( @@ -20,24 +22,38 @@ async def init_integration( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - aioclient_mock.put( - "http://127.0.0.1:9123/elgato/lights", - text=load_fixture("elgato/state.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - aioclient_mock.get( - "http://127.0.0.1:9123/elgato/lights", - text=load_fixture("elgato/state.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - aioclient_mock.get( "http://127.0.0.2:9123/elgato/accessory-info", text=load_fixture("elgato/info.json"), headers={"Content-Type": CONTENT_TYPE_JSON}, ) + settings = "elgato/settings.json" + if color: + settings = "elgato/settings-color.json" + + aioclient_mock.get( + "http://127.0.0.1:9123/elgato/lights/settings", + text=load_fixture(settings), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + state = "elgato/state.json" + if mode_color: + state = "elgato/state-color.json" + + aioclient_mock.get( + "http://127.0.0.1:9123/elgato/lights", + text=load_fixture(state), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + + aioclient_mock.put( + "http://127.0.0.1:9123/elgato/lights", + text=load_fixture("elgato/state.json"), + headers={"Content-Type": CONTENT_TYPE_JSON}, + ) + entry = MockConfigEntry( domain=DOMAIN, unique_id="CN11A1A00001", diff --git a/tests/components/elgato/test_init.py b/tests/components/elgato/test_init.py index 069e533c423..f764ecdba80 100644 --- a/tests/components/elgato/test_init.py +++ b/tests/components/elgato/test_init.py @@ -2,7 +2,7 @@ import aiohttp from homeassistant.components.elgato.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.elgato import init_integration @@ -18,7 +18,7 @@ async def test_config_entry_not_ready( ) entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( diff --git a/tests/components/elgato/test_light.py b/tests/components/elgato/test_light.py index 6c4de76719f..fbc926d318f 100644 --- a/tests/components/elgato/test_light.py +++ b/tests/components/elgato/test_light.py @@ -2,10 +2,19 @@ from unittest.mock import patch from elgato import ElgatoError +import pytest +from homeassistant.components.elgato.const import DOMAIN, SERVICE_IDENTIFY from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, + ATTR_HS_COLOR, + ATTR_MAX_MIREDS, + ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, DOMAIN as LIGHT_DOMAIN, ) from homeassistant.const import ( @@ -23,10 +32,10 @@ from tests.components.elgato import init_integration from tests.test_util.aiohttp import AiohttpClientMocker -async def test_light_state( +async def test_light_state_temperature( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test the creation and values of the Elgato Key Lights.""" + """Test the creation and values of the Elgato Lights in temperature mode.""" await init_integration(hass, aioclient_mock) entity_registry = er.async_get(hass) @@ -36,6 +45,11 @@ async def test_light_state( assert state assert state.attributes.get(ATTR_BRIGHTNESS) == 54 assert state.attributes.get(ATTR_COLOR_TEMP) == 297 + assert state.attributes.get(ATTR_HS_COLOR) is None + assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_COLOR_TEMP + assert state.attributes.get(ATTR_MIN_MIREDS) == 143 + assert state.attributes.get(ATTR_MAX_MIREDS) == 344 + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [COLOR_MODE_COLOR_TEMP] assert state.state == STATE_ON entry = entity_registry.async_get("light.frenck") @@ -43,13 +57,42 @@ async def test_light_state( assert entry.unique_id == "CN11A1A00001" -async def test_light_change_state( +async def test_light_state_color( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test the creation and values of the Elgato Lights in temperature mode.""" + await init_integration(hass, aioclient_mock, color=True, mode_color=True) + + entity_registry = er.async_get(hass) + + # First segment of the strip + state = hass.states.get("light.frenck") + assert state + assert state.attributes.get(ATTR_BRIGHTNESS) == 128 + assert state.attributes.get(ATTR_COLOR_TEMP) is None + assert state.attributes.get(ATTR_HS_COLOR) == (358.0, 6.0) + assert state.attributes.get(ATTR_MIN_MIREDS) == 153 + assert state.attributes.get(ATTR_MAX_MIREDS) == 285 + assert state.attributes.get(ATTR_COLOR_MODE) == COLOR_MODE_HS + assert state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) == [ + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + ] + assert state.state == STATE_ON + + entry = entity_registry.async_get("light.frenck") + assert entry + assert entry.unique_id == "CN11A1A00001" + + +async def test_light_change_state_temperature( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the change of state of a Elgato Key Light device.""" - await init_integration(hass, aioclient_mock) + await init_integration(hass, aioclient_mock, color=True, mode_color=False) state = hass.states.get("light.frenck") + assert state assert state.state == STATE_ON with patch( @@ -68,12 +111,25 @@ async def test_light_change_state( ) await hass.async_block_till_done() assert len(mock_light.mock_calls) == 1 - mock_light.assert_called_with(on=True, brightness=100, temperature=100) + mock_light.assert_called_with( + on=True, brightness=100, temperature=100, hue=None, saturation=None + ) + + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.frenck", + ATTR_BRIGHTNESS: 255, + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 2 + mock_light.assert_called_with( + on=True, brightness=100, temperature=297, hue=None, saturation=None + ) - with patch( - "homeassistant.components.elgato.light.Elgato.light", - return_value=mock_coro(), - ) as mock_light: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -81,14 +137,46 @@ async def test_light_change_state( blocking=True, ) await hass.async_block_till_done() - assert len(mock_light.mock_calls) == 1 + assert len(mock_light.mock_calls) == 3 mock_light.assert_called_with(on=False) -async def test_light_unavailable( +async def test_light_change_state_color( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: - """Test error/unavailable handling of an Elgato Key Light.""" + """Test the color state state of a Elgato Light device.""" + await init_integration(hass, aioclient_mock, color=True) + + state = hass.states.get("light.frenck") + assert state + assert state.state == STATE_ON + + with patch( + "homeassistant.components.elgato.light.Elgato.light", + return_value=mock_coro(), + ) as mock_light: + await hass.services.async_call( + LIGHT_DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: "light.frenck", + ATTR_BRIGHTNESS: 255, + ATTR_HS_COLOR: (10.1, 20.2), + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_light.mock_calls) == 1 + mock_light.assert_called_with( + on=True, brightness=100, temperature=None, hue=10.1, saturation=20.2 + ) + + +@pytest.mark.parametrize("service", [SERVICE_TURN_ON, SERVICE_TURN_OFF]) +async def test_light_unavailable( + service: str, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test error/unavailable handling of an Elgato Light.""" await init_integration(hass, aioclient_mock) with patch( "homeassistant.components.elgato.light.Elgato.light", @@ -99,10 +187,57 @@ async def test_light_unavailable( ): await hass.services.async_call( LIGHT_DOMAIN, - SERVICE_TURN_OFF, + service, {ATTR_ENTITY_ID: "light.frenck"}, blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.frenck") assert state.state == STATE_UNAVAILABLE + + +async def test_light_identify( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test identifying an Elgato Light.""" + await init_integration(hass, aioclient_mock) + + with patch( + "homeassistant.components.elgato.light.Elgato.identify", + return_value=mock_coro(), + ) as mock_identify: + await hass.services.async_call( + DOMAIN, + SERVICE_IDENTIFY, + { + ATTR_ENTITY_ID: "light.frenck", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_identify.mock_calls) == 1 + mock_identify.assert_called_with() + + +async def test_light_identify_error( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, caplog +) -> None: + """Test error occurred during identifying an Elgato Light.""" + await init_integration(hass, aioclient_mock) + + with patch( + "homeassistant.components.elgato.light.Elgato.identify", + side_effect=ElgatoError, + ) as mock_identify: + await hass.services.async_call( + DOMAIN, + SERVICE_IDENTIFY, + { + ATTR_ENTITY_ID: "light.frenck", + }, + blocking=True, + ) + await hass.async_block_till_done() + assert len(mock_identify.mock_calls) == 1 + + assert "An error occurred while identifying the Elgato Light" in caplog.text diff --git a/tests/components/emulated_hue/test_init.py b/tests/components/emulated_hue/test_init.py index 8e8c0f41249..da15fbfba30 100644 --- a/tests/components/emulated_hue/test_init.py +++ b/tests/components/emulated_hue/test_init.py @@ -1,106 +1,101 @@ """Test the Emulated Hue component.""" -from unittest.mock import MagicMock, Mock, patch +from datetime import timedelta -from homeassistant.components.emulated_hue import Config +from homeassistant.components.emulated_hue import ( + DATA_KEY, + DATA_VERSION, + SAVE_DELAY, + Config, +) +from homeassistant.util import utcnow + +from tests.common import async_fire_time_changed -def test_config_google_home_entity_id_to_number(): +async def test_config_google_home_entity_id_to_number(hass, hass_storage): """Test config adheres to the type.""" - mock_hass = Mock() - mock_hass.config.path = MagicMock("path", return_value="test_path") - conf = Config(mock_hass, {"type": "google_home"}) + conf = Config(hass, {"type": "google_home"}) + hass_storage[DATA_KEY] = { + "version": DATA_VERSION, + "key": DATA_KEY, + "data": {"1": "light.test2"}, + } - with patch( - "homeassistant.components.emulated_hue.load_json", - return_value={"1": "light.test2"}, - ) as json_loader, patch( - "homeassistant.components.emulated_hue.save_json" - ) as json_saver: - number = conf.entity_id_to_number("light.test") - assert number == "2" + await conf.async_setup() - assert json_saver.mock_calls[0][1][1] == { - "1": "light.test2", - "2": "light.test", - } + number = conf.entity_id_to_number("light.test") + assert number == "2" - assert json_saver.call_count == 1 - assert json_loader.call_count == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY)) + await hass.async_block_till_done() + assert hass_storage[DATA_KEY]["data"] == { + "1": "light.test2", + "2": "light.test", + } - number = conf.entity_id_to_number("light.test") - assert number == "2" - assert json_saver.call_count == 1 + number = conf.entity_id_to_number("light.test") + assert number == "2" - number = conf.entity_id_to_number("light.test2") - assert number == "1" - assert json_saver.call_count == 1 + number = conf.entity_id_to_number("light.test2") + assert number == "1" - entity_id = conf.number_to_entity_id("1") - assert entity_id == "light.test2" + entity_id = conf.number_to_entity_id("1") + assert entity_id == "light.test2" -def test_config_google_home_entity_id_to_number_altered(): +async def test_config_google_home_entity_id_to_number_altered(hass, hass_storage): """Test config adheres to the type.""" - mock_hass = Mock() - mock_hass.config.path = MagicMock("path", return_value="test_path") - conf = Config(mock_hass, {"type": "google_home"}) + conf = Config(hass, {"type": "google_home"}) + hass_storage[DATA_KEY] = { + "version": DATA_VERSION, + "key": DATA_KEY, + "data": {"21": "light.test2"}, + } - with patch( - "homeassistant.components.emulated_hue.load_json", - return_value={"21": "light.test2"}, - ) as json_loader, patch( - "homeassistant.components.emulated_hue.save_json" - ) as json_saver: - number = conf.entity_id_to_number("light.test") - assert number == "22" - assert json_saver.call_count == 1 - assert json_loader.call_count == 1 + await conf.async_setup() - assert json_saver.mock_calls[0][1][1] == { - "21": "light.test2", - "22": "light.test", - } + number = conf.entity_id_to_number("light.test") + assert number == "22" - number = conf.entity_id_to_number("light.test") - assert number == "22" - assert json_saver.call_count == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY)) + await hass.async_block_till_done() + assert hass_storage[DATA_KEY]["data"] == { + "21": "light.test2", + "22": "light.test", + } - number = conf.entity_id_to_number("light.test2") - assert number == "21" - assert json_saver.call_count == 1 + number = conf.entity_id_to_number("light.test") + assert number == "22" - entity_id = conf.number_to_entity_id("21") - assert entity_id == "light.test2" + number = conf.entity_id_to_number("light.test2") + assert number == "21" + + entity_id = conf.number_to_entity_id("21") + assert entity_id == "light.test2" -def test_config_google_home_entity_id_to_number_empty(): +async def test_config_google_home_entity_id_to_number_empty(hass, hass_storage): """Test config adheres to the type.""" - mock_hass = Mock() - mock_hass.config.path = MagicMock("path", return_value="test_path") - conf = Config(mock_hass, {"type": "google_home"}) + conf = Config(hass, {"type": "google_home"}) + hass_storage[DATA_KEY] = {"version": DATA_VERSION, "key": DATA_KEY, "data": {}} - with patch( - "homeassistant.components.emulated_hue.load_json", return_value={} - ) as json_loader, patch( - "homeassistant.components.emulated_hue.save_json" - ) as json_saver: - number = conf.entity_id_to_number("light.test") - assert number == "1" - assert json_saver.call_count == 1 - assert json_loader.call_count == 1 + await conf.async_setup() - assert json_saver.mock_calls[0][1][1] == {"1": "light.test"} + number = conf.entity_id_to_number("light.test") + assert number == "1" - number = conf.entity_id_to_number("light.test") - assert number == "1" - assert json_saver.call_count == 1 + async_fire_time_changed(hass, utcnow() + timedelta(seconds=SAVE_DELAY)) + await hass.async_block_till_done() + assert hass_storage[DATA_KEY]["data"] == {"1": "light.test"} - number = conf.entity_id_to_number("light.test2") - assert number == "2" - assert json_saver.call_count == 2 + number = conf.entity_id_to_number("light.test") + assert number == "1" - entity_id = conf.number_to_entity_id("2") - assert entity_id == "light.test2" + number = conf.entity_id_to_number("light.test2") + assert number == "2" + + entity_id = conf.number_to_entity_id("2") + assert entity_id == "light.test2" def test_config_alexa_entity_id_to_number(): diff --git a/tests/components/emulated_roku/test_config_flow.py b/tests/components/emulated_roku/test_config_flow.py index 879d95d0cfc..23c807cbfa3 100644 --- a/tests/components/emulated_roku/test_config_flow.py +++ b/tests/components/emulated_roku/test_config_flow.py @@ -1,4 +1,5 @@ """Tests for emulated_roku config flow.""" +from homeassistant import config_entries from homeassistant.components.emulated_roku import config_flow from tests.common import MockConfigEntry @@ -6,10 +7,10 @@ from tests.common import MockConfigEntry async def test_flow_works(hass): """Test that config flow works.""" - flow = config_flow.EmulatedRokuFlowHandler() - flow.hass = hass - result = await flow.async_step_user( - user_input={"name": "Emulated Roku Test", "listen_port": 8060} + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"name": "Emulated Roku Test", "listen_port": 8060}, ) assert result["type"] == "create_entry" @@ -22,10 +23,12 @@ async def test_flow_already_registered_entry(hass): MockConfigEntry( domain="emulated_roku", data={"name": "Emulated Roku Test", "listen_port": 8062} ).add_to_hass(hass) - flow = config_flow.EmulatedRokuFlowHandler() - flow.hass = hass - result = await flow.async_step_user( - user_input={"name": "Emulated Roku Test", "listen_port": 8062} + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data={"name": "Emulated Roku Test", "listen_port": 8062}, ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/epson/test_config_flow.py b/tests/components/epson/test_config_flow.py index 849a88ba112..3ff7753d3eb 100644 --- a/tests/components/epson/test_config_flow.py +++ b/tests/components/epson/test_config_flow.py @@ -1,35 +1,40 @@ """Test the epson config flow.""" from unittest.mock import patch +from epson_projector.const import PWR_OFF_STATE + from homeassistant import config_entries, setup from homeassistant.components.epson.const import DOMAIN -from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, STATE_UNAVAILABLE +from homeassistant.const import CONF_HOST, CONF_NAME, STATE_UNAVAILABLE + +from tests.common import MockConfigEntry async def test_form(hass): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} - ) + with patch("homeassistant.components.epson.Projector.get_power", return_value="01"): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) assert result["type"] == "form" assert result["errors"] == {} assert result["step_id"] == config_entries.SOURCE_USER - with patch( - "homeassistant.components.epson.Projector.get_property", - return_value="04", + "homeassistant.components.epson.Projector.get_power", + return_value="01", ), patch( "homeassistant.components.epson.async_setup_entry", return_value=True, ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) + assert result2["type"] == "create_entry" assert result2["title"] == "test-epson" - assert result2["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80} + assert result2["data"] == {CONF_HOST: "1.1.1.1"} await hass.async_block_till_done() assert len(mock_setup_entry.mock_calls) == 1 @@ -41,21 +46,43 @@ async def test_form_cannot_connect(hass): ) with patch( - "homeassistant.components.epson.Projector.get_property", + "homeassistant.components.epson.Projector.get_power", return_value=STATE_UNAVAILABLE, ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} +async def test_form_powered_off(hass): + """Test we handle powered off during initial configuration.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.epson.Projector.get_power", + return_value=PWR_OFF_STATE, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "powered_off"} + + async def test_import(hass): """Test config.yaml import.""" with patch( + "homeassistant.components.epson.Projector.get_power", + return_value="01", + ), patch( "homeassistant.components.epson.Projector.get_property", return_value="04", ), patch( @@ -65,27 +92,53 @@ async def test_import(hass): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, - data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result["type"] == "create_entry" - assert result["title"] == "test-epson" - assert result["data"] == {CONF_HOST: "1.1.1.1", CONF_PORT: 80} + assert result["type"] == "create_entry" + assert result["title"] == "test-epson" + assert result["data"] == {CONF_HOST: "1.1.1.1"} + + +async def test_already_imported(hass): + """Test config.yaml imported twice.""" + MockConfigEntry( + domain=DOMAIN, + source=config_entries.SOURCE_IMPORT, + unique_id="bla", + title="test-epson", + data={CONF_HOST: "1.1.1.1"}, + ).add_to_hass(hass) + + with patch( + "homeassistant.components.epson.Projector.get_power", + return_value="01", + ), patch( + "homeassistant.components.epson.Projector.get_property", + return_value="04", + ), patch( + "homeassistant.components.epson.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" async def test_import_cannot_connect(hass): - """Test we handle cannot connect error with import.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT} - ) - + """Test we handle cannot connect error.""" with patch( - "homeassistant.components.epson.Projector.get_property", + "homeassistant.components.epson.Projector.get_power", return_value=STATE_UNAVAILABLE, ): - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - {CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson", CONF_PORT: 80}, + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={CONF_HOST: "1.1.1.1", CONF_NAME: "test-epson"}, ) - assert result2["type"] == "form" - assert result2["errors"] == {"base": "cannot_connect"} + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" diff --git a/tests/components/filter/test_sensor.py b/tests/components/filter/test_sensor.py index b8cdaf3c88a..60fae0fc5be 100644 --- a/tests/components/filter/test_sensor.py +++ b/tests/components/filter/test_sensor.py @@ -81,7 +81,6 @@ async def test_chain(hass, values): async def test_chain_history(hass, values, missing=False): """Test if filter chaining works.""" config = { - "history": {}, "sensor": { "platform": "filter", "name": "test", @@ -94,7 +93,6 @@ async def test_chain_history(hass, values, missing=False): }, } await async_init_recorder_component(hass) - assert_setup_component(1, "history") t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) @@ -114,10 +112,10 @@ async def test_chain_history(hass, values, missing=False): } with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, ), patch( - "homeassistant.components.history.get_last_state_changes", + "homeassistant.components.recorder.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): @@ -208,7 +206,6 @@ async def test_chain_history_missing(hass, values): async def test_history_time(hass): """Test loading from history based on a time window.""" config = { - "history": {}, "sensor": { "platform": "filter", "name": "test", @@ -217,7 +214,6 @@ async def test_history_time(hass): }, } await async_init_recorder_component(hass) - assert_setup_component(1, "history") t_0 = dt_util.utcnow() - timedelta(minutes=1) t_1 = dt_util.utcnow() - timedelta(minutes=2) @@ -231,10 +227,10 @@ async def test_history_time(hass): ] } with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, ), patch( - "homeassistant.components.history.get_last_state_changes", + "homeassistant.components.recorder.history.get_last_state_changes", return_value=fake_states, ): with assert_setup_component(1, "sensor"): diff --git a/tests/components/flume/test_config_flow.py b/tests/components/flume/test_config_flow.py index 3a9e3376f05..5c439933b0b 100644 --- a/tests/components/flume/test_config_flow.py +++ b/tests/components/flume/test_config_flow.py @@ -12,6 +12,8 @@ from homeassistant.const import ( CONF_USERNAME, ) +from tests.common import MockConfigEntry + def _get_mocked_flume_device_list(): flume_device_list_mock = MagicMock() @@ -124,7 +126,7 @@ async def test_form_invalid_auth(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {"password": "invalid_auth"} async def test_form_cannot_connect(hass): @@ -151,3 +153,82 @@ async def test_form_cannot_connect(hass): assert result2["type"] == "form" assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth(hass): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test@test.org", + CONF_CLIENT_ID: "client_id", + CONF_CLIENT_SECRET: "client_secret", + }, + unique_id="test@test.org", + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"password": "invalid_auth"} + + with patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + side_effect=requests.exceptions.ConnectionError(), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + + mock_flume_device_list = _get_mocked_flume_device_list() + + with patch( + "homeassistant.components.flume.config_flow.FlumeAuth", + return_value=True, + ), patch( + "homeassistant.components.flume.config_flow.FlumeDeviceList", + return_value=mock_flume_device_list, + ), patch( + "homeassistant.components.flume.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert mock_setup_entry.called + assert result4["type"] == "abort" + assert result4["reason"] == "reauth_successful" diff --git a/tests/components/flux/test_switch.py b/tests/components/flux/test_switch.py index 7438b690ab5..e331a788e9c 100644 --- a/tests/components/flux/test_switch.py +++ b/tests/components/flux/test_switch.py @@ -127,7 +127,9 @@ async def test_invalid_config_no_lights(hass): await hass.async_block_till_done() -async def test_flux_when_switch_is_off(hass, legacy_patchable_time): +async def test_flux_when_switch_is_off( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch when it is off.""" platform = getattr(hass.components, "test.light") platform.init() @@ -178,7 +180,9 @@ async def test_flux_when_switch_is_off(hass, legacy_patchable_time): assert not turn_on_calls -async def test_flux_before_sunrise(hass, legacy_patchable_time): +async def test_flux_before_sunrise( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -237,7 +241,9 @@ async def test_flux_before_sunrise(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.606, 0.379] -async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time): +async def test_flux_before_sunrise_known_location( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -296,7 +302,9 @@ async def test_flux_before_sunrise_known_location(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time): +async def test_flux_after_sunrise_before_sunset( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch after sunrise and before sunset.""" platform = getattr(hass.components, "test.light") platform.init() @@ -355,7 +363,9 @@ async def test_flux_after_sunrise_before_sunset(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time): +async def test_flux_after_sunset_before_stop( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch after sunset and before stop.""" platform = getattr(hass.components, "test.light") platform.init() @@ -415,7 +425,9 @@ async def test_flux_after_sunset_before_stop(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time): +async def test_flux_after_stop_before_sunrise( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch after stop and before sunrise.""" platform = getattr(hass.components, "test.light") platform.init() @@ -474,7 +486,9 @@ async def test_flux_after_stop_before_sunrise(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time): +async def test_flux_with_custom_start_stop_times( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux with custom start and stop times.""" platform = getattr(hass.components, "test.light") platform.init() @@ -534,7 +548,9 @@ async def test_flux_with_custom_start_stop_times(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.504, 0.385] -async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time): +async def test_flux_before_sunrise_stop_next_day( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch before sunrise. This test has the stop_time on the next day (after midnight). @@ -598,7 +614,7 @@ async def test_flux_before_sunrise_stop_next_day(hass, legacy_patchable_time): # pylint: disable=invalid-name async def test_flux_after_sunrise_before_sunset_stop_next_day( - hass, legacy_patchable_time + hass, legacy_patchable_time, enable_custom_integrations ): """ Test the flux switch after sunrise and before sunset. @@ -665,7 +681,7 @@ async def test_flux_after_sunrise_before_sunset_stop_next_day( # pylint: disable=invalid-name @pytest.mark.parametrize("x", [0, 1]) async def test_flux_after_sunset_before_midnight_stop_next_day( - hass, legacy_patchable_time, x + hass, legacy_patchable_time, x, enable_custom_integrations ): """Test the flux switch after sunset and before stop. @@ -730,7 +746,7 @@ async def test_flux_after_sunset_before_midnight_stop_next_day( # pylint: disable=invalid-name async def test_flux_after_sunset_after_midnight_stop_next_day( - hass, legacy_patchable_time + hass, legacy_patchable_time, enable_custom_integrations ): """Test the flux switch after sunset and before stop. @@ -795,7 +811,7 @@ async def test_flux_after_sunset_after_midnight_stop_next_day( # pylint: disable=invalid-name async def test_flux_after_stop_before_sunrise_stop_next_day( - hass, legacy_patchable_time + hass, legacy_patchable_time, enable_custom_integrations ): """Test the flux switch after stop and before sunrise. @@ -859,7 +875,9 @@ async def test_flux_after_stop_before_sunrise_stop_next_day( # pylint: disable=invalid-name -async def test_flux_with_custom_colortemps(hass, legacy_patchable_time): +async def test_flux_with_custom_colortemps( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux with custom start and stop colortemps.""" platform = getattr(hass.components, "test.light") platform.init() @@ -921,7 +939,9 @@ async def test_flux_with_custom_colortemps(hass, legacy_patchable_time): # pylint: disable=invalid-name -async def test_flux_with_custom_brightness(hass, legacy_patchable_time): +async def test_flux_with_custom_brightness( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux with custom start and stop colortemps.""" platform = getattr(hass.components, "test.light") platform.init() @@ -981,7 +1001,9 @@ async def test_flux_with_custom_brightness(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.506, 0.385] -async def test_flux_with_multiple_lights(hass, legacy_patchable_time): +async def test_flux_with_multiple_lights( + hass, legacy_patchable_time, enable_custom_integrations +): """Test the flux switch with multiple light entities.""" platform = getattr(hass.components, "test.light") platform.init() @@ -1064,7 +1086,7 @@ async def test_flux_with_multiple_lights(hass, legacy_patchable_time): assert call.data[light.ATTR_XY_COLOR] == [0.46, 0.376] -async def test_flux_with_mired(hass, legacy_patchable_time): +async def test_flux_with_mired(hass, legacy_patchable_time, enable_custom_integrations): """Test the flux switch´s mode mired.""" platform = getattr(hass.components, "test.light") platform.init() @@ -1121,7 +1143,7 @@ async def test_flux_with_mired(hass, legacy_patchable_time): assert call.data[light.ATTR_COLOR_TEMP] == 269 -async def test_flux_with_rgb(hass, legacy_patchable_time): +async def test_flux_with_rgb(hass, legacy_patchable_time, enable_custom_integrations): """Test the flux switch´s mode rgb.""" platform = getattr(hass.components, "test.light") platform.init() diff --git a/tests/components/forked_daapd/test_config_flow.py b/tests/components/forked_daapd/test_config_flow.py index 843aa12a759..668f1be0a4f 100644 --- a/tests/components/forked_daapd/test_config_flow.py +++ b/tests/components/forked_daapd/test_config_flow.py @@ -11,11 +11,7 @@ from homeassistant.components.forked_daapd.const import ( CONF_TTS_VOLUME, DOMAIN, ) -from homeassistant.config_entries import ( - CONN_CLASS_LOCAL_PUSH, - SOURCE_USER, - SOURCE_ZEROCONF, -) +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT from tests.common import MockConfigEntry @@ -50,9 +46,7 @@ def config_entry_fixture(): title="", data=data, options={}, - system_options={}, source=SOURCE_USER, - connection_class=CONN_CLASS_LOCAL_PUSH, entry_id=1, ) diff --git a/tests/components/forked_daapd/test_media_player.py b/tests/components/forked_daapd/test_media_player.py index ffbf7a569f9..a2e0050c3d9 100644 --- a/tests/components/forked_daapd/test_media_player.py +++ b/tests/components/forked_daapd/test_media_player.py @@ -54,7 +54,7 @@ from homeassistant.components.media_player.const import ( MEDIA_TYPE_MUSIC, MEDIA_TYPE_TVSHOW, ) -from homeassistant.config_entries import CONN_CLASS_LOCAL_PUSH, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME, @@ -282,9 +282,7 @@ def config_entry_fixture(): title="", data=data, options={CONF_TTS_PAUSE_TIME: 0}, - system_options={}, source=SOURCE_USER, - connection_class=CONN_CLASS_LOCAL_PUSH, entry_id=1, ) diff --git a/tests/components/freebox/test_init.py b/tests/components/freebox/test_init.py index 6b5589ac647..44af000f79a 100644 --- a/tests/components/freebox/test_init.py +++ b/tests/components/freebox/test_init.py @@ -5,7 +5,7 @@ from homeassistant.components.device_tracker import DOMAIN as DT_DOMAIN from homeassistant.components.freebox.const import DOMAIN as DOMAIN, SERVICE_REBOOT from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -85,7 +85,7 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock): assert await async_setup_component(hass, DOMAIN, {}) is True await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state_dt = hass.states.get(entity_id_dt) assert state_dt state_sensor = hass.states.get(entity_id_sensor) @@ -95,7 +95,7 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock): await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state_dt = hass.states.get(entity_id_dt) assert state_dt.state == STATE_UNAVAILABLE state_sensor = hass.states.get(entity_id_sensor) @@ -110,7 +110,7 @@ async def test_unload_remove(hass: HomeAssistant, router: Mock): await hass.async_block_till_done() assert router().close.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state_dt = hass.states.get(entity_id_dt) assert state_dt is None state_sensor = hass.states.get(entity_id_sensor) diff --git a/tests/components/fritz/__init__.py b/tests/components/fritz/__init__.py index 5a9b6cb1652..a1fd1ce42fb 100644 --- a/tests/components/fritz/__init__.py +++ b/tests/components/fritz/__init__.py @@ -49,7 +49,7 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods "NewBytesReceived": 12045, }, ("DeviceInfo:1", "GetInfo"): { - "NewSerialNumber": 1234, + "NewSerialNumber": "abcdefgh", "NewName": "TheName", "NewModelName": "FRITZ!Box 7490", }, @@ -106,23 +106,22 @@ class FritzConnectionMock: # pylint: disable=too-few-public-methods def __init__(self): """Inint Mocking class.""" type(self).modelname = mock.PropertyMock(return_value=self.MODELNAME) - self.call_action = mock.Mock(side_effect=self._side_effect_callaction) - type(self).actionnames = mock.PropertyMock( - side_effect=self._side_effect_actionnames + self.call_action = mock.Mock(side_effect=self._side_effect_call_action) + type(self).action_names = mock.PropertyMock( + side_effect=self._side_effect_action_names ) services = { srv: None - for srv, _ in list(self.FRITZBOX_DATA.keys()) - + list(self.FRITZBOX_DATA_INDEXED.keys()) + for srv, _ in list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) } type(self).services = mock.PropertyMock(side_effect=[services]) - def _side_effect_callaction(self, service, action, **kwargs): + def _side_effect_call_action(self, service, action, **kwargs): if kwargs: index = next(iter(kwargs.values())) return self.FRITZBOX_DATA_INDEXED[(service, action)][index] return self.FRITZBOX_DATA[(service, action)] - def _side_effect_actionnames(self): - return list(self.FRITZBOX_DATA.keys()) + list(self.FRITZBOX_DATA_INDEXED.keys()) + def _side_effect_action_names(self): + return list(self.FRITZBOX_DATA) + list(self.FRITZBOX_DATA_INDEXED) diff --git a/tests/components/fritz/test_config_flow.py b/tests/components/fritz/test_config_flow.py index 14830249da9..6e051ef1bdd 100644 --- a/tests/components/fritz/test_config_flow.py +++ b/tests/components/fritz/test_config_flow.py @@ -4,10 +4,14 @@ from unittest.mock import patch from fritzconnection.core.exceptions import FritzConnectionException, FritzSecurityError import pytest +from homeassistant.components.device_tracker.const import ( + CONF_CONSIDER_HOME, + DEFAULT_CONSIDER_HOME, +) from homeassistant.components.fritz.const import ( DOMAIN, ERROR_AUTH_INVALID, - ERROR_CONNECTION_ERROR, + ERROR_CANNOT_CONNECT, ERROR_UNKNOWN, ) from homeassistant.components.ssdp import ( @@ -54,11 +58,11 @@ MOCK_SSDP_DATA = { @pytest.fixture() -def fc_class_mock(mocker): +def fc_class_mock(): """Fixture that sets up a mocked FritzConnection class.""" - result = mocker.patch("fritzconnection.FritzConnection", autospec=True) - result.return_value = FritzConnectionMock() - yield result + with patch("fritzconnection.FritzConnection", autospec=True) as result: + result.return_value = FritzConnectionMock() + yield result async def test_user(hass: HomeAssistant, fc_class_mock): @@ -83,6 +87,10 @@ async def test_user(hass: HomeAssistant, fc_class_mock): assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_PASSWORD] == "fake_pass" assert result["data"][CONF_USERNAME] == "fake_user" + assert ( + result["options"][CONF_CONSIDER_HOME] + == DEFAULT_CONSIDER_HOME.total_seconds() + ) assert not result["result"].unique_id await hass.async_block_till_done() @@ -111,6 +119,7 @@ async def test_user_already_configured(hass: HomeAssistant, fc_class_mock): ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" + assert result["errors"]["base"] == "already_configured" async def test_exception_security(hass: HomeAssistant): @@ -156,7 +165,7 @@ async def test_exception_connection(hass: HomeAssistant): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" - assert result["errors"]["base"] == ERROR_CONNECTION_ERROR + assert result["errors"]["base"] == ERROR_CANNOT_CONNECT async def test_exception_unknown(hass: HomeAssistant): @@ -248,6 +257,7 @@ async def test_reauth_not_successful(hass: HomeAssistant, fc_class_mock): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" + assert result["errors"]["base"] == "cannot_connect" async def test_ssdp_already_configured(hass: HomeAssistant, fc_class_mock): @@ -414,3 +424,30 @@ async def test_import(hass: HomeAssistant, fc_class_mock): await hass.async_block_till_done() assert mock_setup_entry.called + + +async def test_options_flow(hass: HomeAssistant, fc_class_mock): + """Test options flow.""" + + mock_config = MockConfigEntry(domain=DOMAIN, data=MOCK_USER_DATA) + mock_config.add_to_hass(hass) + + with patch( + "homeassistant.components.fritz.common.FritzConnection", + side_effect=fc_class_mock, + ), patch("homeassistant.components.fritz.common.FritzStatus"), patch( + "homeassistant.components.fritz.common.FritzBoxTools" + ): + result = await hass.config_entries.options.async_init(mock_config.entry_id) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "init" + + result = await hass.config_entries.options.async_init(mock_config.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + user_input={ + CONF_CONSIDER_HOME: 37, + }, + ) + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert mock_config.options[CONF_CONSIDER_HOME] == 37 diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 14df6f869f8..438335868cd 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -8,11 +8,7 @@ from requests.exceptions import HTTPError 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, - ENTRY_STATE_SETUP_ERROR, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -94,14 +90,14 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock): assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get(entity_id) assert state await hass.config_entries.async_unload(entry.entry_id) assert fritz().logout.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert state.state == STATE_UNAVAILABLE @@ -109,13 +105,13 @@ async def test_unload_remove(hass: HomeAssistant, fritz: Mock): await hass.async_block_till_done() assert fritz().logout.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED state = hass.states.get(entity_id) assert state is None async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant): - """Config entry state is ENTRY_STATE_SETUP_RETRY when fritzbox is offline.""" + """Config entry state is SETUP_RETRY when fritzbox is offline.""" entry = MockConfigEntry( domain=FB_DOMAIN, data={CONF_HOST: "any", **MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0]}, @@ -132,4 +128,4 @@ async def test_raise_config_entry_not_ready_when_offline(hass: HomeAssistant): entries = hass.config_entries.async_entries() config_entry = entries[0] - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/frontend/test_init.py b/tests/components/frontend/test_init.py index fe624452475..9746fc6d838 100644 --- a/tests/components/frontend/test_init.py +++ b/tests/components/frontend/test_init.py @@ -26,6 +26,25 @@ from tests.common import async_capture_events, async_fire_time_changed MOCK_THEMES = { "happy": {"primary-color": "red", "app-header-background-color": "blue"}, "dark": {"primary-color": "black"}, + "light_only": { + "primary-color": "blue", + "modes": { + "light": {"secondary-color": "black"}, + }, + }, + "dark_only": { + "primary-color": "blue", + "modes": { + "dark": {"secondary-color": "white"}, + }, + }, + "light_and_dark": { + "primary-color": "blue", + "modes": { + "light": {"secondary-color": "black"}, + "dark": {"secondary-color": "white"}, + }, + }, } CONFIG_THEMES = {DOMAIN: {CONF_THEMES: MOCK_THEMES}} @@ -279,6 +298,15 @@ async def test_themes_set_dark_theme(hass, themes_ws_client): assert msg["result"]["default_dark_theme"] is None + await hass.services.async_call( + DOMAIN, "set_theme", {"name": "light_and_dark", "mode": "dark"}, blocking=True + ) + + await themes_ws_client.send_json({"id": 8, "type": "frontend/get_themes"}) + msg = await themes_ws_client.receive_json() + + assert msg["result"]["default_dark_theme"] == "light_and_dark" + async def test_themes_set_dark_theme_wrong_name(hass, frontend, themes_ws_client): """Test frontend.set_theme service called with mode dark and wrong name.""" diff --git a/tests/components/garages_amsterdam/__init__.py b/tests/components/garages_amsterdam/__init__.py new file mode 100644 index 00000000000..ff430c0e7b2 --- /dev/null +++ b/tests/components/garages_amsterdam/__init__.py @@ -0,0 +1 @@ +"""Tests for the Garages Amsterdam integration.""" diff --git a/tests/components/garages_amsterdam/conftest.py b/tests/components/garages_amsterdam/conftest.py new file mode 100644 index 00000000000..49d242dabd5 --- /dev/null +++ b/tests/components/garages_amsterdam/conftest.py @@ -0,0 +1,32 @@ +"""Test helpers.""" + +from unittest.mock import Mock, patch + +import pytest + + +@pytest.fixture(autouse=True) +def mock_cases(): + """Mock garages_amsterdam garages.""" + with patch( + "garages_amsterdam.get_garages", + return_value=[ + Mock( + garage_name="IJDok", + free_space_short=100, + free_space_long=10, + short_capacity=120, + long_capacity=60, + state="ok", + ), + Mock( + garage_name="Arena", + free_space_short=200, + free_space_long=20, + short_capacity=240, + long_capacity=80, + state="error", + ), + ], + ) as mock_get_garages: + yield mock_get_garages diff --git a/tests/components/garages_amsterdam/test_config_flow.py b/tests/components/garages_amsterdam/test_config_flow.py new file mode 100644 index 00000000000..464fcb799ad --- /dev/null +++ b/tests/components/garages_amsterdam/test_config_flow.py @@ -0,0 +1,65 @@ +"""Test the Garages Amsterdam config flow.""" +from unittest.mock import patch + +from aiohttp import ClientResponseError +import pytest + +from homeassistant import config_entries, setup +from homeassistant.components.garages_amsterdam.const import DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + + +async def test_full_flow(hass: HomeAssistant) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + with patch( + "homeassistant.components.garages_amsterdam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"garage_name": "IJDok"}, + ) + await hass.async_block_till_done() + + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "IJDok" + assert "result" in result2 + assert result2["result"].unique_id == "IJDok" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "side_effect,reason", + [ + (RuntimeError, "unknown"), + (ClientResponseError(None, None, status=500), "cannot_connect"), + ], +) +async def test_error_handling( + side_effect: Exception, reason: str, hass: HomeAssistant +) -> None: + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + with patch( + "homeassistant.components.garages_amsterdam.config_flow.garages_amsterdam.get_garages", + side_effect=side_effect, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == reason diff --git a/tests/components/garmin_connect/test_config_flow.py b/tests/components/garmin_connect/test_config_flow.py index f3784d5e2e2..2ad36ffa29c 100644 --- a/tests/components/garmin_connect/test_config_flow.py +++ b/tests/components/garmin_connect/test_config_flow.py @@ -1,7 +1,7 @@ """Test the Garmin Connect config flow.""" from unittest.mock import patch -from garminconnect import ( +from garminconnect_aio import ( GarminConnectAuthenticationError, GarminConnectConnectionError, GarminConnectTooManyRequestsError, @@ -15,7 +15,7 @@ from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry MOCK_CONF = { - CONF_ID: "First Lastname", + CONF_ID: "my@email.address", CONF_USERNAME: "my@email.address", CONF_PASSWORD: "mypassw0rd", } @@ -23,27 +23,33 @@ MOCK_CONF = { @pytest.fixture(name="mock_garmin_connect") def mock_garmin(): - """Mock Garmin.""" + """Mock Garmin Connect.""" with patch( "homeassistant.components.garmin_connect.config_flow.Garmin", ) as garmin: - garmin.return_value.get_full_name.return_value = MOCK_CONF[CONF_ID] + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] yield garmin.return_value async def test_show_form(hass): """Test that the form is served with no input.""" + 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" + assert result["errors"] == {} + assert result["step_id"] == config_entries.SOURCE_USER -async def test_step_user(hass, mock_garmin_connect): +async def test_step_user(hass): """Test registering an integration and finishing flow works.""" with patch( + "homeassistant.components.garmin_connect.Garmin.login", + return_value="my@email.address", + ), patch( "homeassistant.components.garmin_connect.async_setup_entry", return_value=True ): result = await hass.config_entries.flow.async_init( @@ -95,12 +101,18 @@ async def test_unknown_error(hass, mock_garmin_connect): assert result["errors"] == {"base": "unknown"} -async def test_abort_if_already_setup(hass, mock_garmin_connect): +async def test_abort_if_already_setup(hass): """Test abort if already setup.""" - entry = MockConfigEntry(domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID]) - entry.add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_CONF - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" + MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_ID] + ).add_to_hass(hass) + with patch( + "homeassistant.components.garmin_connect.config_flow.Garmin", autospec=True + ) as garmin: + garmin.return_value.login.return_value = MOCK_CONF[CONF_ID] + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"}, data=MOCK_CONF + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" diff --git a/tests/components/generic/test_camera.py b/tests/components/generic/test_camera.py index deb8049da33..1a1edc4eece 100644 --- a/tests/components/generic/test_camera.py +++ b/tests/components/generic/test_camera.py @@ -440,7 +440,7 @@ async def test_timeout_cancelled(hass, hass_client): respx.get("http://example.com").respond(text="not hello world") with patch( - "homeassistant.components.generic.camera.GenericCamera._async_camera_image", + "homeassistant.components.generic.camera.GenericCamera.async_camera_image", side_effect=asyncio.CancelledError(), ): resp = await client.get("/api/camera_proxy/camera.config_test") diff --git a/tests/components/generic_thermostat/__init__.py b/tests/components/generic_thermostat/__init__.py new file mode 100644 index 00000000000..10482cd30d0 --- /dev/null +++ b/tests/components/generic_thermostat/__init__.py @@ -0,0 +1 @@ +"""generic_thermostat tests.""" diff --git a/tests/components/generic_thermostat/test_climate.py b/tests/components/generic_thermostat/test_climate.py index f5a27ac8b97..4b9fbca41e2 100644 --- a/tests/components/generic_thermostat/test_climate.py +++ b/tests/components/generic_thermostat/test_climate.py @@ -4,7 +4,6 @@ from os import path from unittest.mock import patch import pytest -import pytz import voluptuous as vol from homeassistant import config as hass_config @@ -37,6 +36,7 @@ import homeassistant.core as ha from homeassistant.core import DOMAIN as HASS_DOMAIN, CoreState, State, callback from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import ( @@ -125,7 +125,7 @@ async def test_heater_input_boolean(hass, setup_comp_1): assert hass.states.get(heater_switch).state == STATE_ON -async def test_heater_switch(hass, setup_comp_1): +async def test_heater_switch(hass, setup_comp_1, enable_custom_integrations): """Test heater switching test switch.""" platform = getattr(hass.components, "test.switch") platform.init() @@ -691,9 +691,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_on_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -719,9 +717,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough(hass, setup_comp_4): async def test_temp_change_ac_trigger_off_long_enough(hass, setup_comp_4): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -801,9 +797,7 @@ async def test_temp_change_ac_trigger_on_not_long_enough_2(hass, setup_comp_5): async def test_temp_change_ac_trigger_on_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -829,9 +823,7 @@ async def test_temp_change_ac_trigger_off_not_long_enough_2(hass, setup_comp_5): async def test_temp_change_ac_trigger_off_long_enough_2(hass, setup_comp_5): """Test if temperature change turn ac on.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -919,9 +911,7 @@ async def test_temp_change_heater_trigger_on_not_long_enough(hass, setup_comp_6) async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): """Test if temperature change turn heater on after min cycle.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -938,9 +928,7 @@ async def test_temp_change_heater_trigger_on_long_enough(hass, setup_comp_6): async def test_temp_change_heater_trigger_off_long_enough(hass, setup_comp_6): """Test if temperature change turn heater off after min cycle.""" - fake_changed = datetime.datetime( - 1918, 11, 11, 11, 11, 11, tzinfo=datetime.timezone.utc - ) + fake_changed = datetime.datetime(1918, 11, 11, 11, 11, 11, tzinfo=dt_util.UTC) with patch( "homeassistant.helpers.condition.dt_util.utcnow", return_value=fake_changed ): @@ -1019,7 +1007,7 @@ async def test_temp_change_ac_trigger_on_long_enough_3(hass, setup_comp_7): _setup_sensor(hass, 30) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 @@ -1042,7 +1030,7 @@ async def test_temp_change_ac_trigger_off_long_enough_3(hass, setup_comp_7): _setup_sensor(hass, 20) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 @@ -1090,7 +1078,7 @@ async def test_temp_change_heater_trigger_on_long_enough_2(hass, setup_comp_8): _setup_sensor(hass, 20) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 @@ -1113,7 +1101,7 @@ async def test_temp_change_heater_trigger_off_long_enough_2(hass, setup_comp_8): _setup_sensor(hass, 30) await hass.async_block_till_done() await common.async_set_temperature(hass, 25) - test_time = datetime.datetime.now(pytz.UTC) + test_time = datetime.datetime.now(dt_util.UTC) async_fire_time_changed(hass, test_time) await hass.async_block_till_done() assert len(calls) == 0 diff --git a/tests/components/gios/__init__.py b/tests/components/gios/__init__.py index 6b1aa982c71..537d6265125 100644 --- a/tests/components/gios/__init__.py +++ b/tests/components/gios/__init__.py @@ -17,7 +17,7 @@ async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: entry = MockConfigEntry( domain=DOMAIN, title="Home", - unique_id=123, + unique_id="123", data={"station_id": 123, "name": "Home"}, ) @@ -26,8 +26,8 @@ async def init_integration(hass, incomplete_data=False) -> MockConfigEntry: sensors = json.loads(load_fixture("gios/sensors.json")) if incomplete_data: indexes["stIndexLevel"]["indexLevelName"] = "foo" - sensors["PM10"]["values"][0]["value"] = None - sensors["PM10"]["values"][1]["value"] = None + sensors["pm10"]["values"][0]["value"] = None + sensors["pm10"]["values"][1]["value"] = None with patch( "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS diff --git a/tests/components/gios/test_air_quality.py b/tests/components/gios/test_air_quality.py index 873e1e089a3..b7ce8d1f97a 100644 --- a/tests/components/gios/test_air_quality.py +++ b/tests/components/gios/test_air_quality.py @@ -13,9 +13,10 @@ from homeassistant.components.air_quality import ( ATTR_PM_2_5, ATTR_PM_10, ATTR_SO2, + DOMAIN as AIR_QUALITY_DOMAIN, ) from homeassistant.components.gios.air_quality import ATTRIBUTION -from homeassistant.components.gios.const import AQI_GOOD +from homeassistant.components.gios.const import AQI_GOOD, DOMAIN from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_ICON, @@ -55,7 +56,7 @@ async def test_air_quality(hass): entry = registry.async_get("air_quality.home") assert entry - assert entry.unique_id == 123 + assert entry.unique_id == "123" async def test_air_quality_with_incomplete_data(hass): @@ -83,7 +84,7 @@ async def test_air_quality_with_incomplete_data(hass): entry = registry.async_get("air_quality.home") assert entry - assert entry.unique_id == 123 + assert entry.unique_id == "123" async def test_availability(hass): @@ -122,3 +123,23 @@ async def test_availability(hass): assert state assert state.state != STATE_UNAVAILABLE assert state.state == "4" + + +async def test_migrate_unique_id(hass): + """Test migrate unique_id of the air_quality entity.""" + registry = er.async_get(hass) + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + AIR_QUALITY_DOMAIN, + DOMAIN, + 123, + suggested_object_id="home", + disabled_by=None, + ) + + await init_integration(hass) + + entry = registry.async_get("air_quality.home") + assert entry + assert entry.unique_id == "123" diff --git a/tests/components/gios/test_config_flow.py b/tests/components/gios/test_config_flow.py index 830b3a198a5..6b1f829c4d8 100644 --- a/tests/components/gios/test_config_flow.py +++ b/tests/components/gios/test_config_flow.py @@ -102,4 +102,4 @@ async def test_create_entry(hass): assert result["title"] == CONFIG[CONF_STATION_ID] assert result["data"][CONF_STATION_ID] == CONFIG[CONF_STATION_ID] - assert flow.context["unique_id"] == CONFIG[CONF_STATION_ID] + assert flow.context["unique_id"] == "123" diff --git a/tests/components/gios/test_init.py b/tests/components/gios/test_init.py index 344afe4e047..08629608cd4 100644 --- a/tests/components/gios/test_init.py +++ b/tests/components/gios/test_init.py @@ -1,15 +1,14 @@ """Test init of GIOS integration.""" +import json from unittest.mock import patch from homeassistant.components.gios.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE -from tests.common import MockConfigEntry +from . import STATIONS + +from tests.common import MockConfigEntry, load_fixture, mock_device_registry from tests.components.gios import init_integration @@ -38,7 +37,7 @@ async def test_config_not_ready(hass): ): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -46,10 +45,53 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) + + +async def test_migrate_device_and_config_entry(hass): + """Test device_info identifiers and config entry migration.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + title="Home", + unique_id=123, + data={ + "station_id": 123, + "name": "Home", + }, + ) + + indexes = json.loads(load_fixture("gios/indexes.json")) + station = json.loads(load_fixture("gios/station.json")) + sensors = json.loads(load_fixture("gios/sensors.json")) + + with patch( + "homeassistant.components.gios.Gios._get_stations", return_value=STATIONS + ), patch( + "homeassistant.components.gios.Gios._get_station", + return_value=station, + ), patch( + "homeassistant.components.gios.Gios._get_all_sensors", + return_value=sensors, + ), patch( + "homeassistant.components.gios.Gios._get_indexes", return_value=indexes + ): + config_entry.add_to_hass(hass) + + device_reg = mock_device_registry(hass) + device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, 123)} + ) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + migrated_device_entry = device_reg.async_get_or_create( + config_entry_id=config_entry.entry_id, identifiers={(DOMAIN, "123")} + ) + assert device_entry.id == migrated_device_entry.id diff --git a/tests/components/goalzero/__init__.py b/tests/components/goalzero/__init__.py index fb531dfca4b..1b5302dbc1b 100644 --- a/tests/components/goalzero/__init__.py +++ b/tests/components/goalzero/__init__.py @@ -2,6 +2,7 @@ from unittest.mock import AsyncMock, patch +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.const import CONF_HOST, CONF_NAME HOST = "1.2.3.4" @@ -17,6 +18,12 @@ CONF_CONFIG_FLOW = { CONF_NAME: NAME, } +CONF_DHCP_FLOW = { + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", +} + async def _create_mocked_yeti(raise_exception=False): mocked_yeti = AsyncMock() diff --git a/tests/components/goalzero/test_config_flow.py b/tests/components/goalzero/test_config_flow.py index 10ef02bfcff..6df5465eff9 100644 --- a/tests/components/goalzero/test_config_flow.py +++ b/tests/components/goalzero/test_config_flow.py @@ -4,16 +4,18 @@ from unittest.mock import patch from goalzero import exceptions from homeassistant.components.goalzero.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER from homeassistant.data_entry_flow import ( RESULT_TYPE_ABORT, RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM, ) +from homeassistant.setup import async_setup_component from . import ( CONF_CONFIG_FLOW, CONF_DATA, + CONF_DHCP_FLOW, CONF_HOST, CONF_NAME, NAME, @@ -114,3 +116,69 @@ async def test_flow_user_unknown_error(hass): assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} + + +async def test_dhcp_discovery(hass): + """Test we can process the discovery from dhcp.""" + await async_setup_component(hass, "persistent_notification", {}) + mocked_yeti = await _create_mocked_yeti() + with _patch_config_flow_yeti(mocked_yeti), _patch_setup(): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == "form" + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_CREATE_ENTRY + assert result["data"] == { + CONF_HOST: "1.1.1.1", + CONF_NAME: "Yeti", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dhcp_discovery_failed(hass): + """Test failed setup from dhcp.""" + mocked_yeti = await _create_mocked_yeti(True) + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = exceptions.ConnectError + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" + + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = exceptions.InvalidHost + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "invalid_host" + + with _patch_config_flow_yeti(mocked_yeti) as yetimock: + yetimock.side_effect = Exception + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data=CONF_DHCP_FLOW, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "unknown" diff --git a/tests/components/gogogate2/__init__.py b/tests/components/gogogate2/__init__.py index bc867ab646b..f7e3d40a44b 100644 --- a/tests/components/gogogate2/__init__.py +++ b/tests/components/gogogate2/__init__.py @@ -1 +1,139 @@ """Tests for the GogoGate2 component.""" + +from ismartgate.common import ( + DoorMode, + DoorStatus, + GogoGate2Door, + GogoGate2InfoResponse, + ISmartGateDoor, + ISmartGateInfoResponse, + Network, + Outputs, + Wifi, +) + + +def _mocked_gogogate_open_door_response(): + return GogoGate2InfoResponse( + user="user1", + gogogatename="gogogatename0", + model="gogogate2", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc123.blah.blah", + firmwareversion="222", + apicode="", + door1=GogoGate2Door( + door_id=1, + permission=True, + name="Door1", + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.OPENED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + voltage=40, + ), + door2=GogoGate2Door( + door_id=2, + permission=True, + name=None, + gate=True, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + voltage=40, + ), + door3=GogoGate2Door( + door_id=3, + permission=True, + name=None, + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + voltage=40, + ), + outputs=Outputs(output1=True, output2=False, output3=True), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) + + +def _mocked_ismartgate_closed_door_response(): + return ISmartGateInfoResponse( + user="user1", + ismartgatename="ismartgatename0", + model="ismartgatePRO", + apiversion="", + remoteaccessenabled=False, + remoteaccess="abc321.blah.blah", + firmwareversion="555", + pin=123, + lang="en", + newfirmware=False, + door1=ISmartGateDoor( + door_id=1, + permission=True, + name="Door1", + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, + voltage=40, + ), + door2=ISmartGateDoor( + door_id=2, + permission=True, + name="Door2", + gate=True, + mode=DoorMode.GARAGE, + status=DoorStatus.CLOSED, + sensor=True, + sensorid=None, + camera=False, + events=2, + temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, + voltage=40, + ), + door3=ISmartGateDoor( + door_id=3, + permission=True, + name=None, + gate=False, + mode=DoorMode.GARAGE, + status=DoorStatus.UNDEFINED, + sensor=True, + sensorid=None, + camera=False, + events=0, + temperature=None, + enabled=True, + apicode="apicode0", + customimage=False, + voltage=40, + ), + network=Network(ip=""), + wifi=Wifi(SSID="", linkquality="", signal=""), + ) diff --git a/tests/components/gogogate2/test_config_flow.py b/tests/components/gogogate2/test_config_flow.py index 621ea4d5232..0722874e9e5 100644 --- a/tests/components/gogogate2/test_config_flow.py +++ b/tests/components/gogogate2/test_config_flow.py @@ -1,9 +1,9 @@ """Tests for the GogoGate2 component.""" from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api -from gogogate2_api.common import ApiError -from gogogate2_api.const import GogoGate2ApiErrorCode +from ismartgate import GogoGate2Api, ISmartGateApi +from ismartgate.common import ApiError +from ismartgate.const import GogoGate2ApiErrorCode from homeassistant import config_entries, setup from homeassistant.components.gogogate2.const import ( @@ -19,7 +19,13 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) + +from . import _mocked_ismartgate_closed_door_response from tests.common import MockConfigEntry @@ -75,6 +81,24 @@ async def test_auth_fail( assert result["type"] == RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + api.reset_mock() + api.async_info.side_effect = ApiError(0, "blah") + result = await hass.config_entries.flow.async_init( + "gogogate2", context={"source": SOURCE_USER} + ) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, + CONF_IP_ADDRESS: "127.0.0.2", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {"base": "cannot_connect"} + async def test_form_homekit_unique_id_already_setup(hass): """Test that we abort from homekit if gogogate2 is already setup.""" @@ -145,3 +169,86 @@ async def test_form_homekit_ip_address(hass): CONF_PASSWORD: "password", CONF_USERNAME: "username", } + + +@patch("homeassistant.components.gogogate2.async_setup_entry", return_value=True) +@patch("homeassistant.components.gogogate2.common.ISmartGateApi") +async def test_discovered_dhcp( + ismartgateapi_mock, async_setup_entry_mock, hass +) -> None: + """Test we get the form with homekit and abort for dhcp source when we get both.""" + api: ISmartGateApi = MagicMock(spec=ISmartGateApi) + ismartgateapi_mock.return_value = api + + api.reset_mock() + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "1.2.3.4", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result2 + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + api.reset_mock() + + closed_door_response = _mocked_ismartgate_closed_door_response() + api.async_info.return_value = closed_door_response + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + user_input={ + CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, + CONF_IP_ADDRESS: "1.2.3.4", + CONF_USERNAME: "user0", + CONF_PASSWORD: "password0", + }, + ) + assert result3 + assert result3["type"] == RESULT_TYPE_CREATE_ENTRY + assert result3["data"] == { + "device": "ismartgate", + "ip_address": "1.2.3.4", + "password": "password0", + "username": "user0", + } + + +async def test_discovered_by_homekit_and_dhcp(hass): + """Test we get the form with homekit and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": MOCK_MAC_ADDR}}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": MOCK_MAC_ADDR}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"}, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" diff --git a/tests/components/gogogate2/test_cover.py b/tests/components/gogogate2/test_cover.py index 41b4368c640..d3507283426 100644 --- a/tests/components/gogogate2/test_cover.py +++ b/tests/components/gogogate2/test_cover.py @@ -2,18 +2,16 @@ from datetime import timedelta from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api, ISmartGateApi -from gogogate2_api.common import ( - ApiError, +from ismartgate import GogoGate2Api, ISmartGateApi +from ismartgate.common import ( DoorMode, DoorStatus, GogoGate2ActivateResponse, GogoGate2Door, GogoGate2InfoResponse, - ISmartGateDoor, - ISmartGateInfoResponse, Network, Outputs, + TransitionDoorStatus, Wifi, ) @@ -30,236 +28,31 @@ from homeassistant.components.gogogate2.const import ( DOMAIN, MANUFACTURER, ) -from homeassistant.components.homeassistant import DOMAIN as HA_DOMAIN -from homeassistant.config import async_process_ha_core_config from homeassistant.config_entries import SOURCE_USER from homeassistant.const import ( ATTR_DEVICE_CLASS, CONF_DEVICE, CONF_IP_ADDRESS, - CONF_NAME, CONF_PASSWORD, - CONF_PLATFORM, - CONF_UNIT_SYSTEM, - CONF_UNIT_SYSTEM_METRIC, CONF_USERNAME, STATE_CLOSED, + STATE_CLOSING, STATE_OPEN, + STATE_OPENING, STATE_UNAVAILABLE, STATE_UNKNOWN, ) from homeassistant.core import HomeAssistant -from homeassistant.setup import async_setup_component from homeassistant.util.dt import utcnow +from . import ( + _mocked_gogogate_open_door_response, + _mocked_ismartgate_closed_door_response, +) + from tests.common import MockConfigEntry, async_fire_time_changed, mock_device_registry -def _mocked_gogogate_open_door_response(): - return GogoGate2InfoResponse( - user="user1", - gogogatename="gogogatename0", - model="gogogate2", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc123.blah.blah", - firmwareversion="222", - apicode="", - door1=GogoGate2Door( - door_id=1, - permission=True, - name="Door1", - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.OPENED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - voltage=40, - ), - door2=GogoGate2Door( - door_id=2, - permission=True, - name=None, - gate=True, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - voltage=40, - ), - door3=GogoGate2Door( - door_id=3, - permission=True, - name=None, - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - voltage=40, - ), - outputs=Outputs(output1=True, output2=False, output3=True), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - - -def _mocked_ismartgate_closed_door_response(): - return ISmartGateInfoResponse( - user="user1", - ismartgatename="ismartgatename0", - model="ismartgatePRO", - apiversion="", - remoteaccessenabled=False, - remoteaccess="abc321.blah.blah", - firmwareversion="555", - pin=123, - lang="en", - newfirmware=False, - door1=ISmartGateDoor( - door_id=1, - permission=True, - name="Door1", - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - door2=ISmartGateDoor( - door_id=2, - permission=True, - name="Door2", - gate=True, - mode=DoorMode.GARAGE, - status=DoorStatus.CLOSED, - sensor=True, - sensorid=None, - camera=False, - events=2, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - door3=ISmartGateDoor( - door_id=3, - permission=True, - name=None, - gate=False, - mode=DoorMode.GARAGE, - status=DoorStatus.UNDEFINED, - sensor=True, - sensorid=None, - camera=False, - events=0, - temperature=None, - enabled=True, - apicode="apicode0", - customimage=False, - voltage=40, - ), - network=Network(ip=""), - wifi=Wifi(SSID="", linkquality="", signal=""), - ) - - -@patch("homeassistant.components.gogogate2.common.GogoGate2Api") -async def test_import_fail(gogogate2api_mock, hass: HomeAssistant) -> None: - """Test the failure to import.""" - api = MagicMock(spec=GogoGate2Api) - api.async_info.side_effect = ApiError(22, "Error") - gogogate2api_mock.return_value = api - - hass_config = { - HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, - COVER_DOMAIN: [ - { - CONF_PLATFORM: "gogogate2", - CONF_NAME: "cover0", - CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, - CONF_IP_ADDRESS: "127.0.1.0", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - } - ], - } - - await async_process_ha_core_config(hass, hass_config[HA_DOMAIN]) - assert await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, COVER_DOMAIN, hass_config) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) - assert not entity_ids - - -@patch("homeassistant.components.gogogate2.common.GogoGate2Api") -@patch("homeassistant.components.gogogate2.common.ISmartGateApi") -async def test_import( - ismartgateapi_mock, gogogate2api_mock, hass: HomeAssistant -) -> None: - """Test importing of file based config.""" - api0 = MagicMock(spec=GogoGate2Api) - api0.async_info.return_value = _mocked_gogogate_open_door_response() - gogogate2api_mock.return_value = api0 - - api1 = MagicMock(spec=ISmartGateApi) - api1.async_info.return_value = _mocked_ismartgate_closed_door_response() - ismartgateapi_mock.return_value = api1 - - hass_config = { - HA_DOMAIN: {CONF_UNIT_SYSTEM: CONF_UNIT_SYSTEM_METRIC}, - COVER_DOMAIN: [ - { - CONF_PLATFORM: "gogogate2", - CONF_NAME: "cover0", - CONF_DEVICE: DEVICE_TYPE_GOGOGATE2, - CONF_IP_ADDRESS: "127.0.1.0", - CONF_USERNAME: "user0", - CONF_PASSWORD: "password0", - }, - { - CONF_PLATFORM: "gogogate2", - CONF_NAME: "cover1", - CONF_DEVICE: DEVICE_TYPE_ISMARTGATE, - CONF_IP_ADDRESS: "127.0.1.1", - CONF_USERNAME: "user1", - CONF_PASSWORD: "password1", - }, - ], - } - - await async_process_ha_core_config(hass, hass_config[HA_DOMAIN]) - assert await async_setup_component(hass, HA_DOMAIN, {}) - assert await async_setup_component(hass, COVER_DOMAIN, hass_config) - await hass.async_block_till_done() - - entity_ids = hass.states.async_entity_ids(COVER_DOMAIN) - assert entity_ids is not None - assert len(entity_ids) == 3 - assert "cover.door1" in entity_ids - assert "cover.door1_2" in entity_ids - assert "cover.door2" in entity_ids - - @patch("homeassistant.components.gogogate2.common.GogoGate2Api") async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None: """Test open and close and data update.""" @@ -331,6 +124,10 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None api = MagicMock(GogoGate2Api) api.async_activate.return_value = GogoGate2ActivateResponse(result=True) api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } gogogate2api_mock.return_value = api config_entry = MockConfigEntry( @@ -351,32 +148,102 @@ async def test_open_close_update(gogogate2api_mock, hass: HomeAssistant) -> None assert dict(hass.states.get("cover.door1").attributes) == expected_attributes api.async_info.return_value = info_response(DoorStatus.CLOSED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.CLOSED, + 2: DoorStatus.CLOSED, + } await hass.services.async_call( COVER_DOMAIN, "close_cover", service_data={"entity_id": "cover.door1"}, ) + api.async_get_door_statuses_from_info.return_value = { + 1: TransitionDoorStatus.CLOSING, + 2: TransitionDoorStatus.CLOSING, + } + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSING + api.async_close_door.assert_called_with(1) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_CLOSING + + api.async_info.return_value = info_response(DoorStatus.CLOSED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.CLOSED, + 2: DoorStatus.CLOSED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED - api.async_close_door.assert_called_with(1) api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } await hass.services.async_call( COVER_DOMAIN, "open_cover", service_data={"entity_id": "cover.door1"}, ) + api.async_get_door_statuses_from_info.return_value = { + 1: TransitionDoorStatus.OPENING, + 2: TransitionDoorStatus.OPENING, + } + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + api.async_open_door.assert_called_with(1) + + async_fire_time_changed(hass, utcnow() + timedelta(seconds=10)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + + api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_OPEN - api.async_open_door.assert_called_with(1) api.async_info.return_value = info_response(DoorStatus.UNDEFINED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.UNDEFINED, + 2: DoorStatus.UNDEFINED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_UNKNOWN + api.async_info.return_value = info_response(DoorStatus.OPENED) + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.OPENED, + 2: DoorStatus.OPENED, + } + await hass.services.async_call( + COVER_DOMAIN, + "close_cover", + service_data={"entity_id": "cover.door1"}, + ) + await hass.services.async_call( + COVER_DOMAIN, + "open_cover", + service_data={"entity_id": "cover.door1"}, + ) + api.async_get_door_statuses_from_info.return_value = { + 1: TransitionDoorStatus.OPENING, + 2: TransitionDoorStatus.OPENING, + } + async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) + await hass.async_block_till_done() + assert hass.states.get("cover.door1").state == STATE_OPENING + api.async_open_door.assert_called_with(1) + assert await hass.config_entries.async_unload(config_entry.entry_id) assert not hass.states.async_entity_ids(DOMAIN) @@ -430,6 +297,10 @@ async def test_availability(ismartgateapi_mock, hass: HomeAssistant) -> None: api.async_info.side_effect = None api.async_info.return_value = closed_door_response + api.async_get_door_statuses_from_info.return_value = { + 1: DoorStatus.CLOSED, + 2: DoorStatus.CLOSED, + } async_fire_time_changed(hass, utcnow() + timedelta(hours=2)) await hass.async_block_till_done() assert hass.states.get("cover.door1").state == STATE_CLOSED diff --git a/tests/components/gogogate2/test_init.py b/tests/components/gogogate2/test_init.py index 7bcd2f8d2f2..1cfbf52284f 100644 --- a/tests/components/gogogate2/test_init.py +++ b/tests/components/gogogate2/test_init.py @@ -2,7 +2,7 @@ import asyncio from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api +from ismartgate import GogoGate2Api import pytest from homeassistant.components.gogogate2 import DEVICE_TYPE_GOGOGATE2, async_setup_entry diff --git a/tests/components/gogogate2/test_sensor.py b/tests/components/gogogate2/test_sensor.py index 020989c003a..5adc4532750 100644 --- a/tests/components/gogogate2/test_sensor.py +++ b/tests/components/gogogate2/test_sensor.py @@ -2,8 +2,8 @@ from datetime import timedelta from unittest.mock import MagicMock, patch -from gogogate2_api import GogoGate2Api, ISmartGateApi -from gogogate2_api.common import ( +from ismartgate import GogoGate2Api, ISmartGateApi +from ismartgate.common import ( DoorMode, DoorStatus, GogoGate2ActivateResponse, diff --git a/tests/components/google_assistant/test_trait.py b/tests/components/google_assistant/test_trait.py index 3d506be644d..c3678e7f99a 100644 --- a/tests/components/google_assistant/test_trait.py +++ b/tests/components/google_assistant/test_trait.py @@ -26,6 +26,10 @@ from homeassistant.components.climate import const as climate from homeassistant.components.google_assistant import const, error, helpers, trait from homeassistant.components.google_assistant.error import SmartHomeError from homeassistant.components.humidifier import const as humidifier +from homeassistant.components.media_player.const import ( + MEDIA_TYPE_CHANNEL, + SERVICE_PLAY_MEDIA, +) from homeassistant.config import async_process_ha_core_config from homeassistant.const import ( ATTR_ASSUMED_STATE, @@ -2653,3 +2657,52 @@ async def test_media_state(hass, state): "activityState": trt.activity_lookup.get(state), "playbackState": trt.playback_lookup.get(state), } + + +async def test_channel(hass): + """Test Channel trait support.""" + assert helpers.get_google_type(media_player.DOMAIN, None) is not None + assert trait.ChannelTrait.supported( + media_player.DOMAIN, + media_player.SUPPORT_PLAY_MEDIA, + media_player.DEVICE_CLASS_TV, + None, + ) + assert ( + trait.ChannelTrait.supported( + media_player.DOMAIN, media_player.SUPPORT_PLAY_MEDIA, None, None + ) + is False + ) + assert trait.ChannelTrait.supported(media_player.DOMAIN, 0, None, None) is False + + trt = trait.ChannelTrait(hass, State("media_player.demo", STATE_ON), BASIC_CONFIG) + + assert trt.sync_attributes() == { + "availableChannels": [], + "commandOnlyChannels": True, + } + assert trt.query_attributes() == {} + + media_player_calls = async_mock_service( + hass, media_player.DOMAIN, SERVICE_PLAY_MEDIA + ) + await trt.execute( + trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelNumber": "1"}, {} + ) + assert len(media_player_calls) == 1 + assert media_player_calls[0].data == { + ATTR_ENTITY_ID: "media_player.demo", + media_player.ATTR_MEDIA_CONTENT_ID: "1", + media_player.ATTR_MEDIA_CONTENT_TYPE: MEDIA_TYPE_CHANNEL, + } + + with pytest.raises(SmartHomeError, match="Channel is not available"): + await trt.execute( + trait.COMMAND_SELECT_CHANNEL, BASIC_DATA, {"channelCode": "Channel 3"}, {} + ) + assert len(media_player_calls) == 1 + + with pytest.raises(SmartHomeError, match="Unsupported command"): + await trt.execute("Unknown command", BASIC_DATA, {"channelNumber": "1"}, {}) + assert len(media_player_calls) == 1 diff --git a/tests/components/google_travel_time/conftest.py b/tests/components/google_travel_time/conftest.py index 18e16a79e27..87d922ac22c 100644 --- a/tests/components/google_travel_time/conftest.py +++ b/tests/components/google_travel_time/conftest.py @@ -37,6 +37,16 @@ def bypass_setup_fixture(): yield +@pytest.fixture(name="bypass_platform_setup") +def bypass_platform_setup_fixture(): + """Bypass platform setup.""" + with patch( + "homeassistant.components.google_travel_time.sensor.async_setup_entry", + return_value=True, + ): + yield + + @pytest.fixture(name="bypass_update") def bypass_update_fixture(): """Bypass sensor update.""" diff --git a/tests/components/google_travel_time/test_config_flow.py b/tests/components/google_travel_time/test_config_flow.py index a0767246087..6118aac7cfa 100644 --- a/tests/components/google_travel_time/test_config_flow.py +++ b/tests/components/google_travel_time/test_config_flow.py @@ -14,6 +14,7 @@ from homeassistant.components.google_travel_time.const import ( CONF_TRAFFIC_MODEL, CONF_TRANSIT_MODE, CONF_TRANSIT_ROUTING_PREFERENCE, + CONF_TRAVEL_MODE, CONF_UNITS, DEFAULT_NAME, DEPARTURE_TIME, @@ -24,6 +25,7 @@ from homeassistant.const import ( CONF_MODE, CONF_NAME, CONF_UNIT_SYSTEM_IMPERIAL, + CONF_UNIT_SYSTEM_METRIC, ) from tests.common import MockConfigEntry @@ -197,8 +199,8 @@ async def test_options_flow_departure_time(hass, validate_config_entry, bypass_u } -async def test_dupe_id(hass, validate_config_entry, bypass_setup): - """Test setting up the same entry twice fails.""" +async def test_dupe(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -233,8 +235,7 @@ async def test_dupe_id(hass, validate_config_entry, bypass_setup): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_import_flow(hass, validate_config_entry, bypass_update): @@ -297,3 +298,317 @@ async def test_import_flow(hass, validate_config_entry, bypass_update): CONF_TRANSIT_MODE: "train", CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", } + + +async def test_dupe_import_no_options(hass, bypass_update): + """Test duplicate import with no options.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dupe_import_default_options(hass, bypass_update): + """Test duplicate import with default options.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def _setup_dupe_import(hass, bypass_update): + """Set up dupe import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + +async def test_dupe_import(hass, bypass_update): + """Test duplicate import.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dupe_import_false_check_data_keys(hass, bypass_update): + """Test false duplicate import check when data keys differ.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key2", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_no_units(hass, bypass_update): + """Test false duplicate import check when units aren't provided.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_units(hass, bypass_update): + """Test false duplicate import check when units are provided but different.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_METRIC, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_travel_mode(hass, bypass_update): + """Test false duplicate import check when travel mode differs.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_TRAVEL_MODE: "driving", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_mode(hass, bypass_update): + """Test false duplicate import check when mode diiffers.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "driving", + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_no_mode(hass, bypass_update): + """Test false duplicate import check when no mode is provided.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_LANGUAGE: "en", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_options(hass, bypass_update): + """Test false duplicate import check when options differ.""" + await _setup_dupe_import(hass, bypass_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_API_KEY: "api_key", + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_NAME: "test_name", + CONF_OPTIONS: { + CONF_MODE: "walking", + CONF_AVOID: "tolls", + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_ARRIVAL_TIME: "test", + CONF_TRAFFIC_MODEL: "best_guess", + CONF_TRANSIT_MODE: "train", + CONF_TRANSIT_ROUTING_PREFERENCE: "less_walking", + }, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY diff --git a/tests/components/google_travel_time/test_init.py b/tests/components/google_travel_time/test_init.py new file mode 100644 index 00000000000..583cd4dc7ce --- /dev/null +++ b/tests/components/google_travel_time/test_init.py @@ -0,0 +1,21 @@ +"""Test Google Maps Travel Time initialization.""" +from homeassistant.components.google_travel_time.const import DOMAIN +from homeassistant.helpers.entity_registry import async_get + +from tests.common import MockConfigEntry + + +async def test_migration(hass, bypass_platform_setup): + """Test migration logic for unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, version=1, entry_id="test", unique_id="test" + ) + ent_reg = async_get(hass) + ent_entry = ent_reg.async_get_or_create( + "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=config_entry + ) + entity_id = ent_entry.entity_id + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.unique_id is None + assert ent_reg.async_get(entity_id).unique_id == config_entry.entry_id diff --git a/tests/components/gree/test_init.py b/tests/components/gree/test_init.py index 7443ae1e94c..82b082b03cb 100644 --- a/tests/components/gree/test_init.py +++ b/tests/components/gree/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.gree.const import DOMAIN as GREE_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry @@ -25,7 +25,7 @@ async def test_setup_simple(hass): assert len(climate_setup.mock_calls) == 1 assert len(switch_setup.mock_calls) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED # No flows started assert len(hass.config_entries.flow.async_progress()) == 0 @@ -43,4 +43,4 @@ async def test_unload_config_entry(hass): await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index c9b861a46a9..06ad1b1101b 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -26,6 +26,7 @@ from homeassistant.components.light import ( COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, + COLOR_MODE_ONOFF, COLOR_MODE_RGBW, COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, @@ -114,7 +115,7 @@ async def test_state_reporting(hass): assert hass.states.get("light.light_group").state == STATE_UNAVAILABLE -async def test_brightness(hass): +async def test_brightness(hass, enable_custom_integrations): """Test brightness reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -183,7 +184,7 @@ async def test_brightness(hass): assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] -async def test_color_hs(hass): +async def test_color_hs(hass, enable_custom_integrations): """Test hs color reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -251,7 +252,7 @@ async def test_color_hs(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgbw(hass): +async def test_color_rgbw(hass, enable_custom_integrations): """Test rgbw color reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -322,7 +323,7 @@ async def test_color_rgbw(hass): assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 -async def test_color_rgbww(hass): +async def test_color_rgbww(hass, enable_custom_integrations): """Test rgbww color reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -434,7 +435,7 @@ async def test_white_value(hass): assert state.attributes[ATTR_WHITE_VALUE] == 100 -async def test_color_temp(hass): +async def test_color_temp(hass, enable_custom_integrations): """Test color temp reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -501,7 +502,7 @@ async def test_color_temp(hass): assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] -async def test_emulated_color_temp_group(hass): +async def test_emulated_color_temp_group(hass, enable_custom_integrations): """Test emulated color temperature in a group.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -564,7 +565,7 @@ async def test_emulated_color_temp_group(hass): assert state.attributes[ATTR_HS_COLOR] == (27.001, 19.243) -async def test_min_max_mireds(hass): +async def test_min_max_mireds(hass, enable_custom_integrations): """Test min/max mireds reporting. min/max mireds is reported both when light is on and off @@ -656,6 +657,10 @@ async def test_effect_list(hass): await hass.async_block_till_done() state = hass.states.get("light.light_group") assert set(state.attributes[ATTR_EFFECT_LIST]) == {"None", "Random", "Colorloop"} + # These ensure the output is sorted as expected + assert state.attributes[ATTR_EFFECT_LIST][0] == "None" + assert state.attributes[ATTR_EFFECT_LIST][1] == "Colorloop" + assert state.attributes[ATTR_EFFECT_LIST][2] == "Random" hass.states.async_set( "light.test2", @@ -735,7 +740,7 @@ async def test_effect(hass): assert state.attributes[ATTR_EFFECT] == "Random" -async def test_supported_color_modes(hass): +async def test_supported_color_modes(hass, enable_custom_integrations): """Test supported_color_modes reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -780,7 +785,7 @@ async def test_supported_color_modes(hass): } -async def test_color_mode(hass): +async def test_color_mode(hass, enable_custom_integrations): """Test color_mode reporting.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -852,6 +857,80 @@ async def test_color_mode(hass): assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_HS +async def test_color_mode2(hass, enable_custom_integrations): + """Test onoff color_mode and brightness are given lowest priority.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test4", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test5", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test6", STATE_ON)) + + entity = platform.ENTITIES[0] + entity.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity.color_mode = COLOR_MODE_COLOR_TEMP + + entity = platform.ENTITIES[1] + entity.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity.color_mode = COLOR_MODE_BRIGHTNESS + + entity = platform.ENTITIES[2] + entity.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity.color_mode = COLOR_MODE_BRIGHTNESS + + entity = platform.ENTITIES[3] + entity.supported_color_modes = {COLOR_MODE_ONOFF} + entity.color_mode = COLOR_MODE_ONOFF + + entity = platform.ENTITIES[4] + entity.supported_color_modes = {COLOR_MODE_ONOFF} + entity.color_mode = COLOR_MODE_ONOFF + + entity = platform.ENTITIES[5] + entity.supported_color_modes = {COLOR_MODE_ONOFF} + entity.color_mode = COLOR_MODE_ONOFF + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": [ + "light.test1", + "light.test2", + "light.test3", + "light.test4", + "light.test5", + "light.test6", + ], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": ["light.test1"]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_BRIGHTNESS + + async def test_supported_features(hass): """Test supported features reporting.""" await async_setup_component( diff --git a/tests/components/growatt_server/__init__.py b/tests/components/growatt_server/__init__.py new file mode 100644 index 00000000000..999e1782a9f --- /dev/null +++ b/tests/components/growatt_server/__init__.py @@ -0,0 +1 @@ +"""Tests for the growatt_server component.""" diff --git a/tests/components/growatt_server/test_config_flow.py b/tests/components/growatt_server/test_config_flow.py new file mode 100644 index 00000000000..cc11c2f8bf2 --- /dev/null +++ b/tests/components/growatt_server/test_config_flow.py @@ -0,0 +1,188 @@ +"""Tests for the Growatt server config flow.""" +from copy import deepcopy +from unittest.mock import patch + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.growatt_server.const import CONF_PLANT_ID, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +FIXTURE_USER_INPUT = {CONF_USERNAME: "username", CONF_PASSWORD: "password"} + +GROWATT_PLANT_LIST_RESPONSE = { + "data": [ + { + "plantMoneyText": "474.9 (€)", + "plantName": "Plant name", + "plantId": "123456", + "isHaveStorage": "false", + "todayEnergy": "2.6 kWh", + "totalEnergy": "2.37 MWh", + "currentPower": "628.8 W", + } + ], + "totalData": { + "currentPowerSum": "628.8 W", + "CO2Sum": "2.37 KT", + "isHaveStorage": "false", + "eTotalMoneyText": "474.9 (€)", + "todayEnergySum": "2.6 kWh", + "totalEnergySum": "2.37 MWh", + }, + "success": True, +} +GROWATT_LOGIN_RESPONSE = {"userId": 123456, "userLevel": 1, "success": True} + + +async def test_show_authenticate_form(hass): + """Test that the setup form is served.""" + 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" + + +async def test_incorrect_username(hass): + """Test that it shows the appropriate error when an incorrect username is entered.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "growattServer.GrowattApi.login", + return_value={"errCode": "102", "success": False}, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {"base": "invalid_auth"} + + +async def test_no_plants_on_account(hass): + """Test registering an integration and finishing flow with an entered plant_id.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE) + plant_list["data"] = [] + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch("growattServer.GrowattApi.plant_list", return_value=plant_list): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] == "abort" + assert result["reason"] == "no_plants" + + +async def test_multiple_plant_ids(hass): + """Test registering an integration and finishing flow with an entered plant_id.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + plant_list = deepcopy(GROWATT_PLANT_LIST_RESPONSE) + plant_list["data"].append(plant_list["data"][0]) + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch("growattServer.GrowattApi.plant_list", return_value=plant_list), patch( + "homeassistant.components.growatt_server.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "plant" + + user_input = {CONF_PLANT_ID: "123456"} + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_PLANT_ID] == "123456" + + +async def test_one_plant_on_account(hass): + """Test registering an integration and finishing flow with an entered plant_id.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ), patch( + "homeassistant.components.growatt_server.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_PLANT_ID] == "123456" + + +async def test_import_one_plant(hass): + """Test import step with a single plant.""" + import_data = FIXTURE_USER_INPUT.copy() + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ), patch( + "homeassistant.components.growatt_server.async_setup_entry", return_value=True + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=import_data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["data"][CONF_USERNAME] == FIXTURE_USER_INPUT[CONF_USERNAME] + assert result["data"][CONF_PASSWORD] == FIXTURE_USER_INPUT[CONF_PASSWORD] + assert result["data"][CONF_PLANT_ID] == "123456" + + +async def test_existing_plant_configured(hass): + """Test entering an existing plant_id.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id="123456") + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + user_input = FIXTURE_USER_INPUT.copy() + + with patch( + "growattServer.GrowattApi.login", return_value=GROWATT_LOGIN_RESPONSE + ), patch( + "growattServer.GrowattApi.plant_list", + return_value=GROWATT_PLANT_LIST_RESPONSE, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input + ) + + assert result["type"] == "abort" + assert result["reason"] == "already_configured" diff --git a/tests/components/guardian/test_config_flow.py b/tests/components/guardian/test_config_flow.py index e9b53b4e629..4fbff7d7e48 100644 --- a/tests/components/guardian/test_config_flow.py +++ b/tests/components/guardian/test_config_flow.py @@ -4,12 +4,13 @@ from unittest.mock import patch from aioguardian.errors import GuardianError from homeassistant import data_entry_flow +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.guardian import CONF_UID, DOMAIN from homeassistant.components.guardian.config_flow import ( async_get_pin_from_discovery_hostname, async_get_pin_from_uid, ) -from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_IP_ADDRESS, CONF_PORT from tests.common import MockConfigEntry @@ -95,7 +96,7 @@ async def test_step_zeroconf(hass, ping_client): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "zeroconf_confirm" + assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -124,7 +125,7 @@ async def test_step_zeroconf_already_in_progress(hass): DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "zeroconf_confirm" + assert result["step_id"] == "discovery_confirm" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_ZEROCONF}, data=zeroconf_data @@ -133,10 +134,48 @@ async def test_step_zeroconf_already_in_progress(hass): assert result["reason"] == "already_in_progress" -async def test_step_zeroconf_no_discovery_info(hass): - """Test the zeroconf step aborting because no discovery info came along.""" +async def test_step_dhcp(hass, ping_client): + """Test the dhcp step.""" + dhcp_data = { + IP_ADDRESS: "192.168.1.100", + HOSTNAME: "GVC1-ABCD.local.", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + } + result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_ZEROCONF} + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "cannot_connect" + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovery_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"] == "ABCDEF123456" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PORT: 7777, + CONF_UID: "ABCDEF123456", + } + + +async def test_step_dhcp_already_in_progress(hass): + """Test the zeroconf step aborting because it's already in progress.""" + dhcp_data = { + IP_ADDRESS: "192.168.1.100", + HOSTNAME: "GVC1-ABCD.local.", + MAC_ADDRESS: "aa:bb:cc:dd:ee:ff", + } + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "discovery_confirm" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_DHCP}, data=dhcp_data + ) + assert result["type"] == "abort" + assert result["reason"] == "already_in_progress" diff --git a/tests/components/harmony/test_remote.py b/tests/components/harmony/test_remote.py index 4222244f00d..df75485e30d 100644 --- a/tests/components/harmony/test_remote.py +++ b/tests/components/harmony/test_remote.py @@ -6,7 +6,6 @@ from aioharmony.const import SendCommandDevice from homeassistant.components.harmony.const import ( DOMAIN, - HARMONY_DATA, SERVICE_CHANGE_CHANNEL, SERVICE_SYNC, ) @@ -150,7 +149,7 @@ async def test_remote_toggles(mock_hc, hass, mock_write_config): assert hass.states.is_state(ENTITY_PLAY_MUSIC, STATE_OFF) -async def test_async_send_command(mock_hc, hass, mock_write_config): +async def test_async_send_command(mock_hc, harmony_client, hass, mock_write_config): """Ensure calls to send remote commands properly propagate to devices.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} @@ -160,8 +159,7 @@ async def test_async_send_command(mock_hc, hass, mock_write_config): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - send_commands_mock = data._client.send_commands + send_commands_mock = harmony_client.send_commands # No device provided await _send_commands_and_wait( @@ -283,7 +281,9 @@ async def test_async_send_command(mock_hc, hass, mock_write_config): send_commands_mock.reset_mock() -async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config): +async def test_async_send_command_custom_delay( + mock_hc, harmony_client, hass, mock_write_config +): """Ensure calls to send remote commands properly propagate to devices with custom delays.""" entry = MockConfigEntry( domain=DOMAIN, @@ -298,8 +298,7 @@ async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - send_commands_mock = data._client.send_commands + send_commands_mock = harmony_client.send_commands # Tell the TV to play by id await _send_commands_and_wait( @@ -324,7 +323,7 @@ async def test_async_send_command_custom_delay(mock_hc, hass, mock_write_config) send_commands_mock.reset_mock() -async def test_change_channel(mock_hc, hass, mock_write_config): +async def test_change_channel(mock_hc, harmony_client, hass, mock_write_config): """Test change channel commands.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} @@ -334,8 +333,7 @@ async def test_change_channel(mock_hc, hass, mock_write_config): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - change_channel_mock = data._client.change_channel + change_channel_mock = harmony_client.change_channel # Tell the remote to change channels await hass.services.async_call( @@ -349,7 +347,7 @@ async def test_change_channel(mock_hc, hass, mock_write_config): change_channel_mock.assert_awaited_once_with(100) -async def test_sync(mock_hc, mock_write_config, hass): +async def test_sync(mock_hc, harmony_client, mock_write_config, hass): """Test the sync command.""" entry = MockConfigEntry( domain=DOMAIN, data={CONF_HOST: "192.0.2.0", CONF_NAME: HUB_NAME} @@ -359,8 +357,7 @@ async def test_sync(mock_hc, mock_write_config, hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - data = hass.data[DOMAIN][entry.entry_id][HARMONY_DATA] - sync_mock = data._client.sync + sync_mock = harmony_client.sync # Tell the remote to change channels await hass.services.async_call( diff --git a/tests/components/hassio/test_init.py b/tests/components/hassio/test_init.py index 5bf8a45ab52..7e9d7cd91c8 100644 --- a/tests/components/hassio/test_init.py +++ b/tests/components/hassio/test_init.py @@ -32,6 +32,13 @@ def mock_all(aioclient_mock, request): "data": {"supervisor": "222", "homeassistant": "0.110.0", "hassos": None}, }, ) + aioclient_mock.get( + "http://127.0.0.1/store", + json={ + "result": "ok", + "data": {"addons": [], "repositories": []}, + }, + ) aioclient_mock.get( "http://127.0.0.1/host/info", json={ @@ -67,6 +74,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com/home-assistant/addons/test", }, { @@ -76,6 +84,7 @@ def mock_all(aioclient_mock, request): "update_available": False, "version": "1.0.0", "version_latest": "1.0.0", + "repository": "core", "url": "https://github.com", }, ], @@ -92,7 +101,7 @@ async def test_setup_api_ping(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert hass.components.hassio.get_core_info()["version_latest"] == "1.0.0" assert hass.components.hassio.is_hassio() @@ -131,7 +140,7 @@ async def test_setup_api_push_api_data(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert aioclient_mock.mock_calls[1][2]["watchdog"] @@ -147,7 +156,7 @@ async def test_setup_api_push_api_data_server_host(hass, aioclient_mock): ) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 9999 assert not aioclient_mock.mock_calls[1][2]["watchdog"] @@ -159,7 +168,7 @@ async def test_setup_api_push_api_data_default(hass, aioclient_mock, hass_storag result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 refresh_token = aioclient_mock.mock_calls[1][2]["refresh_token"] @@ -206,7 +215,7 @@ async def test_setup_api_existing_hassio_user(hass, aioclient_mock, hass_storage result = await async_setup_component(hass, "hassio", {"http": {}, "hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert not aioclient_mock.mock_calls[1][2]["ssl"] assert aioclient_mock.mock_calls[1][2]["port"] == 8123 assert aioclient_mock.mock_calls[1][2]["refresh_token"] == token.token @@ -220,7 +229,7 @@ async def test_setup_core_push_timezone(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[2][2]["timezone"] == "testzone" with patch("homeassistant.util.dt.set_default_time_zone"): @@ -237,7 +246,7 @@ async def test_setup_hassio_no_additional_data(hass, aioclient_mock): result = await async_setup_component(hass, "hassio", {"hassio": {}}) assert result - assert aioclient_mock.call_count == 9 + assert aioclient_mock.call_count == 10 assert aioclient_mock.mock_calls[-1][3]["X-Hassio-Key"] == "123456" diff --git a/tests/components/here_travel_time/test_sensor.py b/tests/components/here_travel_time/test_sensor.py index b2fcef715f1..2f69dc97a84 100644 --- a/tests/components/here_travel_time/test_sensor.py +++ b/tests/components/here_travel_time/test_sensor.py @@ -83,7 +83,6 @@ def _build_mock_url(origin, destination, modes, api_key, departure=None, arrival if departure is None and arrival is None: parameters["departure"] = "now" url = base_url + urllib.parse.urlencode(parameters) - print(url) return url diff --git a/tests/components/history/conftest.py b/tests/components/history/conftest.py index 35829ece721..5e81b444393 100644 --- a/tests/components/history/conftest.py +++ b/tests/components/history/conftest.py @@ -2,28 +2,8 @@ import pytest from homeassistant.components import history -from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.setup import setup_component -from tests.common import get_test_home_assistant, init_recorder_component - - -@pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - @pytest.fixture def hass_history(hass_recorder): diff --git a/tests/components/history/test_init.py b/tests/components/history/test_init.py index 497f296437f..bf8d34e6ffe 100644 --- a/tests/components/history/test_init.py +++ b/tests/components/history/test_init.py @@ -1,20 +1,21 @@ """The tests the History component.""" # pylint: disable=protected-access,invalid-name -from copy import copy from datetime import timedelta import json from unittest.mock import patch, sentinel import pytest +from pytest import approx from homeassistant.components import history, recorder +from homeassistant.components.recorder.history import get_significant_states from homeassistant.components.recorder.models import process_timestamp import homeassistant.core as ha from homeassistant.helpers.json import JSONEncoder from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util -from tests.common import init_recorder_component, mock_state_change_event +from tests.common import init_recorder_component from tests.components.recorder.common import trigger_db_commit, wait_recording_done @@ -25,151 +26,6 @@ def test_setup(): pass -def test_get_states(hass_history): - """Test getting states at a specific point in time.""" - hass = hass_history - states = [] - - now = dt_util.utcnow() - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): - for i in range(5): - state = ha.State( - f"test.point_in_time_{i % 5}", - f"State {i}", - {"attribute_test": i}, - ) - - mock_state_change_event(hass, state) - - states.append(state) - - wait_recording_done(hass) - - future = now + timedelta(seconds=1) - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future): - for i in range(5): - state = ha.State( - f"test.point_in_time_{i % 5}", - f"State {i}", - {"attribute_test": i}, - ) - - mock_state_change_event(hass, state) - - wait_recording_done(hass) - - # Get states returns everything before POINT - for state1, state2 in zip( - states, - sorted(history.get_states(hass, future), key=lambda state: state.entity_id), - ): - assert state1 == state2 - - # Test get_state here because we have a DB setup - assert states[0] == history.get_state(hass, future, states[0].entity_id) - - time_before_recorder_ran = now - timedelta(days=1000) - assert history.get_states(hass, time_before_recorder_ran) == [] - - assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None - - -def test_state_changes_during_period(hass_history): - """Test state change during period.""" - hass = hass_history - entity_id = "media_player.test" - - def set_state(state): - """Set the state.""" - hass.states.set(entity_id, state) - wait_recording_done(hass) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - point = start + timedelta(seconds=1) - end = point + timedelta(seconds=1) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): - set_state("idle") - set_state("YouTube") - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): - states = [ - set_state("idle"), - set_state("Netflix"), - set_state("Plex"), - set_state("YouTube"), - ] - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end): - set_state("Netflix") - set_state("Plex") - - hist = history.state_changes_during_period(hass, start, end, entity_id) - - assert states == hist[entity_id] - - -def test_get_last_state_changes(hass_history): - """Test number of state changes.""" - hass = hass_history - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.set(entity_id, state) - wait_recording_done(hass) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - point2 = point + timedelta(minutes=1) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): - set_state("1") - - states = [] - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): - states.append(set_state("2")) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point2): - states.append(set_state("3")) - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert states == hist[entity_id] - - -def test_ensure_state_can_be_copied(hass_history): - """Ensure a state can pass though copy(). - - The filter integration uses copy() on states - from history. - """ - hass = hass_history - entity_id = "sensor.test" - - def set_state(state): - """Set the state.""" - hass.states.set(entity_id, state) - wait_recording_done(hass) - return hass.states.get(entity_id) - - start = dt_util.utcnow() - timedelta(minutes=2) - point = start + timedelta(minutes=1) - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): - set_state("1") - - with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): - set_state("2") - - hist = history.get_last_state_changes(hass, 2, entity_id) - - assert copy(hist[entity_id][0]) == hist[entity_id][0] - assert copy(hist[entity_id][1]) == hist[entity_id][1] - - def test_get_significant_states(hass_history): """Test that only significant states are returned. @@ -179,7 +35,7 @@ def test_get_significant_states(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - hist = history.get_significant_states(hass, zero, four, filters=history.Filters()) + hist = get_significant_states(hass, zero, four, filters=history.Filters()) assert states == hist @@ -195,7 +51,7 @@ def test_get_significant_states_minimal_response(hass_history): """ hass = hass_history zero, four, states = record_states(hass) - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, filters=history.Filters(), minimal_response=True ) @@ -236,7 +92,7 @@ def test_get_significant_states_with_initial(hass_history): if state.last_changed == one: state.last_changed = one_and_half - hist = history.get_significant_states( + hist = get_significant_states( hass, one_and_half, four, @@ -263,7 +119,7 @@ def test_get_significant_states_without_initial(hass_history): ) del states["media_player.test2"] - hist = history.get_significant_states( + hist = get_significant_states( hass, one_and_half, four, @@ -283,7 +139,7 @@ def test_get_significant_states_entity_id(hass_history): del states["thermostat.test2"] del states["script.can_cancel_this_one"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, ["media_player.test"], filters=history.Filters() ) assert states == hist @@ -298,7 +154,7 @@ def test_get_significant_states_multiple_entity_ids(hass_history): del states["thermostat.test2"] del states["script.can_cancel_this_one"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, @@ -570,12 +426,12 @@ def test_get_significant_states_are_ordered(hass_history): hass = hass_history zero, four, _states = record_states(hass) entity_ids = ["media_player.test", "media_player.test2"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, entity_ids, filters=history.Filters() ) assert list(hist.keys()) == entity_ids entity_ids = ["media_player.test2", "media_player.test"] - hist = history.get_significant_states( + hist = get_significant_states( hass, zero, four, entity_ids, filters=history.Filters() ) assert list(hist.keys()) == entity_ids @@ -619,14 +475,14 @@ def test_get_significant_states_only(hass_history): # everything is different states.append(set_state("412", attributes={"attribute": 54.23})) - hist = history.get_significant_states(hass, start, significant_changes_only=True) + hist = get_significant_states(hass, start, significant_changes_only=True) assert len(hist[entity_id]) == 2 assert states[0] not in hist[entity_id] assert states[1] in hist[entity_id] assert states[2] in hist[entity_id] - hist = history.get_significant_states(hass, start, significant_changes_only=False) + hist = get_significant_states(hass, start, significant_changes_only=False) assert len(hist[entity_id]) == 3 assert states == hist[entity_id] @@ -644,7 +500,7 @@ def check_significant_states(hass, zero, four, states, config): filters.included_entities = include.get(history.CONF_ENTITIES, []) filters.included_domains = include.get(history.CONF_DOMAINS, []) - hist = history.get_significant_states(hass, zero, four, filters=filters) + hist = get_significant_states(hass, zero, four, filters=filters) assert states == hist @@ -971,3 +827,120 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(hass, hass_clien assert len(response_json) == 2 assert response_json[0][0]["entity_id"] == "light.kitchen" assert response_json[1][0]["entity_id"] == "light.cow" + + +async def test_statistics_during_period(hass, hass_ws_client): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await async_setup_component(hass, "sensor", {}) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + hass.states.async_set( + "sensor.test", + 10, + attributes={"device_class": "temperature", "state_class": "measurement"}, + ) + await hass.async_block_till_done() + + await hass.async_add_executor_job(trigger_db_commit, hass) + await hass.async_block_till_done() + + hass.data[recorder.DATA_INSTANCE].do_adhoc_statistics(period="hourly", start=now) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": now.isoformat(), + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == {"statistics": {}} + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "statistic_id": "sensor.test", + } + ) + response = await client.receive_json() + assert response["success"] + assert response["result"] == { + "statistics": { + "sensor.test": [ + { + "statistic_id": "sensor.test", + "start": now.isoformat(), + "mean": approx(10.0), + "min": approx(10.0), + "max": approx(10.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + } + + +async def test_statistics_during_period_bad_start_time(hass, hass_ws_client): + """Test statistics_during_period.""" + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": "cats", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_start_time" + + +async def test_statistics_during_period_bad_end_time(hass, hass_ws_client): + """Test statistics_during_period.""" + now = dt_util.utcnow() + + await hass.async_add_executor_job(init_recorder_component, hass) + await async_setup_component( + hass, + "history", + {"history": {}}, + ) + await hass.async_add_executor_job(hass.data[recorder.DATA_INSTANCE].block_till_done) + + client = await hass_ws_client() + await client.send_json( + { + "id": 1, + "type": "history/statistics_during_period", + "start_time": now.isoformat(), + "end_time": "dogs", + } + ) + response = await client.receive_json() + assert not response["success"] + assert response["error"]["code"] == "invalid_end_time" diff --git a/tests/components/history_stats/test_sensor.py b/tests/components/history_stats/test_sensor.py index 06ba1f22f47..01ce5bf06b3 100644 --- a/tests/components/history_stats/test_sensor.py +++ b/tests/components/history_stats/test_sensor.py @@ -6,7 +6,6 @@ import unittest from unittest.mock import patch import pytest -import pytz from homeassistant import config as hass_config from homeassistant.components.history_stats import DOMAIN @@ -36,7 +35,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test the history statistics sensor setup.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -58,7 +56,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test the history statistics sensor setup for multiple states.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -82,7 +79,7 @@ class TestHistoryStatsSensor(unittest.TestCase): ) def test_period_parsing(self, mock): """Test the conversion from templates to period.""" - now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=pytz.utc) + now = datetime(2019, 1, 1, 23, 30, 0, tzinfo=dt_util.UTC) with patch("homeassistant.util.dt.now", return_value=now): today = Template( "{{ now().replace(hour=0).replace(minute=0).replace(second=0) }}", @@ -147,7 +144,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test when duration value is not a timedelta.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -188,7 +184,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test config when not enough arguments provided.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -207,7 +202,6 @@ class TestHistoryStatsSensor(unittest.TestCase): """Test config when too many arguments provided.""" self.init_recorder() config = { - "history": {}, "sensor": { "platform": "history_stats", "entity_id": "binary_sensor.test_id", @@ -345,9 +339,9 @@ async def test_measure_multiple(hass): ) with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, - ), patch("homeassistant.components.history.get_state", return_value=None): + ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") await hass.async_block_till_done() @@ -422,9 +416,9 @@ async def async_test_measure(hass): ) with patch( - "homeassistant.components.history.state_changes_during_period", + "homeassistant.components.recorder.history.state_changes_during_period", return_value=fake_states, - ), patch("homeassistant.components.history.get_state", return_value=None): + ), patch("homeassistant.components.recorder.history.get_state", return_value=None): for i in range(1, 5): await hass.helpers.entity_component.async_update_entity(f"sensor.sensor{i}") await hass.async_block_till_done() diff --git a/tests/components/hive/__init__.py b/tests/components/hive/__init__.py new file mode 100644 index 00000000000..c10dcd8e690 --- /dev/null +++ b/tests/components/hive/__init__.py @@ -0,0 +1 @@ +"""Tests for hive.""" diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py index 6a13fae70dc..7a57bc20417 100644 --- a/tests/components/hlk_sw16/test_config_flow.py +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -104,9 +104,8 @@ async def test_form(hass): conf, ) - assert result4["type"] == "form" - assert result4["errors"] == {"base": "already_configured"} - await hass.async_block_till_done() + assert result4["type"] == "abort" + assert result4["reason"] == "already_configured" async def test_import(hass): diff --git a/tests/components/home_plus_control/conftest.py b/tests/components/home_plus_control/conftest.py index cb9c869002f..78a0da41fb8 100644 --- a/tests/components/home_plus_control/conftest.py +++ b/tests/components/home_plus_control/conftest.py @@ -3,7 +3,6 @@ from homepluscontrol.homeplusinteractivemodule import HomePlusInteractiveModule from homepluscontrol.homeplusplant import HomePlusPlant import pytest -from homeassistant import config_entries from homeassistant.components.home_plus_control.const import DOMAIN from tests.common import MockConfigEntry @@ -35,9 +34,7 @@ def mock_config_entry(): }, }, source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, options={}, - system_options={"disable_new_entities": False}, unique_id=DOMAIN, entry_id="home_plus_control_entry_id", ) diff --git a/tests/components/home_plus_control/test_init.py b/tests/components/home_plus_control/test_init.py index e48a9dc1f85..4da913047a2 100644 --- a/tests/components/home_plus_control/test_init.py +++ b/tests/components/home_plus_control/test_init.py @@ -36,7 +36,7 @@ async def test_loading(hass, mock_config_entry): await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED async def test_loading_with_no_config(hass, mock_config_entry): @@ -44,7 +44,7 @@ async def test_loading_with_no_config(hass, mock_config_entry): mock_config_entry.add_to_hass(hass) await setup.async_setup_component(hass, DOMAIN, {}) # Component setup fails because the oauth2 implementation could not be registered - assert mock_config_entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert mock_config_entry.state is config_entries.ConfigEntryState.SETUP_ERROR async def test_unloading(hass, mock_config_entry): @@ -68,8 +68,8 @@ async def test_unloading(hass, mock_config_entry): await hass.async_block_till_done() assert len(mock_check.mock_calls) == 1 - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED # We now unload the entry assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/home_plus_control/test_switch.py b/tests/components/home_plus_control/test_switch.py index f699fe08d05..aec23f0d32a 100644 --- a/tests/components/home_plus_control/test_switch.py +++ b/tests/components/home_plus_control/test_switch.py @@ -388,7 +388,7 @@ async def test_initial_api_error( assert len(mock_check.mock_calls) == 1 # The component has been loaded - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED # Check the entities and devices - None have been configured entity_assertions(hass, num_exp_entities=0) @@ -421,7 +421,7 @@ async def test_update_with_api_error( assert len(mock_check.mock_calls) == 1 # The component has been loaded - assert mock_config_entry.state == config_entries.ENTRY_STATE_LOADED + assert mock_config_entry.state is config_entries.ConfigEntryState.LOADED # Check the entities and devices - all entities should be there entity_assertions( diff --git a/tests/components/homekit/conftest.py b/tests/components/homekit/conftest.py index 469a0a7deb7..5441bcc195c 100644 --- a/tests/components/homekit/conftest.py +++ b/tests/components/homekit/conftest.py @@ -12,7 +12,7 @@ from tests.common import async_capture_events @pytest.fixture def hk_driver(loop): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer.async_stop"), patch( "pyhap.accessory_driver.HAPServer.async_start" diff --git a/tests/components/homekit/test_homekit.py b/tests/components/homekit/test_homekit.py index fd7d74aeaba..bd7af3b3596 100644 --- a/tests/components/homekit/test_homekit.py +++ b/tests/components/homekit/test_homekit.py @@ -250,7 +250,7 @@ async def test_homekit_setup(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=zeroconf_mock, + async_zeroconf_instance=zeroconf_mock, ) assert homekit.driver.safe_mode is False @@ -290,7 +290,7 @@ async def test_homekit_setup_ip_address(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address=None, - zeroconf_instance=mock_zeroconf, + async_zeroconf_instance=mock_zeroconf, ) @@ -315,10 +315,10 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): entry_title=entry.title, ) - zeroconf_instance = MagicMock() + async_zeroconf_instance = MagicMock() path = get_persist_fullpath_for_entry_id(hass, entry.entry_id) with patch(f"{PATH_HOMEKIT}.HomeDriver", return_value=hk_driver) as mock_driver: - await hass.async_add_executor_job(homekit.setup, zeroconf_instance) + await hass.async_add_executor_job(homekit.setup, async_zeroconf_instance) mock_driver.assert_called_with( hass, entry.entry_id, @@ -329,7 +329,7 @@ async def test_homekit_setup_advertise_ip(hass, hk_driver, mock_zeroconf): port=DEFAULT_PORT, persist_file=path, advertised_address="192.168.1.100", - zeroconf_instance=zeroconf_instance, + async_zeroconf_instance=async_zeroconf_instance, ) @@ -851,7 +851,7 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): options={}, ) assert await async_setup_component(hass, "zeroconf", {"zeroconf": {}}) - system_zc = await zeroconf.async_get_instance(hass) + system_async_zc = await zeroconf.async_get_async_instance(hass) with patch("pyhap.accessory_driver.AccessoryDriver.async_start"), patch( f"{PATH_HOMEKIT}.HomeKit.async_stop" @@ -859,7 +859,10 @@ async def test_homekit_uses_system_zeroconf(hass, hk_driver, mock_zeroconf): entry.add_to_hass(hass) assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser == system_zc + assert ( + hass.data[DOMAIN][entry.entry_id][HOMEKIT].driver.advertiser + == system_async_zc + ) assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() diff --git a/tests/components/homekit/test_type_cameras.py b/tests/components/homekit/test_type_cameras.py index ba08ea3caaf..354db900470 100644 --- a/tests/components/homekit/test_type_cameras.py +++ b/tests/components/homekit/test_type_cameras.py @@ -80,7 +80,7 @@ async def _async_stop_stream(hass, acc, session_info): @pytest.fixture() def run_driver(hass): """Return a custom AccessoryDriver instance for HomeKit accessory init.""" - with patch("pyhap.accessory_driver.Zeroconf"), patch( + with patch("pyhap.accessory_driver.AsyncZeroconf"), patch( "pyhap.accessory_driver.AccessoryEncoder" ), patch("pyhap.accessory_driver.HAPServer"), patch( "pyhap.accessory_driver.AccessoryDriver.publish" diff --git a/tests/components/homekit_controller/common.py b/tests/components/homekit_controller/common.py index a1b5f37324d..c3c182c8b51 100644 --- a/tests/components/homekit_controller/common.py +++ b/tests/components/homekit_controller/common.py @@ -9,7 +9,6 @@ from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes from aiohomekit.testing import FakeController -from homeassistant import config_entries from homeassistant.components.homekit_controller import config_flow from homeassistant.components.homekit_controller.const import ( CONTROLLER, @@ -99,7 +98,6 @@ async def setup_test_accessories(hass, accessories): entry_id="TestData", data={"AccessoryPairingID": pairing_id}, title="test", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, ) config_entry.add_to_hass(hass) diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index 3cde3912709..266fa177fb2 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -14,7 +14,7 @@ from tests.components.light.conftest import mock_light_profiles # noqa: F401 @pytest.fixture(autouse=True) def mock_zeroconf(): """Mock zeroconf.""" - with mock.patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + with mock.patch("homeassistant.components.zeroconf.models.HaZeroconf") as mock_zc: yield mock_zc.return_value diff --git a/tests/components/homekit_controller/specific_devices/__init__.py b/tests/components/homekit_controller/specific_devices/__init__.py new file mode 100644 index 00000000000..fc249d33623 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/__init__.py @@ -0,0 +1 @@ +"""Tests for specific devices.""" diff --git a/tests/components/homekit_controller/specific_devices/test_ecobee3.py b/tests/components/homekit_controller/specific_devices/test_ecobee3.py index ae050f67324..cc6c7ae4b9f 100644 --- a/tests/components/homekit_controller/specific_devices/test_ecobee3.py +++ b/tests/components/homekit_controller/specific_devices/test_ecobee3.py @@ -14,7 +14,7 @@ from homeassistant.components.climate.const import ( SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.components.homekit_controller.common import ( @@ -143,7 +143,7 @@ async def test_ecobee3_setup_connection_failure(hass): # If there is no cached entity map and the accessory connection is # failing then we have to fail the config entry setup. config_entry, pairing = await setup_test_accessories(hass, accessories) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY climate = entity_registry.async_get("climate.homew") assert climate is None diff --git a/tests/components/homekit_controller/specific_devices/test_haa_fan.py b/tests/components/homekit_controller/specific_devices/test_haa_fan.py new file mode 100644 index 00000000000..9e04434d830 --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_haa_fan.py @@ -0,0 +1,48 @@ +"""Make sure that a H.A.A. fan can be setup.""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_haa_fan_setup(hass): + """Test that a H.A.A. fan can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "haa_fan.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + device_registry = dr.async_get(hass) + + # Check that the switch entity is handled correctly + + entry = entity_registry.async_get("switch.haa_c718b3") + assert entry.unique_id == "homekit-C718B3-2-8" + + helper = Helper(hass, "switch.haa_c718b3", pairing, accessories[0], config_entry) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3" + + device = device_registry.async_get(entry.device_id) + assert device.manufacturer == "José A. Jiménez Campos" + assert device.name == "HAA-C718B3" + assert device.sw_version == "5.0.18" + assert device.via_device_id is not None + + # Assert the fan is detected + entry = entity_registry.async_get("fan.haa_c718b3") + assert entry.unique_id == "homekit-C718B3-1-8" + + helper = Helper( + hass, + "fan.haa_c718b3", + pairing, + accessories[0], + config_entry, + ) + state = await helper.poll_and_get_state() + assert state.attributes["friendly_name"] == "HAA-C718B3" + assert round(state.attributes["percentage_step"], 2) == 33.33 diff --git a/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py new file mode 100644 index 00000000000..58fe9df077f --- /dev/null +++ b/tests/components/homekit_controller/specific_devices/test_netamo_doorbell.py @@ -0,0 +1,73 @@ +""" +Regression tests for Netamo Doorbell. + +https://github.com/home-assistant/core/issues/44596 +""" + +from homeassistant.helpers import device_registry as dr, entity_registry as er + +from tests.common import assert_lists_same, async_get_device_automations +from tests.components.homekit_controller.common import ( + Helper, + setup_accessories_from_file, + setup_test_accessories, +) + + +async def test_netamo_doorbell_setup(hass): + """Test that a Netamo Doorbell can be correctly setup in HA.""" + accessories = await setup_accessories_from_file(hass, "netamo_doorbell.json") + config_entry, pairing = await setup_test_accessories(hass, accessories) + + entity_registry = er.async_get(hass) + + # Check that the camera is correctly found and set up + doorbell_id = "camera.netatmo_doorbell_g738658" + doorbell = entity_registry.async_get(doorbell_id) + assert doorbell.unique_id == "homekit-g738658-aid:1" + + camera_helper = Helper( + hass, + "camera.netatmo_doorbell_g738658", + pairing, + accessories[0], + config_entry, + ) + camera_helper = await camera_helper.poll_and_get_state() + assert camera_helper.attributes["friendly_name"] == "Netatmo-Doorbell-g738658" + + device_registry = dr.async_get(hass) + + device = device_registry.async_get(doorbell.device_id) + assert device.manufacturer == "Netatmo" + assert device.name == "Netatmo-Doorbell-g738658" + assert device.model == "Netatmo Doorbell" + assert device.sw_version == "80.0.0" + assert device.via_device_id is None + + # The fixture file has 1 button + expected = [] + for subtype in ("single_press", "double_press", "long_press"): + expected.append( + { + "device_id": doorbell.device_id, + "domain": "homekit_controller", + "platform": "device", + "type": "doorbell", + "subtype": subtype, + } + ) + + for type in ("no_motion", "motion"): + expected.append( + { + "device_id": doorbell.device_id, + "domain": "binary_sensor", + "entity_id": "binary_sensor.netatmo_doorbell_g738658", + "platform": "device", + "type": type, + } + ) + + triggers = await async_get_device_automations(hass, "trigger", doorbell.device_id) + assert_lists_same(triggers, expected) diff --git a/tests/components/homekit_controller/test_climate.py b/tests/components/homekit_controller/test_climate.py index 52671703cca..bc9fdaa1013 100644 --- a/tests/components/homekit_controller/test_climate.py +++ b/tests/components/homekit_controller/test_climate.py @@ -1,4 +1,6 @@ """Basic checks for HomeKitclimate.""" +from unittest.mock import patch + from aiohomekit.model.characteristics import ( ActivationStateValues, CharacteristicsTypes, @@ -19,6 +21,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_SWING_MODE, SERVICE_SET_TEMPERATURE, ) +from homeassistant.const import TEMP_FAHRENHEIT from tests.components.homekit_controller.common import setup_test_component @@ -445,6 +448,11 @@ async def test_climate_read_thermostat_state(hass, utcnow): state = await helper.poll_and_get_state() assert state.state == HVAC_MODE_HEAT_COOL + # Ensure converted Fahrenheit precision is reported in tenths + with patch.object(hass.config.units, "temperature_unit", TEMP_FAHRENHEIT): + state = await helper.poll_and_get_state() + assert state.attributes["current_temperature"] == 69.8 + async def test_hvac_mode_vs_hvac_action(hass, utcnow): """Check that we haven't conflated hvac_mode and hvac_action.""" diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 12381614a83..99c6966e827 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -42,6 +42,16 @@ PAIRING_FINISH_ABORT_ERRORS = [ (aiohomekit.AccessoryNotFoundError, "accessory_not_found_error") ] + +INSECURE_PAIRING_CODES = [ + "111-11-111", + "123-45-678", + "22222222", + "111-11-111 ", + " 111-11-111", +] + + INVALID_PAIRING_CODES = [ "aaa-aa-aaa", "aaa-11-aaa", @@ -49,11 +59,8 @@ INVALID_PAIRING_CODES = [ "aaa-aa-111", "1111-1-111", "a111-11-111", - " 111-11-111", - "111-11-111 ", "111-11-111a", "1111111", - "22222222", ] @@ -94,6 +101,15 @@ def test_invalid_pairing_codes(pairing_code): config_flow.ensure_pin_format(pairing_code) +@pytest.mark.parametrize("pairing_code", INSECURE_PAIRING_CODES) +def test_insecure_pairing_codes(pairing_code): + """Test ensure_pin_format raises for an invalid setup code.""" + with pytest.raises(config_flow.InsecureSetupCode): + config_flow.ensure_pin_format(pairing_code) + + config_flow.ensure_pin_format(pairing_code, allow_insecure_setup_codes=True) + + @pytest.mark.parametrize("pairing_code", VALID_PAIRING_CODES) def test_valid_pairing_codes(pairing_code): """Test ensure_pin_format corrects format for a valid pin in an alternative format.""" @@ -624,6 +640,49 @@ async def test_user_works(hass, controller): assert result["title"] == "Koogeek-LS1-20833F" +async def test_user_pairing_with_insecure_setup_code(hass, controller): + """Test user initiated disovers devices.""" + device = setup_mock_accessory(controller) + device.pairing_code = "123-45-678" + + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == "form" + assert result["step_id"] == "user" + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_USER, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"device": "TestDevice"} + ) + assert result["type"] == "form" + assert result["step_id"] == "pair" + + assert get_flow_context(hass, result) == { + "source": config_entries.SOURCE_USER, + "unique_id": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "123-45-678"} + ) + assert result["type"] == "form" + assert result["step_id"] == "pair" + assert result["errors"] == {"pairing_code": "insecure_setup_code"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={"pairing_code": "123-45-678", "allow_insecure_setup_codes": True}, + ) + assert result["type"] == "create_entry" + assert result["title"] == "Koogeek-LS1-20833F" + + async def test_user_no_devices(hass, controller): """Test user initiated pairing where no devices discovered.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/homekit_controller/test_storage.py b/tests/components/homekit_controller/test_storage.py index 4f0dabb9bc8..aa0a5e55057 100644 --- a/tests/components/homekit_controller/test_storage.py +++ b/tests/components/homekit_controller/test_storage.py @@ -95,8 +95,6 @@ async def test_storage_is_removed_on_config_entry_removal(hass, utcnow): "TestData", pairing_data, "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, ) assert hkid in hass.data[ENTITY_MAP].storage_data diff --git a/tests/components/homematicip_cloud/conftest.py b/tests/components/homematicip_cloud/conftest.py index 2d21f9cf861..c720df4a1bb 100644 --- a/tests/components/homematicip_cloud/conftest.py +++ b/tests/components/homematicip_cloud/conftest.py @@ -62,8 +62,6 @@ def hmip_config_entry_fixture() -> config_entries.ConfigEntry: unique_id=HAPID, data=entry_data, source=SOURCE_IMPORT, - connection_class=config_entries.CONN_CLASS_CLOUD_PUSH, - system_options={"disable_new_entities": False}, ) return config_entry diff --git a/tests/components/homematicip_cloud/test_hap.py b/tests/components/homematicip_cloud/test_hap.py index 1f85626980c..a8b3229e8db 100644 --- a/tests/components/homematicip_cloud/test_hap.py +++ b/tests/components/homematicip_cloud/test_hap.py @@ -18,7 +18,7 @@ from homeassistant.components.homematicip_cloud.hap import ( HomematicipAuth, HomematicipHAP, ) -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.exceptions import ConfigEntryNotReady from .helper import HAPID, HAPPIN @@ -109,7 +109,7 @@ async def test_hap_reset_unloads_entry_if_setup(hass, default_mock_hap_factory): # hap_reset is called during unload await hass.config_entries.async_unload(config_entries[0].entry_id) # entry is unloaded - assert config_entries[0].state == ENTRY_STATE_NOT_LOADED + assert config_entries[0].state is ConfigEntryState.NOT_LOADED assert hass.data[HMIPC_DOMAIN] == {} diff --git a/tests/components/homematicip_cloud/test_init.py b/tests/components/homematicip_cloud/test_init.py index 250cba81637..5354070c062 100644 --- a/tests/components/homematicip_cloud/test_init.py +++ b/tests/components/homematicip_cloud/test_init.py @@ -13,12 +13,7 @@ from homeassistant.components.homematicip_cloud.const import ( HMIPC_NAME, ) from homeassistant.components.homematicip_cloud.hap import HomematicipHAP -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_NAME from homeassistant.setup import async_setup_component @@ -116,7 +111,7 @@ async def test_load_entry_fails_due_to_connection_error( assert await async_setup_component(hass, HMIPC_DOMAIN, {}) assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] - assert hmip_config_entry.state == ENTRY_STATE_SETUP_RETRY + assert hmip_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry): @@ -132,7 +127,7 @@ async def test_load_entry_fails_due_to_generic_exception(hass, hmip_config_entry assert await async_setup_component(hass, HMIPC_DOMAIN, {}) assert hass.data[HMIPC_DOMAIN][hmip_config_entry.unique_id] - assert hmip_config_entry.state == ENTRY_STATE_SETUP_ERROR + assert hmip_config_entry.state is ConfigEntryState.SETUP_ERROR async def test_unload_entry(hass): @@ -157,9 +152,9 @@ async def test_unload_entry(hass): assert hass.data[HMIPC_DOMAIN]["ABC123"] config_entries = hass.config_entries.async_entries(HMIPC_DOMAIN) assert len(config_entries) == 1 - assert config_entries[0].state == ENTRY_STATE_LOADED + assert config_entries[0].state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entries[0].entry_id) - assert config_entries[0].state == ENTRY_STATE_NOT_LOADED + assert config_entries[0].state is ConfigEntryState.NOT_LOADED assert mock_hap.return_value.mock_calls[2][0] == "async_reset" # entry is unloaded assert hass.data[HMIPC_DOMAIN] == {} diff --git a/tests/components/http/test_auth.py b/tests/components/http/test_auth.py index 71c01630a67..6bd1d622b12 100644 --- a/tests/components/http/test_auth.py +++ b/tests/components/http/test_auth.py @@ -53,7 +53,7 @@ def app(hass): app = web.Application() app["hass"] = hass app.router.add_get("/", mock_handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) return app diff --git a/tests/components/http/test_forwarded.py b/tests/components/http/test_forwarded.py index 2946c0b383c..4b7a3421b0a 100644 --- a/tests/components/http/test_forwarded.py +++ b/tests/components/http/test_forwarded.py @@ -28,7 +28,7 @@ async def test_x_forwarded_for_without_trusted_proxy(aiohttp_client, caplog): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -74,7 +74,7 @@ async def test_x_forwarded_for_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) async_setup_forwarded( - app, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] + app, True, [ip_network(trusted_proxy) for trusted_proxy in trusted_proxies] ) mock_api_client = await aiohttp_client(app) @@ -83,6 +83,33 @@ async def test_x_forwarded_for_with_trusted_proxy( assert resp.status == 200 +async def test_x_forwarded_for_disabled_with_proxy(aiohttp_client, caplog): + """Test that we warn when processing is disabled, but proxy has been detected.""" + + async def handler(request): + url = mock_api_client.make_url("/") + assert request.host == f"{url.host}:{url.port}" + assert request.scheme == "http" + assert not request.secure + assert request.remote == "127.0.0.1" + + return web.Response() + + app = web.Application() + app.router.add_get("/", handler) + + async_setup_forwarded(app, False, []) + + mock_api_client = await aiohttp_client(app) + resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) + + assert resp.status == 200 + assert ( + "A request from a reverse proxy was received from 127.0.0.1, but your HTTP " + "integration is not set-up for reverse proxies" in caplog.text + ) + + async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): """Test that we get the IP from transport with untrusted proxy.""" @@ -97,7 +124,7 @@ async def test_x_forwarded_for_with_untrusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("1.1.1.1")]) + async_setup_forwarded(app, True, [ip_network("1.1.1.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_FOR: "255.255.255.255"}) @@ -119,7 +146,7 @@ async def test_x_forwarded_for_with_spoofed_header(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -148,7 +175,7 @@ async def test_x_forwarded_for_with_malformed_header( """Test that we get a HTTP 400 bad request with a malformed header.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -162,7 +189,7 @@ async def test_x_forwarded_for_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) @@ -193,7 +220,7 @@ async def test_x_forwarded_proto_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -245,7 +272,7 @@ async def test_x_forwarded_proto_with_trusted_proxy( app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -273,7 +300,7 @@ async def test_x_forwarded_proto_with_trusted_proxy_multiple_for(aiohttp_client) app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.0/24")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.0/24")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -301,7 +328,7 @@ async def test_x_forwarded_proto_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_PROTO: "https"}) @@ -313,7 +340,7 @@ async def test_x_forwarded_proto_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -339,7 +366,7 @@ async def test_x_forwarded_proto_empty_element( """Test that we get a HTTP 400 bad request with empty proto.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -364,7 +391,7 @@ async def test_x_forwarded_proto_incorrect_number_of_elements( """Test that we get a HTTP 400 bad request with incorrect number of elements.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -397,7 +424,7 @@ async def test_x_forwarded_host_without_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, []) + async_setup_forwarded(app, True, []) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -421,7 +448,7 @@ async def test_x_forwarded_host_with_trusted_proxy(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -446,7 +473,7 @@ async def test_x_forwarded_host_not_processed_without_for(aiohttp_client): app = web.Application() app.router.add_get("/", handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get("/", headers={X_FORWARDED_HOST: "example.com"}) @@ -458,7 +485,7 @@ async def test_x_forwarded_host_with_multiple_headers(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with multiple headers.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( @@ -478,7 +505,7 @@ async def test_x_forwarded_host_with_empty_header(aiohttp_client, caplog): """Test that we get a HTTP 400 bad request with empty host value.""" app = web.Application() app.router.add_get("/", mock_handler) - async_setup_forwarded(app, [ip_network("127.0.0.1")]) + async_setup_forwarded(app, True, [ip_network("127.0.0.1")]) mock_api_client = await aiohttp_client(app) resp = await mock_api_client.get( diff --git a/tests/components/http/test_init.py b/tests/components/http/test_init.py index 993f0dba1fd..65f01118c71 100644 --- a/tests/components/http/test_init.py +++ b/tests/components/http/test_init.py @@ -1,4 +1,5 @@ """The tests for the Home Assistant HTTP component.""" +from datetime import timedelta from ipaddress import ip_network import logging from unittest.mock import Mock, patch @@ -7,8 +8,11 @@ import pytest import homeassistant.components.http as http from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from homeassistant.util.ssl import server_context_intermediate, server_context_modern +from tests.common import async_fire_time_changed + @pytest.fixture def mock_stack(): @@ -189,6 +193,10 @@ async def test_storing_config(hass, aiohttp_client, aiohttp_unused_port): assert await async_setup_component(hass, http.DOMAIN, {http.DOMAIN: config}) await hass.async_start() + + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=200)) + await hass.async_block_till_done() + restored = await hass.components.http.async_get_last_config() restored["trusted_proxies"][0] = ip_network(restored["trusted_proxies"][0]) diff --git a/tests/components/hue/conftest.py b/tests/components/hue/conftest.py index 3fc55692cc4..648337d7539 100644 --- a/tests/components/hue/conftest.py +++ b/tests/components/hue/conftest.py @@ -1,5 +1,6 @@ """Test helpers for Hue.""" from collections import deque +import logging from unittest.mock import AsyncMock, Mock, patch from aiohue.groups import Groups @@ -8,7 +9,6 @@ from aiohue.scenes import Scenes from aiohue.sensors import Sensors import pytest -from homeassistant import config_entries from homeassistant.components import hue from homeassistant.components.hue import sensor_base as hue_sensor_base @@ -31,46 +31,47 @@ def create_mock_bridge(hass): authorized=True, allow_unreachable=False, allow_groups=False, - api=Mock(), + api=create_mock_api(hass), + config_entry=None, reset_jobs=[], spec=hue.HueBridge, ) bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = [] - # We're using a deque so we can schedule multiple responses - # and also means that `popleft()` will blow up if we get more updates - # than expected. - bridge.mock_light_responses = deque() - bridge.mock_group_responses = deque() - bridge.mock_sensor_responses = deque() + bridge.mock_requests = bridge.api.mock_requests + bridge.mock_light_responses = bridge.api.mock_light_responses + bridge.mock_group_responses = bridge.api.mock_group_responses + bridge.mock_sensor_responses = bridge.api.mock_sensor_responses - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) + async def async_setup(): + if bridge.config_entry: + hass.data.setdefault(hue.DOMAIN, {})[bridge.config_entry.entry_id] = bridge + return True - if path == "lights": - return bridge.mock_light_responses.popleft() - if path == "groups": - return bridge.mock_group_responses.popleft() - if path == "sensors": - return bridge.mock_sensor_responses.popleft() - return None + bridge.async_setup = async_setup async def async_request_call(task): await task() bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.lights = Lights({}, mock_request) - bridge.api.groups = Groups({}, mock_request) - bridge.api.sensors = Sensors({}, mock_request) + + async def async_reset(): + if bridge.config_entry: + hass.data[hue.DOMAIN].pop(bridge.config_entry.entry_id) + return True + + bridge.async_reset = async_reset + return bridge @pytest.fixture def mock_api(hass): """Mock the Hue api.""" + return create_mock_api(hass) + + +def create_mock_api(hass): + """Create a mock API.""" api = Mock(initialize=AsyncMock()) api.mock_requests = [] api.mock_light_responses = deque() @@ -93,11 +94,21 @@ def mock_api(hass): return api.mock_scene_responses.popleft() return None - api.config.apiversion = "9.9.9" - api.lights = Lights({}, mock_request) - api.groups = Groups({}, mock_request) - api.sensors = Sensors({}, mock_request) - api.scenes = Scenes({}, mock_request) + logger = logging.getLogger(__name__) + + api.config = Mock( + bridgeid="ff:ff:ff:ff:ff:ff", + mac="aa:bb:cc:dd:ee:ff", + modelid="BSB002", + apiversion="9.9.9", + swversion="1935144040", + ) + api.config.name = "Home" + + api.lights = Lights(logger, {}, [], mock_request) + api.groups = Groups(logger, {}, [], mock_request) + api.sensors = Sensors(logger, {}, [], mock_request) + api.scenes = Scenes(logger, {}, [], mock_request) return api @@ -116,8 +127,6 @@ async def setup_bridge_for_sensors(hass, mock_bridge, hostname=None): domain=hue.DOMAIN, title="Mock Title", data={"host": hostname}, - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} diff --git a/tests/components/hue/test_bridge.py b/tests/components/hue/test_bridge.py index 9792eefba5e..034acf88efa 100644 --- a/tests/components/hue/test_bridge.py +++ b/tests/components/hue/test_bridge.py @@ -1,4 +1,5 @@ """Test Hue bridge.""" +import asyncio from unittest.mock import AsyncMock, Mock, patch import pytest @@ -12,8 +13,19 @@ from homeassistant.components.hue.const import ( ) from homeassistant.exceptions import ConfigEntryNotReady +ORIG_SUBSCRIBE_EVENTS = bridge.HueBridge._subscribe_events -async def test_bridge_setup(hass): + +@pytest.fixture(autouse=True) +def mock_subscribe_events(): + """Mock subscribe events method.""" + with patch( + "homeassistant.components.hue.bridge.HueBridge._subscribe_events" + ) as mock: + yield mock + + +async def test_bridge_setup(hass, mock_subscribe_events): """Test a successful setup.""" entry = Mock() api = Mock(initialize=AsyncMock()) @@ -31,6 +43,8 @@ async def test_bridge_setup(hass): forward_entries = {c[1][1] for c in mock_forward.mock_calls} assert forward_entries == {"light", "binary_sensor", "sensor"} + assert len(mock_subscribe_events.mock_calls) == 1 + async def test_bridge_setup_invalid_username(hass): """Test we start config flow if username is no longer whitelisted.""" @@ -78,20 +92,23 @@ async def test_reset_if_entry_had_wrong_auth(hass): assert await hue_bridge.async_reset() -async def test_reset_unloads_entry_if_setup(hass): +async def test_reset_unloads_entry_if_setup(hass, mock_subscribe_events): """Test calling reset while the entry has been setup.""" entry = Mock() entry.data = {"host": "1.2.3.4", "username": "mock-username"} entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} hue_bridge = bridge.HueBridge(hass, entry) - with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( - "aiohue.Bridge", return_value=Mock() + with patch.object(bridge, "authenticate_bridge"), patch( + "aiohue.Bridge" ), patch.object(hass.config_entries, "async_forward_entry_setup") as mock_forward: assert await hue_bridge.async_setup() is True + await asyncio.sleep(0) + assert len(hass.services.async_services()) == 0 assert len(mock_forward.mock_calls) == 3 + assert len(mock_subscribe_events.mock_calls) == 1 with patch.object( hass.config_entries, "async_forward_entry_unload", return_value=True @@ -109,9 +126,7 @@ async def test_handle_unauthorized(hass): entry.options = {CONF_ALLOW_HUE_GROUPS: False, CONF_ALLOW_UNREACHABLE: False} hue_bridge = bridge.HueBridge(hass, entry) - with patch.object(bridge, "authenticate_bridge", return_value=Mock()), patch( - "aiohue.Bridge", return_value=Mock() - ): + with patch.object(bridge, "authenticate_bridge"), patch("aiohue.Bridge"): assert await hue_bridge.async_setup() is True assert hue_bridge.authorized is True @@ -166,8 +181,6 @@ async def test_hue_activate_scene(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -201,8 +214,6 @@ async def test_hue_activate_scene_transition(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -236,8 +247,6 @@ async def test_hue_activate_scene_group_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -266,8 +275,6 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): "Mock Title", {"host": "mock-host", "username": "mock-username"}, "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={CONF_ALLOW_HUE_GROUPS: True, CONF_ALLOW_UNREACHABLE: False}, ) hue_bridge = bridge.HueBridge(hass, config_entry) @@ -286,3 +293,77 @@ async def test_hue_activate_scene_scene_not_found(hass, mock_api): call.data = {"group_name": "Group 1", "scene_name": "Cozy dinner"} with patch("aiohue.Bridge", return_value=mock_api): assert await hue_bridge.hue_activate_scene(call.data) is False + + +async def test_event_updates(hass, caplog): + """Test calling reset while the entry has been setup.""" + events = asyncio.Queue() + + async def iterate_queue(): + while True: + event = await events.get() + if event is None: + return + yield event + + async def wait_empty_queue(): + count = 0 + while not events.empty() and count < 50: + await asyncio.sleep(0) + count += 1 + + hue_bridge = bridge.HueBridge(None, None) + hue_bridge.api = Mock(listen_events=iterate_queue) + subscription_task = asyncio.create_task(ORIG_SUBSCRIBE_EVENTS(hue_bridge)) + + calls = [] + + def obj_updated(): + calls.append(True) + + unsub = hue_bridge.listen_updates("lights", "2", obj_updated) + + events.put_nowait(Mock(ITEM_TYPE="lights", id="1")) + + await wait_empty_queue() + assert len(calls) == 0 + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 1 + + unsub() + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 1 + + # Test we can override update listener. + def obj_updated_false(): + calls.append(False) + + unsub = hue_bridge.listen_updates("lights", "2", obj_updated) + unsub_false = hue_bridge.listen_updates("lights", "2", obj_updated_false) + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 3 + assert calls[-2] is True + assert calls[-1] is False + + # Also call multiple times to make sure that works. + unsub() + unsub() + unsub_false() + unsub_false() + + events.put_nowait(Mock(ITEM_TYPE="lights", id="2")) + + await wait_empty_queue() + assert len(calls) == 3 + + events.put_nowait(None) + await subscription_task diff --git a/tests/components/hue/test_init.py b/tests/components/hue/test_init.py index 0c1d75c2ce2..afc920a6667 100644 --- a/tests/components/hue/test_init.py +++ b/tests/components/hue/test_init.py @@ -38,9 +38,14 @@ async def test_unload_entry(hass, mock_bridge_setup): assert await async_setup_component(hass, hue.DOMAIN, {}) is True assert len(mock_bridge_setup.mock_calls) == 1 - mock_bridge_setup.async_reset = AsyncMock(return_value=True) + hass.data[hue.DOMAIN] = {entry.entry_id: mock_bridge_setup} + + async def mock_reset(): + hass.data[hue.DOMAIN].pop(entry.entry_id) + return True + + mock_bridge_setup.async_reset = mock_reset assert await hue.async_unload_entry(hass, entry) - assert len(mock_bridge_setup.async_reset.mock_calls) == 1 assert hue.DOMAIN not in hass.data diff --git a/tests/components/hue/test_init_multiple_bridges.py b/tests/components/hue/test_init_multiple_bridges.py index 4e5378ae5e1..1e3df824a38 100644 --- a/tests/components/hue/test_init_multiple_bridges.py +++ b/tests/components/hue/test_init_multiple_bridges.py @@ -1,17 +1,13 @@ """Test Hue init with multiple bridges.""" -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import patch -from aiohue.groups import Groups -from aiohue.lights import Lights -from aiohue.scenes import Scenes -from aiohue.sensors import Sensors import pytest -from homeassistant import config_entries from homeassistant.components import hue -from homeassistant.components.hue import sensor_base as hue_sensor_base from homeassistant.setup import async_setup_component +from .conftest import create_mock_bridge + from tests.common import MockConfigEntry @@ -132,7 +128,6 @@ def create_config_entry(): return MockConfigEntry( domain=hue.DOMAIN, data={"host": "mock-host"}, - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, ) @@ -146,37 +141,3 @@ def mock_bridge1(hass): def mock_bridge2(hass): """Mock a Hue bridge.""" return create_mock_bridge(hass) - - -def create_mock_bridge(hass): - """Create a mock Hue bridge.""" - bridge = Mock( - hass=hass, - available=True, - authorized=True, - allow_unreachable=False, - allow_groups=False, - api=Mock(), - reset_jobs=[], - spec=hue.HueBridge, - async_setup=AsyncMock(return_value=True), - ) - bridge.sensor_manager = hue_sensor_base.SensorManager(bridge) - bridge.mock_requests = [] - - async def mock_request(method, path, **kwargs): - kwargs["method"] = method - kwargs["path"] = path - bridge.mock_requests.append(kwargs) - return {} - - async def async_request_call(task): - await task() - - bridge.async_request_call = async_request_call - bridge.api.config.apiversion = "9.9.9" - bridge.api.lights = Lights({}, mock_request) - bridge.api.groups = Groups({}, mock_request) - bridge.api.sensors = Sensors({}, mock_request) - bridge.api.scenes = Scenes({}, mock_request) - return bridge diff --git a/tests/components/hue/test_light.py b/tests/components/hue/test_light.py index df873536ce2..f4f663c23ae 100644 --- a/tests/components/hue/test_light.py +++ b/tests/components/hue/test_light.py @@ -179,8 +179,6 @@ async def setup_bridge(hass, mock_bridge): "Mock Title", {"host": "mock-host"}, "test", - config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, ) mock_bridge.config_entry = config_entry hass.data[hue.DOMAIN] = {config_entry.entry_id: mock_bridge} @@ -270,6 +268,10 @@ async def test_groups(hass, mock_bridge): mock_bridge.allow_groups = True mock_bridge.mock_light_responses.append({}) mock_bridge.mock_group_responses.append(GROUP_RESPONSE) + mock_bridge.api.groups._v2_resources = [ + {"id_v1": "/groups/1", "id": "group-1-mock-id", "type": "room"}, + {"id_v1": "/groups/2", "id": "group-2-mock-id", "type": "room"}, + ] await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 2 @@ -286,6 +288,10 @@ async def test_groups(hass, mock_bridge): assert lamp_2 is not None assert lamp_2.state == "on" + ent_reg = er.async_get(hass) + assert ent_reg.async_get("light.group_1").unique_id == "group-1-mock-id" + assert ent_reg.async_get("light.group_2").unique_id == "group-2-mock-id" + async def test_new_group_discovered(hass, mock_bridge): """Test if 2nd update has a new group.""" diff --git a/tests/components/hue/test_sensor_base.py b/tests/components/hue/test_sensor_base.py index eb7ece241c3..bc11c013555 100644 --- a/tests/components/hue/test_sensor_base.py +++ b/tests/components/hue/test_sensor_base.py @@ -4,10 +4,14 @@ from unittest.mock import Mock import aiohue +from homeassistant.components.hue import sensor_base from homeassistant.components.hue.hue_event import CONF_HUE_EVENT +from homeassistant.util import dt as dt_util from .conftest import create_mock_bridge, setup_bridge_for_sensors as setup_bridge +from tests.common import async_capture_events, async_fire_time_changed + PRESENCE_SENSOR_1_PRESENT = { "state": {"presence": True, "lastupdated": "2019-01-01T01:00:00"}, "swupdate": {"state": "noupdates", "lastinstall": "2019-01-01T00:00:00"}, @@ -435,55 +439,76 @@ async def test_hue_events(hass, mock_bridge): """Test that hue remotes fire events when pressed.""" mock_bridge.mock_sensor_responses.append(SENSOR_RESPONSE) - mock_listener = Mock() - unsub = hass.bus.async_listen(CONF_HUE_EVENT, mock_listener) + events = async_capture_events(hass, CONF_HUE_EVENT) await setup_bridge(hass, mock_bridge) assert len(mock_bridge.mock_requests) == 1 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 0 + assert len(events) == 0 new_sensor_response = dict(SENSOR_RESPONSE) new_sensor_response["7"]["state"] = { + "buttonevent": 18, + "lastupdated": "2019-12-28T22:58:03", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 2 + assert len(hass.states.async_all()) == 7 + assert len(events) == 1 + assert events[-1].data == { + "id": "hue_tap", + "unique_id": "00:00:00:00:00:44:23:08-f2", + "event": 18, + "last_updated": "2019-12-28T22:58:03", + } + + new_sensor_response = dict(new_sensor_response) + new_sensor_response["8"]["state"] = { + "buttonevent": 3002, + "lastupdated": "2019-12-28T22:58:03", + } + mock_bridge.mock_sensor_responses.append(new_sensor_response) + + # Force updates to run again + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) + await hass.async_block_till_done() + + assert len(mock_bridge.mock_requests) == 3 + assert len(hass.states.async_all()) == 7 + assert len(events) == 2 + assert events[-1].data == { + "id": "hue_dimmer_switch_1", + "unique_id": "00:17:88:01:10:3e:3a:dc-02-fc00", + "event": 3002, + "last_updated": "2019-12-28T22:58:03", + } + + # Fire old event, it should be ignored + new_sensor_response = dict(new_sensor_response) + new_sensor_response["8"]["state"] = { "buttonevent": 18, "lastupdated": "2019-12-28T22:58:02", } mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 2 + assert len(mock_bridge.mock_requests) == 4 assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 1 - assert mock_listener.mock_calls[0][1][0].data == { - "id": "hue_tap", - "unique_id": "00:00:00:00:00:44:23:08-f2", - "event": 18, - "last_updated": "2019-12-28T22:58:02", - } - - new_sensor_response = dict(new_sensor_response) - new_sensor_response["8"]["state"] = { - "buttonevent": 3002, - "lastupdated": "2019-12-28T22:58:01", - } - mock_bridge.mock_sensor_responses.append(new_sensor_response) - - # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() - await hass.async_block_till_done() - - assert len(mock_bridge.mock_requests) == 3 - assert len(hass.states.async_all()) == 7 - assert len(mock_listener.mock_calls) == 2 - assert mock_listener.mock_calls[1][1][0].data == { - "id": "hue_dimmer_switch_1", - "unique_id": "00:17:88:01:10:3e:3a:dc-02-fc00", - "event": 3002, - "last_updated": "2019-12-28T22:58:01", - } + assert len(events) == 2 # Add a new remote. In discovery the new event is registered **but not fired** new_sensor_response = dict(new_sensor_response) @@ -521,29 +546,31 @@ async def test_hue_events(hass, mock_bridge): mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 4 + assert len(mock_bridge.mock_requests) == 5 assert len(hass.states.async_all()) == 8 - assert len(mock_listener.mock_calls) == 2 + assert len(events) == 2 # A new press fires the event new_sensor_response["21"]["state"]["lastupdated"] = "2020-01-31T15:57:19" mock_bridge.mock_sensor_responses.append(new_sensor_response) # Force updates to run again - await mock_bridge.sensor_manager.coordinator.async_refresh() + async_fire_time_changed( + hass, dt_util.utcnow() + sensor_base.SensorManager.SCAN_INTERVAL + ) await hass.async_block_till_done() - assert len(mock_bridge.mock_requests) == 5 + assert len(mock_bridge.mock_requests) == 6 assert len(hass.states.async_all()) == 8 - assert len(mock_listener.mock_calls) == 3 - assert mock_listener.mock_calls[2][1][0].data == { + assert len(events) == 3 + assert events[-1].data == { "id": "lutron_aurora_1", "unique_id": "ff:ff:00:0f:e7:fd:bc:b7-01-fc00-0014", "event": 2, "last_updated": "2020-01-31T15:57:19", } - - unsub() diff --git a/tests/components/huisbaasje/test_init.py b/tests/components/huisbaasje/test_init.py index 2d68cdf8a11..390dc6c304d 100644 --- a/tests/components/huisbaasje/test_init.py +++ b/tests/components/huisbaasje/test_init.py @@ -4,12 +4,7 @@ from unittest.mock import patch from huisbaasje import HuisbaasjeException from homeassistant.components import huisbaasje -from homeassistant.config_entries import ( - CONN_CLASS_CLOUD_POLL, - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME, STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component @@ -46,17 +41,15 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Assert integration is loaded - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED assert huisbaasje.DOMAIN in hass.config.components assert huisbaasje.DOMAIN in hass.data assert config_entry.entry_id in hass.data[huisbaasje.DOMAIN] @@ -87,17 +80,15 @@ async def test_setup_entry_error(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() # Assert integration is loaded with error - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR assert huisbaasje.DOMAIN not in hass.data # Assert entities are not loaded @@ -129,21 +120,19 @@ async def test_unload_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={}, ) config_entry.add_to_hass(hass) # Load config entry assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED entities = hass.states.async_entity_ids("sensor") assert len(entities) == 14 # Unload config entry await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED entities = hass.states.async_entity_ids("sensor") assert len(entities) == 14 for entity in entities: diff --git a/tests/components/huisbaasje/test_sensor.py b/tests/components/huisbaasje/test_sensor.py index cfb17cd5f2d..45ce20af628 100644 --- a/tests/components/huisbaasje/test_sensor.py +++ b/tests/components/huisbaasje/test_sensor.py @@ -2,7 +2,6 @@ from unittest.mock import patch from homeassistant.components import huisbaasje -from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL from homeassistant.const import CONF_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -35,8 +34,6 @@ async def test_setup_entry(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={}, ) config_entry.add_to_hass(hass) @@ -92,8 +89,6 @@ async def test_setup_entry_absent_measurement(hass: HomeAssistant): CONF_PASSWORD: "password", }, source="test", - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={}, ) config_entry.add_to_hass(hass) diff --git a/tests/components/hunterdouglas_powerview/test_config_flow.py b/tests/components/hunterdouglas_powerview/test_config_flow.py index f88e65ff854..6423daff3f8 100644 --- a/tests/components/hunterdouglas_powerview/test_config_flow.py +++ b/tests/components/hunterdouglas_powerview/test_config_flow.py @@ -3,23 +3,72 @@ import asyncio import json from unittest.mock import AsyncMock, MagicMock, patch +import pytest + from homeassistant import config_entries, setup from homeassistant.components.hunterdouglas_powerview.const import DOMAIN from tests.common import MockConfigEntry, load_fixture +HOMEKIT_DISCOVERY_INFO = { + "name": "Hunter Douglas Powerview Hub._hap._tcp.local.", + "host": "1.2.3.4", + "properties": {"id": "AA::BB::CC::DD::EE::FF"}, +} + +ZEROCONF_DISCOVERY_INFO = { + "name": "Hunter Douglas Powerview Hub._powerview._tcp.local.", + "host": "1.2.3.4", +} + +DHCP_DISCOVERY_INFO = {"hostname": "Hunter Douglas Powerview Hub", "ip": "1.2.3.4"} + +DISCOVERY_DATA = [ + ( + config_entries.SOURCE_HOMEKIT, + HOMEKIT_DISCOVERY_INFO, + ), + ( + config_entries.SOURCE_DHCP, + DHCP_DISCOVERY_INFO, + ), + (config_entries.SOURCE_ZEROCONF, ZEROCONF_DISCOVERY_INFO), +] + def _get_mock_powerview_userdata(userdata=None, get_resources=None): mock_powerview_userdata = MagicMock() if not userdata: userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata.json")) if get_resources: - type(mock_powerview_userdata).get_resources = AsyncMock( + mock_powerview_userdata.get_resources = AsyncMock(side_effect=get_resources) + else: + mock_powerview_userdata.get_resources = AsyncMock(return_value=userdata) + return mock_powerview_userdata + + +def _get_mock_powerview_legacy_userdata(userdata=None, get_resources=None): + mock_powerview_userdata_legacy = MagicMock() + if not userdata: + userdata = json.loads(load_fixture("hunterdouglas_powerview/userdata_v1.json")) + if get_resources: + mock_powerview_userdata_legacy.get_resources = AsyncMock( side_effect=get_resources ) else: - type(mock_powerview_userdata).get_resources = AsyncMock(return_value=userdata) - return mock_powerview_userdata + mock_powerview_userdata_legacy.get_resources = AsyncMock(return_value=userdata) + return mock_powerview_userdata_legacy + + +def _get_mock_powerview_fwversion(fwversion=None, get_resources=None): + mock_powerview_fwversion = MagicMock() + if not fwversion: + fwversion = json.loads(load_fixture("hunterdouglas_powerview/fwversion.json")) + if get_resources: + mock_powerview_fwversion.get_resources = AsyncMock(side_effect=get_resources) + else: + mock_powerview_fwversion.get_resources = AsyncMock(return_value=fwversion) + return mock_powerview_fwversion async def test_user_form(hass): @@ -65,8 +114,83 @@ async def test_user_form(hass): assert result4["type"] == "abort" -async def test_form_homekit(hass): - """Test we get the form with homekit source.""" +async def test_user_form_legacy(hass): + """Test we get the user form with a legacy device.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + mock_powerview_userdata = _get_mock_powerview_legacy_userdata() + mock_powerview_fwversion = _get_mock_powerview_fwversion() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ), patch( + "homeassistant.components.hunterdouglas_powerview.ApiEntryPoint", + return_value=mock_powerview_fwversion, + ), patch( + "homeassistant.components.hunterdouglas_powerview.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.2.3.4"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "create_entry" + assert result2["title"] == "PowerView Hub Gen 1" + assert result2["data"] == { + "host": "1.2.3.4", + } + assert len(mock_setup_entry.mock_calls) == 1 + + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result3["type"] == "form" + assert result3["errors"] == {} + + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + {"host": "1.2.3.4"}, + ) + assert result4["type"] == "abort" + + +@pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) +async def test_form_homekit_and_dhcp_cannot_connect(hass, source, discovery_info): + """Test we get the form with homekit and dhcp source.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + ignored_config_entry = MockConfigEntry( + domain=DOMAIN, data={}, source=config_entries.SOURCE_IGNORE + ) + ignored_config_entry.add_to_hass(hass) + + mock_powerview_userdata = _get_mock_powerview_userdata( + get_resources=asyncio.TimeoutError + ) + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=discovery_info, + ) + + assert result["type"] == "abort" + assert result["reason"] == "cannot_connect" + + +@pytest.mark.parametrize("source, discovery_info", DISCOVERY_DATA) +async def test_form_homekit_and_dhcp(hass, source, discovery_info): + """Test we get the form with homekit and dhcp source.""" await setup.async_setup_component(hass, "persistent_notification", {}) ignored_config_entry = MockConfigEntry( @@ -81,12 +205,8 @@ async def test_form_homekit(hass): ): result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={ - "host": "1.2.3.4", - "properties": {"id": "AA::BB::CC::DD::EE::FF"}, - "name": "PowerViewHub._hap._tcp.local.", - }, + context={"source": source}, + data=discovery_info, ) assert result["type"] == "form" @@ -94,7 +214,7 @@ async def test_form_homekit(hass): assert result["errors"] is None assert result["description_placeholders"] == { "host": "1.2.3.4", - "name": "PowerViewHub", + "name": "Hunter Douglas Powerview Hub", } with patch( @@ -108,7 +228,7 @@ async def test_form_homekit(hass): await hass.async_block_till_done() assert result2["type"] == "create_entry" - assert result2["title"] == "PowerViewHub" + assert result2["title"] == "Hunter Douglas Powerview Hub" assert result2["data"] == {"host": "1.2.3.4"} assert result2["result"].unique_id == "ABC123" @@ -116,16 +236,44 @@ async def test_form_homekit(hass): result3 = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={ - "host": "1.2.3.4", - "properties": {"id": "AA::BB::CC::DD::EE::FF"}, - "name": "PowerViewHub._hap._tcp.local.", - }, + context={"source": source}, + data=discovery_info, ) assert result3["type"] == "abort" +async def test_discovered_by_homekit_and_dhcp(hass): + """Test we get the form with homekit and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mock_powerview_userdata = _get_mock_powerview_userdata() + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data=HOMEKIT_DISCOVERY_INFO, + ) + + assert result["type"] == "form" + assert result["step_id"] == "link" + + with patch( + "homeassistant.components.hunterdouglas_powerview.UserData", + return_value=mock_powerview_userdata, + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=DHCP_DISCOVERY_INFO, + ) + + assert result2["type"] == "abort" + assert result2["reason"] == "already_in_progress" + + async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" result = await hass.config_entries.flow.async_init( diff --git a/tests/components/hvv_departures/test_config_flow.py b/tests/components/hvv_departures/test_config_flow.py index 2042c6b95d1..9c510bb3db0 100644 --- a/tests/components/hvv_departures/test_config_flow.py +++ b/tests/components/hvv_departures/test_config_flow.py @@ -11,7 +11,7 @@ from homeassistant.components.hvv_departures.const import ( CONF_STATION, DOMAIN, ) -from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_OFFSET, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry, load_fixture @@ -256,8 +256,6 @@ async def test_options_flow(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -307,8 +305,6 @@ async def test_options_flow_invalid_auth(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) @@ -348,8 +344,6 @@ async def test_options_flow_cannot_connect(hass): title="Wartenau", data=FIXTURE_CONFIG_ENTRY, source=SOURCE_USER, - connection_class=CONN_CLASS_CLOUD_POLL, - system_options={"disable_new_entities": False}, options=FIXTURE_OPTIONS, unique_id="1234", ) diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index de0110cb19f..0c6b2cf41df 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -25,10 +25,10 @@ from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, ) from homeassistant.config_entries import ( - ENTRY_STATE_SETUP_ERROR, RELOAD_AFTER_UPDATE_DELAY, SOURCE_REAUTH, ConfigEntry, + ConfigEntryState, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -900,7 +900,7 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistant) -> None: }, data=config_entry.data, ) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None: @@ -928,7 +928,7 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistant) -> None: }, data=config_entry.data, ) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_priority_light_async_updates( diff --git a/tests/components/ialarm/test_init.py b/tests/components/ialarm/test_init.py index 8998b4e0d18..f33234c7256 100644 --- a/tests/components/ialarm/test_init.py +++ b/tests/components/ialarm/test_init.py @@ -5,11 +5,7 @@ from uuid import uuid4 import pytest from homeassistant.components.ialarm.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT from tests.common import MockConfigEntry @@ -41,7 +37,7 @@ async def test_setup_entry(hass, ialarm_api, mock_config_entry): await hass.async_block_till_done() ialarm_api.return_value.get_mac.assert_called_once() - assert mock_config_entry.state == ENTRY_STATE_LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED async def test_setup_not_ready(hass, ialarm_api, mock_config_entry): @@ -51,7 +47,7 @@ async def test_setup_not_ready(hass, ialarm_api, mock_config_entry): mock_config_entry.add_to_hass(hass) assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ENTRY_STATE_SETUP_RETRY + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass, ialarm_api, mock_config_entry): @@ -62,6 +58,6 @@ async def test_unload_entry(hass, ialarm_api, mock_config_entry): assert await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() - assert mock_config_entry.state == ENTRY_STATE_LOADED + assert mock_config_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(mock_config_entry.entry_id) - assert mock_config_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/image_processing/test_init.py b/tests/components/image_processing/test_init.py index 3e6a3cc960f..55c76273ad7 100644 --- a/tests/components/image_processing/test_init.py +++ b/tests/components/image_processing/test_init.py @@ -6,6 +6,7 @@ import homeassistant.components.image_processing as ip from homeassistant.const import ATTR_ENTITY_PICTURE from homeassistant.core import callback from homeassistant.exceptions import HomeAssistantError +from homeassistant.loader import DATA_CUSTOM_COMPONENTS from homeassistant.setup import setup_component from tests.common import ( @@ -50,6 +51,7 @@ class TestImageProcessing: def setup_method(self): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + self.hass.data.pop(DATA_CUSTOM_COMPONENTS) setup_component( self.hass, diff --git a/tests/components/insteon/test_config_flow.py b/tests/components/insteon/test_config_flow.py index 796c9b69d59..172cb6936b3 100644 --- a/tests/components/insteon/test_config_flow.py +++ b/tests/components/insteon/test_config_flow.py @@ -110,7 +110,7 @@ async def test_fail_on_existing(hass: HomeAssistant): options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await hass.config_entries.flow.async_init( DOMAIN, @@ -276,7 +276,7 @@ async def test_import_existing(hass: HomeAssistant): options={}, ) config_entry.add_to_hass(hass) - assert config_entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert config_entry.state is config_entries.ConfigEntryState.NOT_LOADED result = await _import_config( hass, {**MOCK_IMPORT_MINIMUM_HUB_V2, CONF_PORT: 25105, CONF_HUB_VERSION: 2} diff --git a/tests/components/ipp/test_init.py b/tests/components/ipp/test_init.py index f06be4fc8b5..5caffc62d7b 100644 --- a/tests/components/ipp/test_init.py +++ b/tests/components/ipp/test_init.py @@ -1,10 +1,6 @@ """Tests for the IPP integration.""" from homeassistant.components.ipp.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.ipp import init_integration @@ -16,7 +12,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the IPP configuration entry not ready.""" entry = await init_integration(hass, aioclient_mock, conn_error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -27,10 +23,10 @@ async def test_unload_config_entry( assert hass.data[DOMAIN] assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/islamic_prayer_times/test_init.py b/tests/components/islamic_prayer_times/test_init.py index 850edc4b76d..8ca16c50467 100644 --- a/tests/components/islamic_prayer_times/test_init.py +++ b/tests/components/islamic_prayer_times/test_init.py @@ -52,7 +52,7 @@ async def test_successful_config_entry(hass, legacy_patchable_time): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.options == { islamic_prayer_times.CONF_CALC_METHOD: islamic_prayer_times.DEFAULT_CALC_METHOD } @@ -74,7 +74,7 @@ async def test_setup_failed(hass, legacy_patchable_time): ): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass, legacy_patchable_time): @@ -93,7 +93,7 @@ async def test_unload_entry(hass, legacy_patchable_time): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert islamic_prayer_times.DOMAIN not in hass.data diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 1107e184e9b..e5458a3c96b 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -1,10 +1,11 @@ """Test the Universal Devices ISY994 config flow.""" - +import re from unittest.mock import patch +from pyisy import ISYConnectionError, ISYInvalidAuthError + from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components import ssdp -from homeassistant.components.isy994.config_flow import CannotConnect +from homeassistant.components import dhcp, ssdp from homeassistant.components.isy994.const import ( CONF_IGNORE_STRING, CONF_RESTORE_LIGHT_STATE, @@ -15,7 +16,7 @@ from homeassistant.components.isy994.const import ( ISY_URL_POSTFIX, UDN_UUID_PREFIX, ) -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_SSDP +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_IMPORT, SOURCE_SSDP from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -61,13 +62,32 @@ MOCK_IMPORT_FULL_CONFIG = { } MOCK_DEVICE_NAME = "Name of the device" -MOCK_UUID = "CE:FB:72:31:B7:B9" -MOCK_VALIDATED_RESPONSE = {"name": MOCK_DEVICE_NAME, "uuid": MOCK_UUID} +MOCK_UUID = "ce:fb:72:31:b7:b9" +MOCK_MAC = "cefb7231b7b9" -PATCH_CONFIGURATION = "homeassistant.components.isy994.config_flow.Configuration" -PATCH_CONNECTION = "homeassistant.components.isy994.config_flow.Connection" -PATCH_ASYNC_SETUP = "homeassistant.components.isy994.async_setup" -PATCH_ASYNC_SETUP_ENTRY = "homeassistant.components.isy994.async_setup_entry" +MOCK_CONFIG_RESPONSE = """ + + 5.0.16C + ISY-C-994 + + ce:fb:72:31:b7:b9 + Name of the device + + + + 21040 + Networking Module + true + true + + + +""" + +INTEGRATION = "homeassistant.components.isy994" +PATCH_CONNECTION = f"{INTEGRATION}.config_flow.Connection.test_connection" +PATCH_ASYNC_SETUP = f"{INTEGRATION}.async_setup" +PATCH_ASYNC_SETUP_ENTRY = f"{INTEGRATION}.async_setup_entry" async def test_form(hass: HomeAssistant): @@ -79,17 +99,12 @@ async def test_form(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ) as mock_setup_entry: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -128,9 +143,9 @@ async def test_form_invalid_auth(hass: HomeAssistant): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(PATCH_CONFIGURATION), patch( + with patch( PATCH_CONNECTION, - side_effect=ValueError("PyISY could not connect to the ISY."), + side_effect=ISYInvalidAuthError(), ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -141,14 +156,52 @@ async def test_form_invalid_auth(hass: HomeAssistant): assert result2["errors"] == {"base": "invalid_auth"} -async def test_form_cannot_connect(hass: HomeAssistant): - """Test we handle cannot connect error.""" +async def test_form_isy_connection_error(hass: HomeAssistant): + """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - with patch(PATCH_CONFIGURATION), patch( + with patch( PATCH_CONNECTION, - side_effect=CannotConnect, + side_effect=ISYConnectionError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_isy_parse_response_error(hass: HomeAssistant, caplog): + """Test we handle poorly formatted XML response from ISY.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + PATCH_CONNECTION, + return_value=MOCK_CONFIG_RESPONSE.rsplit("\n", 3)[0], # Test with invalid XML + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert "ISY Could not parse response, poorly formatted XML." in caplog.text + + +async def test_form_no_name_in_response(hass: HomeAssistant): + """Test we handle invalid response from ISY with name not set.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + with patch( + PATCH_CONNECTION, + return_value=re.sub( + r"\.*\n", "", MOCK_CONFIG_RESPONSE + ), # Test with line removed. ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], @@ -169,12 +222,7 @@ async def test_form_existing_config_entry(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, @@ -184,15 +232,12 @@ async def test_form_existing_config_entry(hass: HomeAssistant): async def test_import_flow_some_fields(hass: HomeAssistant) -> None: """Test import config flow with just the basic fields.""" - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ): - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -208,15 +253,12 @@ async def test_import_flow_some_fields(hass: HomeAssistant) -> None: async def test_import_flow_with_https(hass: HomeAssistant) -> None: """Test import config with https.""" - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ): - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -231,15 +273,12 @@ async def test_import_flow_with_https(hass: HomeAssistant) -> None: async def test_import_flow_all_fields(hass: HomeAssistant) -> None: """Test import config flow with all fields.""" - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch(PATCH_ASYNC_SETUP, return_value=True), patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ), patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ): - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_IMPORT}, @@ -296,17 +335,49 @@ async def test_form_ssdp(hass: HomeAssistant): assert result["step_id"] == "user" assert result["errors"] == {} - with patch(PATCH_CONFIGURATION) as mock_config_class, patch( - PATCH_CONNECTION - ) as mock_connection_class, patch( + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + PATCH_ASYNC_SETUP, return_value=True + ) as mock_setup, patch( + PATCH_ASYNC_SETUP_ENTRY, + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + MOCK_USER_INPUT, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == f"{MOCK_DEVICE_NAME} ({MOCK_HOSTNAME})" + assert result2["result"].unique_id == MOCK_UUID + assert result2["data"] == MOCK_USER_INPUT + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_dhcp(hass: HomeAssistant): + """Test we can setup from dhcp.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_DHCP}, + data={ + dhcp.IP_ADDRESS: "1.2.3.4", + dhcp.HOSTNAME: "isy994-ems", + dhcp.MAC_ADDRESS: MOCK_MAC, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"] == {} + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( PATCH_ASYNC_SETUP, return_value=True ) as mock_setup, patch( PATCH_ASYNC_SETUP_ENTRY, return_value=True, ) as mock_setup_entry: - isy_conn = mock_connection_class.return_value - isy_conn.get_config.return_value = None - mock_config_class.return_value = MOCK_VALIDATED_RESPONSE result2 = await hass.config_entries.flow.async_configure( result["flow_id"], MOCK_USER_INPUT, diff --git a/tests/components/isy994/test_system_health.py b/tests/components/isy994/test_system_health.py new file mode 100644 index 00000000000..63810b10464 --- /dev/null +++ b/tests/components/isy994/test_system_health.py @@ -0,0 +1,86 @@ +"""Test ISY994 system health.""" +import asyncio +from unittest.mock import Mock + +from aiohttp import ClientError + +from homeassistant.components.isy994.const import DOMAIN, ISY994_ISY, ISY_URL_POSTFIX +from homeassistant.const import CONF_HOST +from homeassistant.setup import async_setup_component + +from .test_config_flow import MOCK_HOSTNAME, MOCK_UUID + +from tests.common import MockConfigEntry, get_system_health_info + +MOCK_ENTRY_ID = "cad4af20b811990e757588519917d6af" +MOCK_CONNECTED = "connected" +MOCK_HEARTBEAT = "2021-05-01T00:00:00.000000" + + +async def test_system_health(hass, aioclient_mock): + """Test system health.""" + aioclient_mock.get(f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", text="") + + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + MockConfigEntry( + domain=DOMAIN, + entry_id=MOCK_ENTRY_ID, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}"}, + unique_id=MOCK_UUID, + ).add_to_hass(hass) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info["host_reachable"] == "ok" + assert info["device_connected"] + assert info["last_heartbeat"] == MOCK_HEARTBEAT + assert info["websocket_status"] == MOCK_CONNECTED + + +async def test_system_health_failed_connect(hass, aioclient_mock): + """Test system health.""" + aioclient_mock.get(f"http://{MOCK_HOSTNAME}{ISY_URL_POSTFIX}", exc=ClientError) + + hass.config.components.add(DOMAIN) + assert await async_setup_component(hass, "system_health", {}) + + MockConfigEntry( + domain=DOMAIN, + entry_id=MOCK_ENTRY_ID, + data={CONF_HOST: f"http://{MOCK_HOSTNAME}"}, + unique_id=MOCK_UUID, + ).add_to_hass(hass) + + hass.data[DOMAIN] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID] = {} + hass.data[DOMAIN][MOCK_ENTRY_ID][ISY994_ISY] = Mock( + connected=True, + websocket=Mock( + last_heartbeat=MOCK_HEARTBEAT, + status=MOCK_CONNECTED, + ), + ) + + info = await get_system_health_info(hass, DOMAIN) + + for key, val in info.items(): + if asyncio.iscoroutine(val): + info[key] = await val + + assert info["host_reachable"] == {"error": "unreachable", "type": "failed"} diff --git a/tests/components/jewish_calendar/__init__.py b/tests/components/jewish_calendar/__init__.py index 2d42458cf1b..b0279fd2748 100644 --- a/tests/components/jewish_calendar/__init__.py +++ b/tests/components/jewish_calendar/__init__.py @@ -26,7 +26,9 @@ def make_nyc_test_params(dtime, results, havdalah_offset=0): if isinstance(results, dict): time_zone = dt_util.get_time_zone("America/New_York") results = { - key: time_zone.localize(value) if isinstance(value, datetime) else value + key: value.replace(tzinfo=time_zone) + if isinstance(value, datetime) + else value for key, value in results.items() } return ( @@ -46,7 +48,9 @@ def make_jerusalem_test_params(dtime, results, havdalah_offset=0): if isinstance(results, dict): time_zone = dt_util.get_time_zone("Asia/Jerusalem") results = { - key: time_zone.localize(value) if isinstance(value, datetime) else value + key: value.replace(tzinfo=time_zone) + if isinstance(value, datetime) + else value for key, value in results.items() } return ( diff --git a/tests/components/jewish_calendar/test_binary_sensor.py b/tests/components/jewish_calendar/test_binary_sensor.py index 1f34532eeb5..b34dfdb28e4 100644 --- a/tests/components/jewish_calendar/test_binary_sensor.py +++ b/tests/components/jewish_calendar/test_binary_sensor.py @@ -179,9 +179,9 @@ async def test_issur_melacha_sensor( ): """Test Issur Melacha sensor output.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude @@ -214,7 +214,7 @@ async def test_issur_melacha_sensor( [ latitude, longitude, - time_zone, + tzname, HDATE_DEFAULT_ALTITUDE, diaspora, "english", @@ -270,9 +270,9 @@ async def test_issur_melacha_sensor_update( ): """Test Issur Melacha sensor output.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude diff --git a/tests/components/jewish_calendar/test_sensor.py b/tests/components/jewish_calendar/test_sensor.py index 8634f28d8fa..970e31c7985 100644 --- a/tests/components/jewish_calendar/test_sensor.py +++ b/tests/components/jewish_calendar/test_sensor.py @@ -163,9 +163,9 @@ async def test_jewish_calendar_sensor( ): """Test Jewish calendar sensor output.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude @@ -188,7 +188,9 @@ async def test_jewish_calendar_sensor( await hass.async_block_till_done() result = ( - dt_util.as_utc(time_zone.localize(result)) if isinstance(result, dt) else result + dt_util.as_utc(result.replace(tzinfo=time_zone)) + if isinstance(result, dt) + else result ) sensor_object = hass.states.get(f"sensor.test_{sensor}") @@ -506,9 +508,9 @@ async def test_shabbat_times_sensor( ): """Test sensor output for upcoming shabbat/yomtov times.""" time_zone = dt_util.get_time_zone(tzname) - test_time = time_zone.localize(now) + test_time = now.replace(tzinfo=time_zone) - hass.config.time_zone = time_zone + hass.config.time_zone = tzname hass.config.latitude = latitude hass.config.longitude = longitude @@ -559,7 +561,7 @@ async def test_shabbat_times_sensor( [ latitude, longitude, - time_zone, + tzname, HDATE_DEFAULT_ALTITUDE, diaspora, language, @@ -593,7 +595,7 @@ OMER_TEST_IDS = [ @pytest.mark.parametrize(["test_time", "result"], OMER_PARAMS, ids=OMER_TEST_IDS) async def test_omer_sensor(hass, legacy_patchable_time, test_time, result): """Test Omer Count sensor output.""" - test_time = hass.config.time_zone.localize(test_time) + test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): assert await async_setup_component( @@ -627,7 +629,7 @@ DAFYOMI_TEST_IDS = [ @pytest.mark.parametrize(["test_time", "result"], DAFYOMI_PARAMS, ids=DAFYOMI_TEST_IDS) async def test_dafyomi_sensor(hass, legacy_patchable_time, test_time, result): """Test Daf Yomi sensor output.""" - test_time = hass.config.time_zone.localize(test_time) + test_time = test_time.replace(tzinfo=dt_util.get_time_zone(hass.config.time_zone)) with alter_time(test_time): assert await async_setup_component( diff --git a/tests/components/keenetic_ndms2/__init__.py b/tests/components/keenetic_ndms2/__init__.py index 1fce0dbe2a6..9f96e56cdd0 100644 --- a/tests/components/keenetic_ndms2/__init__.py +++ b/tests/components/keenetic_ndms2/__init__.py @@ -1,4 +1,5 @@ """Tests for the Keenetic NDMS2 component.""" +from homeassistant.components import ssdp from homeassistant.components.keenetic_ndms2 import const from homeassistant.const import ( CONF_HOST, @@ -9,9 +10,11 @@ from homeassistant.const import ( ) MOCK_NAME = "Keenetic Ultra 2030" +MOCK_IP = "0.0.0.0" +SSDP_LOCATION = f"http://{MOCK_IP}/" MOCK_DATA = { - CONF_HOST: "0.0.0.0", + CONF_HOST: MOCK_IP, CONF_USERNAME: "user", CONF_PASSWORD: "pass", CONF_PORT: 23, @@ -25,3 +28,9 @@ MOCK_OPTIONS = { const.CONF_INCLUDE_ASSOCIATED: True, const.CONF_INTERFACES: ["Home", "VPS0"], } + +MOCK_SSDP_DISCOVERY_INFO = { + ssdp.ATTR_SSDP_LOCATION: SSDP_LOCATION, + ssdp.ATTR_UPNP_UDN: "uuid:xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx", + ssdp.ATTR_UPNP_FRIENDLY_NAME: MOCK_NAME, +} diff --git a/tests/components/keenetic_ndms2/test_config_flow.py b/tests/components/keenetic_ndms2/test_config_flow.py index 7561fb03839..7e7d4882544 100644 --- a/tests/components/keenetic_ndms2/test_config_flow.py +++ b/tests/components/keenetic_ndms2/test_config_flow.py @@ -7,11 +7,12 @@ from ndms2_client.client import InterfaceInfo, RouterInfo import pytest from homeassistant import config_entries, data_entry_flow -from homeassistant.components import keenetic_ndms2 as keenetic +from homeassistant.components import keenetic_ndms2 as keenetic, ssdp from homeassistant.components.keenetic_ndms2 import const +from homeassistant.const import CONF_HOST, CONF_SOURCE from homeassistant.core import HomeAssistant -from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS +from . import MOCK_DATA, MOCK_NAME, MOCK_OPTIONS, MOCK_SSDP_DISCOVERY_INFO from tests.common import MockConfigEntry @@ -43,7 +44,7 @@ def mock_keenetic_connect_failed(): yield -async def test_flow_works(hass: HomeAssistant, connect): +async def test_flow_works(hass: HomeAssistant, connect) -> None: """Test config flow.""" result = await hass.config_entries.flow.async_init( @@ -67,7 +68,7 @@ async def test_flow_works(hass: HomeAssistant, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_import_works(hass: HomeAssistant, connect): +async def test_import_works(hass: HomeAssistant, connect) -> None: """Test config flow.""" with patch( @@ -86,7 +87,7 @@ async def test_import_works(hass: HomeAssistant, connect): assert len(mock_setup_entry.mock_calls) == 1 -async def test_options(hass): +async def test_options(hass: HomeAssistant) -> None: """Test updating options.""" entry = MockConfigEntry(domain=keenetic.DOMAIN, data=MOCK_DATA) entry.add_to_hass(hass) @@ -127,7 +128,7 @@ async def test_options(hass): assert result2["data"] == MOCK_OPTIONS -async def test_host_already_configured(hass, connect): +async def test_host_already_configured(hass: HomeAssistant, connect) -> None: """Test host already configured.""" entry = MockConfigEntry( @@ -147,7 +148,7 @@ async def test_host_already_configured(hass, connect): assert result2["reason"] == "already_configured" -async def test_connection_error(hass, connect_error): +async def test_connection_error(hass: HomeAssistant, connect_error) -> None: """Test error when connection is unsuccessful.""" result = await hass.config_entries.flow.async_init( @@ -158,3 +159,88 @@ async def test_connection_error(hass, connect_error): ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": "cannot_connect"} + + +async def test_ssdp_works(hass: HomeAssistant, connect) -> None: + """Test host already configured and discovered.""" + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.keenetic_ndms2.async_setup_entry", return_value=True + ) as mock_setup_entry: + user_input = MOCK_DATA.copy() + user_input.pop(CONF_HOST) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input=user_input, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == MOCK_NAME + assert result2["data"] == MOCK_DATA + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_ssdp_already_configured(hass: HomeAssistant) -> None: + """Test host already configured and discovered.""" + + entry = MockConfigEntry( + domain=keenetic.DOMAIN, data=MOCK_DATA, options=MOCK_OPTIONS + ) + entry.add_to_hass(hass) + + discovery_info = MOCK_SSDP_DISCOVERY_INFO.copy() + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_ssdp_reject_no_udn(hass: HomeAssistant) -> None: + """Discovered device has no UDN.""" + + discovery_info = { + **MOCK_SSDP_DISCOVERY_INFO, + } + discovery_info.pop(ssdp.ATTR_UPNP_UDN) + + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "no_udn" + + +async def test_ssdp_reject_non_keenetic(hass: HomeAssistant) -> None: + """Discovered device does not look like a keenetic router.""" + + discovery_info = { + **MOCK_SSDP_DISCOVERY_INFO, + ssdp.ATTR_UPNP_FRIENDLY_NAME: "Suspicious device", + } + result = await hass.config_entries.flow.async_init( + keenetic.DOMAIN, + context={CONF_SOURCE: config_entries.SOURCE_SSDP}, + data=discovery_info, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "not_keenetic_ndms2" diff --git a/tests/components/kmtronic/test_config_flow.py b/tests/components/kmtronic/test_config_flow.py index 71482d6f7b2..b2f0f4b0515 100644 --- a/tests/components/kmtronic/test_config_flow.py +++ b/tests/components/kmtronic/test_config_flow.py @@ -5,7 +5,7 @@ from aiohttp import ClientConnectorError, ClientResponseError from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.kmtronic.const import CONF_REVERSE, DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.common import MockConfigEntry @@ -65,7 +65,7 @@ async def test_form_options(hass, aioclient_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM @@ -80,7 +80,7 @@ async def test_form_options(hass, aioclient_mock): await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == config_entries.ConfigEntryState.LOADED async def test_form_invalid_auth(hass): diff --git a/tests/components/kmtronic/test_init.py b/tests/components/kmtronic/test_init.py index 1b9cf7cb407..da1efa25d58 100644 --- a/tests/components/kmtronic/test_init.py +++ b/tests/components/kmtronic/test_init.py @@ -2,11 +2,7 @@ import asyncio from homeassistant.components.kmtronic.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from tests.common import MockConfigEntry @@ -31,12 +27,12 @@ async def test_unload_config_entry(hass, aioclient_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_config_entry_not_ready(hass, aioclient_mock): @@ -59,4 +55,4 @@ async def test_config_entry_not_ready(hass, aioclient_mock): await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/kodi/test_init.py b/tests/components/kodi/test_init.py index aa206270d35..6294eea45df 100644 --- a/tests/components/kodi/test_init.py +++ b/tests/components/kodi/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.kodi.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from . import init_integration @@ -16,10 +16,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/kostal_plenticore/test_config_flow.py b/tests/components/kostal_plenticore/test_config_flow.py index 7ce95f71e8e..2e9b25f5721 100644 --- a/tests/components/kostal_plenticore/test_config_flow.py +++ b/tests/components/kostal_plenticore/test_config_flow.py @@ -5,7 +5,6 @@ from unittest.mock import ANY, AsyncMock, MagicMock, patch from kostal.plenticore import PlenticoreAuthenticationException from homeassistant import config_entries, setup -from homeassistant.components.kostal_plenticore import config_flow from homeassistant.components.kostal_plenticore.const import DOMAIN from tests.common import MockConfigEntry @@ -188,16 +187,3 @@ async def test_already_configured(hass): assert result2["type"] == "abort" assert result2["reason"] == "already_configured" - - -def test_configured_instances(hass): - """Test configured_instances returns all configured hosts.""" - MockConfigEntry( - domain="kostal_plenticore", - data={"host": "2.2.2.2", "password": "foobar"}, - unique_id="112233445566", - ).add_to_hass(hass) - - result = config_flow.configured_instances(hass) - - assert result == {"2.2.2.2"} diff --git a/tests/components/kraken/__init__.py b/tests/components/kraken/__init__.py new file mode 100644 index 00000000000..26b12dd3789 --- /dev/null +++ b/tests/components/kraken/__init__.py @@ -0,0 +1 @@ +"""Tests for the kraken integration.""" diff --git a/tests/components/kraken/const.py b/tests/components/kraken/const.py new file mode 100644 index 00000000000..6e3174a9ae7 --- /dev/null +++ b/tests/components/kraken/const.py @@ -0,0 +1,80 @@ +"""Constants for kraken tests.""" +import pandas + +TRADEABLE_ASSET_PAIR_RESPONSE = pandas.DataFrame( + {"wsname": ["ADA/XBT", "ADA/ETH", "XBT/EUR", "XBT/GBP", "XBT/USD", "XBT/JPY"]}, + columns=["wsname"], + index=["ADAXBT", "ADAETH", "XBTEUR", "XXBTZGBP", "XXBTZUSD", "XXBTZJPY"], +) + +TICKER_INFORMATION_RESPONSE = pandas.DataFrame( + { + "a": [ + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + [0.000349400, 15949, 15949.000], + ], + "b": [ + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + [0.000348400, 20792, 20792.000], + ], + "c": [ + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + [0.000347800, 2809.36384377], + ], + "h": [ + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + [0.000351600, 0.000352100], + ], + "l": [ + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + [0.000344600, 0.000344600], + ], + "o": [ + 0.000351300, + 0.000351300, + 0.000351300, + 0.000351300, + 0.000351300, + 0.000351300, + ], + "p": [ + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + [0.000348573, 0.000344881], + ], + "t": [[82, 128], [82, 128], [82, 128], [82, 128], [82, 128], [82, 128]], + "v": [ + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + [146300.24906838, 253478.04715403], + ], + }, + columns=["a", "b", "c", "h", "l", "o", "p", "t", "v"], + index=["ADAXBT", "ADAETH", "XBTEUR", "XXBTZGBP", "XXBTZUSD", "XXBTZJPY"], +) diff --git a/tests/components/kraken/test_config_flow.py b/tests/components/kraken/test_config_flow.py new file mode 100644 index 00000000000..1a09fbe92c6 --- /dev/null +++ b/tests/components/kraken/test_config_flow.py @@ -0,0 +1,102 @@ +"""Tests for the kraken config_flow.""" +from unittest.mock import patch + +from homeassistant.components.kraken.const import CONF_TRACKED_ASSET_PAIRS, DOMAIN +from homeassistant.const import CONF_SCAN_INTERVAL + +from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE + +from tests.common import MockConfigEntry + + +async def test_config_flow(hass): + """Test we can finish a config flow.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == "create_entry" + + await hass.async_block_till_done() + state = hass.states.get("sensor.xbt_usd_ask") + assert state + + +async def test_already_configured(hass): + """Test we can not add a second config flow.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "form" + + result = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result["type"] == "create_entry" + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "user"} + ) + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + + +async def test_options(hass): + """Test options for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: 60, + CONF_TRACKED_ASSET_PAIRS: [ + "ADA/XBT", + "ADA/ETH", + "XBT/EUR", + "XBT/GBP", + "XBT/USD", + "XBT/JPY", + ], + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert hass.states.get("sensor.xbt_usd_ask") + + result = await hass.config_entries.options.async_init(entry.entry_id) + result = await hass.config_entries.options.async_configure( + result["flow_id"], + { + CONF_SCAN_INTERVAL: 10, + CONF_TRACKED_ASSET_PAIRS: ["ADA/ETH"], + }, + ) + assert result["type"] == "create_entry" + await hass.async_block_till_done() + + ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") + assert ada_eth_sensor.state == "0.0003494" + + assert hass.states.get("sensor.xbt_usd_ask") is None diff --git a/tests/components/kraken/test_init.py b/tests/components/kraken/test_init.py new file mode 100644 index 00000000000..742e48eb1c0 --- /dev/null +++ b/tests/components/kraken/test_init.py @@ -0,0 +1,65 @@ +"""Tests for the kraken integration.""" +from unittest.mock import patch + +from pykrakenapi.pykrakenapi import CallRateLimitError, KrakenAPIError + +from homeassistant.components.kraken.const import DOMAIN + +from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE + +from tests.common import MockConfigEntry + + +async def test_unload_entry(hass): + """Test unload for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert await hass.config_entries.async_unload(entry.entry_id) + assert DOMAIN not in hass.data + + +async def test_unkown_error(hass, caplog): + """Test unload for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=KrakenAPIError("EQuery: Error"), + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert "Unable to fetch data from Kraken.com:" in caplog.text + + +async def test_callrate_limit(hass, caplog): + """Test unload for Kraken.""" + with patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=CallRateLimitError(), + ): + entry = MockConfigEntry(domain=DOMAIN) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert ( + "Exceeded the Kraken.com call rate limit. Increase the update interval to prevent this error" + in caplog.text + ) diff --git a/tests/components/kraken/test_sensor.py b/tests/components/kraken/test_sensor.py new file mode 100644 index 00000000000..98760a3002d --- /dev/null +++ b/tests/components/kraken/test_sensor.py @@ -0,0 +1,267 @@ +"""Tests for the kraken sensor platform.""" +from datetime import timedelta +from unittest.mock import patch + +from pykrakenapi.pykrakenapi import KrakenAPIError + +from homeassistant.components.kraken.const import ( + CONF_TRACKED_ASSET_PAIRS, + DEFAULT_SCAN_INTERVAL, + DEFAULT_TRACKED_ASSET_PAIR, + DOMAIN, +) +from homeassistant.const import CONF_SCAN_INTERVAL, EVENT_HOMEASSISTANT_START +import homeassistant.util.dt as dt_util + +from .const import TICKER_INFORMATION_RESPONSE, TRADEABLE_ASSET_PAIR_RESPONSE + +from tests.common import MockConfigEntry, async_fire_time_changed + + +async def test_sensor(hass): + """Test that sensor has a value.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ), patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="0123456789", + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [ + "ADA/XBT", + "ADA/ETH", + "XBT/EUR", + "XBT/GBP", + "XBT/USD", + "XBT/JPY", + ], + }, + ) + entry.add_to_hass(hass) + + registry = await hass.helpers.entity_registry.async_get_registry() + + # Pre-create registry entries for disabled by default sensors + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_ask_volume", + suggested_object_id="xbt_usd_ask_volume", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_last_trade_closed", + suggested_object_id="xbt_usd_last_trade_closed", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_bid_volume", + suggested_object_id="xbt_usd_bid_volume", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_today", + suggested_object_id="xbt_usd_volume_today", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_last_24h", + suggested_object_id="xbt_usd_volume_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_weighted_average_today", + suggested_object_id="xbt_usd_volume_weighted_average_today", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_volume_weighted_average_last_24h", + suggested_object_id="xbt_usd_volume_weighted_average_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_number_of_trades_today", + suggested_object_id="xbt_usd_number_of_trades_today", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_number_of_trades_last_24h", + suggested_object_id="xbt_usd_number_of_trades_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_low_last_24h", + suggested_object_id="xbt_usd_low_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_high_last_24h", + suggested_object_id="xbt_usd_high_last_24h", + disabled_by=None, + ) + + registry.async_get_or_create( + "sensor", + DOMAIN, + "xbt_usd_opening_price_today", + suggested_object_id="xbt_usd_opening_price_today", + disabled_by=None, + ) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + xbt_usd_sensor = hass.states.get("sensor.xbt_usd_ask") + assert xbt_usd_sensor.state == "0.0003494" + assert xbt_usd_sensor.attributes["icon"] == "mdi:currency-usd" + + xbt_eur_sensor = hass.states.get("sensor.xbt_eur_ask") + assert xbt_eur_sensor.state == "0.0003494" + assert xbt_eur_sensor.attributes["icon"] == "mdi:currency-eur" + + ada_xbt_sensor = hass.states.get("sensor.ada_xbt_ask") + assert ada_xbt_sensor.state == "0.0003494" + assert ada_xbt_sensor.attributes["icon"] == "mdi:currency-btc" + + xbt_jpy_sensor = hass.states.get("sensor.xbt_jpy_ask") + assert xbt_jpy_sensor.state == "0.0003494" + assert xbt_jpy_sensor.attributes["icon"] == "mdi:currency-jpy" + + xbt_gbp_sensor = hass.states.get("sensor.xbt_gbp_ask") + assert xbt_gbp_sensor.state == "0.0003494" + assert xbt_gbp_sensor.attributes["icon"] == "mdi:currency-gbp" + + ada_eth_sensor = hass.states.get("sensor.ada_eth_ask") + assert ada_eth_sensor.state == "0.0003494" + assert ada_eth_sensor.attributes["icon"] == "mdi:cash" + + xbt_usd_ask_volume = hass.states.get("sensor.xbt_usd_ask_volume") + assert xbt_usd_ask_volume.state == "15949" + + xbt_usd_last_trade_closed = hass.states.get("sensor.xbt_usd_last_trade_closed") + assert xbt_usd_last_trade_closed.state == "0.0003478" + + xbt_usd_bid_volume = hass.states.get("sensor.xbt_usd_bid_volume") + assert xbt_usd_bid_volume.state == "20792" + + xbt_usd_volume_today = hass.states.get("sensor.xbt_usd_volume_today") + assert xbt_usd_volume_today.state == "146300.24906838" + + xbt_usd_volume_last_24h = hass.states.get("sensor.xbt_usd_volume_last_24h") + assert xbt_usd_volume_last_24h.state == "253478.04715403" + + xbt_usd_volume_weighted_average_today = hass.states.get( + "sensor.xbt_usd_volume_weighted_average_today" + ) + assert xbt_usd_volume_weighted_average_today.state == "0.000348573" + + xbt_usd_volume_weighted_average_last_24h = hass.states.get( + "sensor.xbt_usd_volume_weighted_average_last_24h" + ) + assert xbt_usd_volume_weighted_average_last_24h.state == "0.000344881" + + xbt_usd_number_of_trades_today = hass.states.get( + "sensor.xbt_usd_number_of_trades_today" + ) + assert xbt_usd_number_of_trades_today.state == "82" + + xbt_usd_number_of_trades_last_24h = hass.states.get( + "sensor.xbt_usd_number_of_trades_last_24h" + ) + assert xbt_usd_number_of_trades_last_24h.state == "128" + + xbt_usd_low_last_24h = hass.states.get("sensor.xbt_usd_low_last_24h") + assert xbt_usd_low_last_24h.state == "0.0003446" + + xbt_usd_high_last_24h = hass.states.get("sensor.xbt_usd_high_last_24h") + assert xbt_usd_high_last_24h.state == "0.0003521" + + xbt_usd_opening_price_today = hass.states.get( + "sensor.xbt_usd_opening_price_today" + ) + assert xbt_usd_opening_price_today.state == "0.0003513" + + +async def test_missing_pair_marks_sensor_unavailable(hass): + """Test that a missing tradable asset pair marks the sensor unavailable.""" + utcnow = dt_util.utcnow() + # Patching 'utcnow' to gain more control over the timed update. + with patch("homeassistant.util.dt.utcnow", return_value=utcnow), patch( + "pykrakenapi.KrakenAPI.get_tradable_asset_pairs", + return_value=TRADEABLE_ASSET_PAIR_RESPONSE, + ): + with patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + return_value=TICKER_INFORMATION_RESPONSE, + ): + entry = MockConfigEntry( + domain=DOMAIN, + options={ + CONF_SCAN_INTERVAL: DEFAULT_SCAN_INTERVAL, + CONF_TRACKED_ASSET_PAIRS: [DEFAULT_TRACKED_ASSET_PAIR], + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.xbt_usd_ask") + assert sensor.state == "0.0003494" + + with patch( + "pykrakenapi.KrakenAPI.get_ticker_information", + side_effect=KrakenAPIError("EQuery:Unknown asset pair"), + ): + async_fire_time_changed( + hass, utcnow + timedelta(seconds=DEFAULT_SCAN_INTERVAL * 2) + ) + await hass.async_block_till_done() + + sensor = hass.states.get("sensor.xbt_usd_ask") + assert sensor.state == "unavailable" diff --git a/tests/components/light/test_device_action.py b/tests/components/light/test_device_action.py index 440cae8f0fc..5628861b72d 100644 --- a/tests/components/light/test_device_action.py +++ b/tests/components/light/test_device_action.py @@ -256,7 +256,7 @@ async def test_get_action_capabilities_features( assert capabilities == expected -async def test_action(hass, calls): +async def test_action(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/light/test_device_condition.py b/tests/components/light/test_device_condition.py index d529c82bfa5..b174a312cd9 100644 --- a/tests/components/light/test_device_condition.py +++ b/tests/components/light/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -165,7 +165,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/light/test_device_trigger.py b/tests/components/light/test_device_trigger.py index 1c9f6cf1454..3217eb461b0 100644 --- a/tests/components/light/test_device_trigger.py +++ b/tests/components/light/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -178,7 +178,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index ef781b56a56..842fb305c6c 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -106,7 +106,7 @@ async def test_methods(hass): assert call.data[light.ATTR_TRANSITION] == "transition_val" -async def test_services(hass, mock_light_profiles): +async def test_services(hass, mock_light_profiles, enable_custom_integrations): """Test the provided services.""" platform = getattr(hass.components, "test.light") @@ -117,6 +117,16 @@ async def test_services(hass, mock_light_profiles): await hass.async_block_till_done() ent1, ent2, ent3 = platform.ENTITIES + ent1.supported_color_modes = [light.COLOR_MODE_HS] + ent3.supported_color_modes = [light.COLOR_MODE_HS] + ent1.supported_features = light.SUPPORT_TRANSITION + ent2.supported_features = ( + light.SUPPORT_COLOR + | light.SUPPORT_EFFECT + | light.SUPPORT_TRANSITION + | light.SUPPORT_WHITE_VALUE + ) + ent3.supported_features = light.SUPPORT_FLASH | light.SUPPORT_TRANSITION # Test init assert light.is_on(hass, ent1.entity_id) @@ -205,6 +215,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent2.entity_id, + light.ATTR_EFFECT: "fun_effect", light.ATTR_RGB_COLOR: (255, 255, 255), light.ATTR_WHITE_VALUE: 255, }, @@ -215,6 +226,7 @@ async def test_services(hass, mock_light_profiles): SERVICE_TURN_ON, { ATTR_ENTITY_ID: ent3.entity_id, + light.ATTR_FLASH: "short", light.ATTR_XY_COLOR: (0.4, 0.6), }, blocking=True, @@ -228,10 +240,14 @@ async def test_services(hass, mock_light_profiles): } _, data = ent2.last_call("turn_on") - assert data == {light.ATTR_HS_COLOR: (0, 0), light.ATTR_WHITE_VALUE: 255} + assert data == { + light.ATTR_EFFECT: "fun_effect", + light.ATTR_HS_COLOR: (0, 0), + light.ATTR_WHITE_VALUE: 255, + } _, data = ent3.last_call("turn_on") - assert data == {light.ATTR_HS_COLOR: (71.059, 100)} + assert data == {light.ATTR_FLASH: "short", light.ATTR_HS_COLOR: (71.059, 100)} # Ensure attributes are filtered when light is turned off await hass.services.async_call( @@ -491,7 +507,12 @@ async def test_services(hass, mock_light_profiles): ), ) async def test_light_profiles( - hass, mock_light_profiles, profile_name, expected_data, last_call + hass, + mock_light_profiles, + profile_name, + expected_data, + last_call, + enable_custom_integrations, ): """Test light profiles.""" platform = getattr(hass.components, "test.light") @@ -516,6 +537,8 @@ async def test_light_profiles( await hass.async_block_till_done() ent1, _, _ = platform.ENTITIES + ent1.supported_color_modes = [light.COLOR_MODE_HS] + ent1.supported_features = light.SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, @@ -535,7 +558,9 @@ async def test_light_profiles( assert data == expected_data -async def test_default_profiles_group(hass, mock_light_profiles): +async def test_default_profiles_group( + hass, mock_light_profiles, enable_custom_integrations +): """Test default turn-on light profile for all lights.""" platform = getattr(hass.components, "test.light") platform.init() @@ -549,6 +574,8 @@ async def test_default_profiles_group(hass, mock_light_profiles): mock_light_profiles[profile.name] = profile ent, _, _ = platform.ENTITIES + ent.supported_color_modes = [light.COLOR_MODE_HS] + ent.supported_features = light.SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ent.entity_id}, blocking=True ) @@ -562,7 +589,7 @@ async def test_default_profiles_group(hass, mock_light_profiles): @pytest.mark.parametrize( - "extra_call_params, expected_params", + "extra_call_params, expected_params_state_was_off, expected_params_state_was_on", ( ( {}, @@ -571,6 +598,11 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 3, }, + { + light.ATTR_HS_COLOR: (50.353, 100), + light.ATTR_BRIGHTNESS: 100, + light.ATTR_TRANSITION: 3, + }, ), ( {light.ATTR_BRIGHTNESS: 22}, @@ -579,6 +611,10 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 22, light.ATTR_TRANSITION: 3, }, + { + light.ATTR_BRIGHTNESS: 22, + light.ATTR_TRANSITION: 3, + }, ), ( {light.ATTR_TRANSITION: 22}, @@ -587,6 +623,9 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 100, light.ATTR_TRANSITION: 22, }, + { + light.ATTR_TRANSITION: 22, + }, ), ( { @@ -599,6 +638,11 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1, }, + { + light.ATTR_HS_COLOR: (38.88, 49.02), + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, ), ( {light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1}, @@ -607,11 +651,20 @@ async def test_default_profiles_group(hass, mock_light_profiles): light.ATTR_BRIGHTNESS: 11, light.ATTR_TRANSITION: 1, }, + { + light.ATTR_BRIGHTNESS: 11, + light.ATTR_TRANSITION: 1, + }, ), ), ) async def test_default_profiles_light( - hass, mock_light_profiles, extra_call_params, expected_params + hass, + mock_light_profiles, + extra_call_params, + enable_custom_integrations, + expected_params_state_was_off, + expected_params_state_was_on, ): """Test default turn-on light profile for a specific light.""" platform = getattr(hass.components, "test.light") @@ -628,6 +681,8 @@ async def test_default_profiles_light( mock_light_profiles[profile.name] = profile dev = next(filter(lambda x: x.entity_id == "light.ceiling_2", platform.ENTITIES)) + dev.supported_color_modes = [light.COLOR_MODE_HS] + dev.supported_features = light.SUPPORT_TRANSITION await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, @@ -639,14 +694,26 @@ async def test_default_profiles_light( ) _, data = dev.last_call("turn_on") - assert data == expected_params + assert data == expected_params_state_was_off await hass.services.async_call( light.DOMAIN, SERVICE_TURN_ON, { ATTR_ENTITY_ID: dev.entity_id, - light.ATTR_BRIGHTNESS: 0, + **extra_call_params, + }, + blocking=True, + ) + + _, data = dev.last_call("turn_on") + assert data == expected_params_state_was_on + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: dev.entity_id, }, blocking=True, ) @@ -657,7 +724,7 @@ async def test_default_profiles_light( } -async def test_light_context(hass, hass_admin_user): +async def test_light_context(hass, hass_admin_user, enable_custom_integrations): """Test that light context works.""" platform = getattr(hass.components, "test.light") platform.init() @@ -681,7 +748,7 @@ async def test_light_context(hass, hass_admin_user): assert state2.context.user_id == hass_admin_user.id -async def test_light_turn_on_auth(hass, hass_admin_user): +async def test_light_turn_on_auth(hass, hass_admin_user, enable_custom_integrations): """Test that light context works.""" platform = getattr(hass.components, "test.light") platform.init() @@ -703,7 +770,7 @@ async def test_light_turn_on_auth(hass, hass_admin_user): ) -async def test_light_brightness_step(hass): +async def test_light_brightness_step(hass, enable_custom_integrations): """Test that light context works.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -752,8 +819,20 @@ async def test_light_brightness_step(hass): _, data = entity1.last_call("turn_on") assert data["brightness"] == 66 # 40 + (255 * 0.10) + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": entity0.entity_id, + "brightness_step": -126, + }, + blocking=True, + ) -async def test_light_brightness_pct_conversion(hass): + assert entity0.state == "off" # 126 - 126; brightness is 0, light should turn off + + +async def test_light_brightness_pct_conversion(hass, enable_custom_integrations): """Test that light brightness percent conversion.""" platform = getattr(hass.components, "test.light") platform.init() @@ -918,7 +997,9 @@ invalid_no_brightness_no_color_no_transition,,, @pytest.mark.parametrize("light_state", (STATE_ON, STATE_OFF)) -async def test_light_backwards_compatibility_supported_color_modes(hass, light_state): +async def test_light_backwards_compatibility_supported_color_modes( + hass, light_state, enable_custom_integrations +): """Test supported_color_modes if not implemented by the entity.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1023,7 +1104,9 @@ async def test_light_backwards_compatibility_supported_color_modes(hass, light_s assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN -async def test_light_backwards_compatibility_color_mode(hass): +async def test_light_backwards_compatibility_color_mode( + hass, enable_custom_integrations +): """Test color_mode if not implemented by the entity.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1099,7 +1182,7 @@ async def test_light_backwards_compatibility_color_mode(hass): assert state.attributes["color_mode"] == light.COLOR_MODE_HS -async def test_light_service_call_rgbw(hass): +async def test_light_service_call_rgbw(hass, enable_custom_integrations): """Test backwards compatibility for rgbw functionality in service calls.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1144,7 +1227,7 @@ async def test_light_service_call_rgbw(hass): assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)} -async def test_light_state_rgbw(hass): +async def test_light_state_rgbw(hass, enable_custom_integrations): """Test rgbw color conversion in state updates.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1202,7 +1285,7 @@ async def test_light_state_rgbw(hass): } -async def test_light_state_rgbww(hass): +async def test_light_state_rgbww(hass, enable_custom_integrations): """Test rgbww color conversion in state updates.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1235,7 +1318,7 @@ async def test_light_state_rgbww(hass): } -async def test_light_service_call_color_conversion(hass): +async def test_light_service_call_color_conversion(hass, enable_custom_integrations): """Test color conversion in service calls.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1503,7 +1586,7 @@ async def test_light_service_call_color_conversion(hass): assert data == {"brightness": 128, "rgbww_color": (0, 75, 140, 255, 255)} -async def test_light_state_color_conversion(hass): +async def test_light_state_color_conversion(hass, enable_custom_integrations): """Test color conversion in state updates.""" platform = getattr(hass.components, "test.light") platform.init(empty=True) @@ -1564,3 +1647,157 @@ async def test_light_state_color_conversion(hass): assert state.attributes["hs_color"] == (240, 100) assert state.attributes["rgb_color"] == (0, 0, 255) assert state.attributes["xy_color"] == (0.136, 0.04) + + +async def test_services_filter_parameters( + hass, mock_light_profiles, enable_custom_integrations +): + """Test turn_on and turn_off filters unsupported parameters.""" + platform = getattr(hass.components, "test.light") + + platform.init() + assert await async_setup_component( + hass, light.DOMAIN, {light.DOMAIN: {CONF_PLATFORM: "test"}} + ) + await hass.async_block_till_done() + + ent1, _, _ = platform.ENTITIES + + # turn off the light by setting brightness to 0, this should work even if the light + # doesn't support brightness + await hass.services.async_call( + light.DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_MATCH_ALL}, blocking=True + ) + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: ENTITY_MATCH_ALL, light.ATTR_BRIGHTNESS: 0}, + blocking=True, + ) + + assert not light.is_on(hass, ent1.entity_id) + + # Ensure all unsupported attributes are filtered when light is turned on + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_EFFECT: "fun_effect", + light.ATTR_FLASH: "short", + light.ATTR_TRANSITION: 10, + light.ATTR_WHITE_VALUE: 0, + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_COLOR_TEMP: 153, + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_HS_COLOR: (0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGB_COLOR: (0, 0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGBW_COLOR: (0, 0, 0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_RGBWW_COLOR: (0, 0, 0, 0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_XY_COLOR: (0, 0), + }, + blocking=True, + ) + _, data = ent1.last_call("turn_on") + assert data == {} + + # Ensure all unsupported attributes are filtered when light is turned off + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_BRIGHTNESS: 0, + light.ATTR_EFFECT: "fun_effect", + light.ATTR_FLASH: "short", + light.ATTR_TRANSITION: 10, + light.ATTR_WHITE_VALUE: 0, + }, + blocking=True, + ) + + assert not light.is_on(hass, ent1.entity_id) + + _, data = ent1.last_call("turn_off") + assert data == {} + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + { + ATTR_ENTITY_ID: ent1.entity_id, + light.ATTR_FLASH: "short", + light.ATTR_TRANSITION: 10, + }, + blocking=True, + ) + + assert not light.is_on(hass, ent1.entity_id) + + _, data = ent1.last_call("turn_off") + assert data == {} diff --git a/tests/components/litterrobot/test_init.py b/tests/components/litterrobot/test_init.py index 22a6ea21022..d8ca690d965 100644 --- a/tests/components/litterrobot/test_init.py +++ b/tests/components/litterrobot/test_init.py @@ -10,10 +10,7 @@ from homeassistant.components.vacuum import ( SERVICE_START, STATE_DOCKED, ) -from homeassistant.config_entries import ( - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from .common import CONFIG, VACUUM_ENTITY_ID @@ -46,8 +43,8 @@ async def test_unload_entry(hass, mock_account): @pytest.mark.parametrize( "side_effect,expected_state", ( - (LitterRobotLoginException, ENTRY_STATE_SETUP_ERROR), - (LitterRobotException, ENTRY_STATE_SETUP_RETRY), + (LitterRobotLoginException, ConfigEntryState.SETUP_ERROR), + (LitterRobotException, ConfigEntryState.SETUP_RETRY), ), ) async def test_entry_not_setup(hass, side_effect, expected_state): @@ -63,4 +60,4 @@ async def test_entry_not_setup(hass, side_effect, expected_state): side_effect=side_effect, ): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == expected_state + assert entry.state is expected_state diff --git a/tests/components/litterrobot/test_vacuum.py b/tests/components/litterrobot/test_vacuum.py index 67c526e4a30..7db0ca5dde4 100644 --- a/tests/components/litterrobot/test_vacuum.py +++ b/tests/components/litterrobot/test_vacuum.py @@ -2,6 +2,7 @@ from datetime import timedelta import pytest +from voluptuous.error import MultipleInvalid from homeassistant.components.litterrobot import DOMAIN from homeassistant.components.litterrobot.entity import REFRESH_WAIT_TIME_SECONDS @@ -92,6 +93,11 @@ async def test_vacuum_with_error(hass: HomeAssistant, mock_account_with_error): "set_wait_time", {"minutes": 3}, ), + ( + SERVICE_SET_WAIT_TIME, + "set_wait_time", + {"minutes": "15"}, + ), ], ) async def test_commands(hass: HomeAssistant, mock_account, service, command, extra): @@ -117,21 +123,19 @@ async def test_commands(hass: HomeAssistant, mock_account, service, command, ext getattr(mock_account.robots[0], command).assert_called_once() -async def test_invalid_commands( - hass: HomeAssistant, caplog, mock_account_with_side_effects -): - """Test sending invalid commands to the vacuum.""" - await setup_integration(hass, mock_account_with_side_effects, PLATFORM_DOMAIN) +async def test_invalid_wait_time(hass: HomeAssistant, mock_account): + """Test an attempt to send an invalid wait time to the vacuum.""" + await setup_integration(hass, mock_account, PLATFORM_DOMAIN) vacuum = hass.states.get(VACUUM_ENTITY_ID) assert vacuum assert vacuum.state == STATE_DOCKED - await hass.services.async_call( - DOMAIN, - SERVICE_SET_WAIT_TIME, - {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, "minutes": 15}, - blocking=True, - ) - mock_account_with_side_effects.robots[0].set_wait_time.assert_called_once() - assert "Invalid command: oops" in caplog.text + with pytest.raises(MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_WAIT_TIME, + {ATTR_ENTITY_ID: VACUUM_ENTITY_ID, "minutes": 10}, + blocking=True, + ) + assert not mock_account.robots[0].set_wait_time.called diff --git a/tests/components/local_ip/test_init.py b/tests/components/local_ip/test_init.py index 3f5c4395f2d..a7ebfba28e2 100644 --- a/tests/components/local_ip/test_init.py +++ b/tests/components/local_ip/test_init.py @@ -1,21 +1,18 @@ """Tests for the local_ip component.""" -import pytest - from homeassistant.components.local_ip import DOMAIN -from homeassistant.setup import async_setup_component from homeassistant.util import get_local_ip - -@pytest.fixture(name="config") -def config_fixture(): - """Create hass config fixture.""" - return {DOMAIN: {}} +from tests.common import MockConfigEntry -async def test_basic_setup(hass, config): +async def test_basic_setup(hass): """Test component setup creates entry from config.""" - assert await async_setup_component(hass, DOMAIN, config) + entry = MockConfigEntry(domain=DOMAIN, data={}) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() + local_ip = await hass.async_add_executor_job(get_local_ip) state = hass.states.get(f"sensor.{DOMAIN}") assert state diff --git a/tests/components/lock/test_device_action.py b/tests/components/lock/test_device_action.py index 7d484ae96aa..a84555bdd42 100644 --- a/tests/components/lock/test_device_action.py +++ b/tests/components/lock/test_device_action.py @@ -30,7 +30,9 @@ def entity_reg(hass): return mock_registry(hass) -async def test_get_actions_support_open(hass, device_reg, entity_reg): +async def test_get_actions_support_open( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a lock which supports open.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -74,7 +76,9 @@ async def test_get_actions_support_open(hass, device_reg, entity_reg): assert_lists_same(actions, expected_actions) -async def test_get_actions_not_support_open(hass, device_reg, entity_reg): +async def test_get_actions_not_support_open( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected actions from a lock which doesn't support open.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() diff --git a/tests/components/logi_circle/test_config_flow.py b/tests/components/logi_circle/test_config_flow.py index 28335b93ad9..dbd35469d79 100644 --- a/tests/components/logi_circle/test_config_flow.py +++ b/tests/components/logi_circle/test_config_flow.py @@ -4,7 +4,7 @@ from unittest.mock import AsyncMock, Mock, patch import pytest -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.logi_circle import config_flow from homeassistant.components.logi_circle.config_flow import ( DOMAIN, @@ -13,7 +13,7 @@ from homeassistant.components.logi_circle.config_flow import ( ) from homeassistant.setup import async_setup_component -from tests.common import mock_coro +from tests.common import MockConfigEntry, mock_coro class MockRequest: @@ -121,24 +121,26 @@ async def test_abort_if_no_implementation_registered(hass): async def test_abort_if_already_setup(hass): """Test we abort if Logi Circle is already setup.""" flow = init_config_flow(hass) + MockConfigEntry(domain=config_flow.DOMAIN).add_to_hass(hass) - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_user() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_import() + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): + with pytest.raises(data_entry_flow.AbortFlow): result = await flow.async_step_code() - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - with patch.object(hass.config_entries, "async_entries", return_value=[{}]): - result = await flow.async_step_auth() + result = await flow.async_step_auth() assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "external_setup" diff --git a/tests/components/lutron_caseta/test_config_flow.py b/tests/components/lutron_caseta/test_config_flow.py index 14adefc37db..36e86778d0a 100644 --- a/tests/components/lutron_caseta/test_config_flow.py +++ b/tests/components/lutron_caseta/test_config_flow.py @@ -189,7 +189,7 @@ async def test_duplicate_bridge_import(hass): ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == CasetaConfigFlow.ABORT_REASON_ALREADY_CONFIGURED + assert result["reason"] == "already_configured" assert len(mock_setup_entry.mock_calls) == 0 diff --git a/tests/components/lyric/test_config_flow.py b/tests/components/lyric/test_config_flow.py index f5f41da08fd..c8197e9d678 100644 --- a/tests/components/lyric/test_config_flow.py +++ b/tests/components/lyric/test_config_flow.py @@ -109,7 +109,7 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/maxcube/__init__.py b/tests/components/maxcube/__init__.py new file mode 100644 index 00000000000..057484f59d6 --- /dev/null +++ b/tests/components/maxcube/__init__.py @@ -0,0 +1 @@ +"""maxcube tests.""" diff --git a/tests/components/mazda/__init__.py b/tests/components/mazda/__init__.py index f7a267a5110..7a81a9224d7 100644 --- a/tests/components/mazda/__init__.py +++ b/tests/components/mazda/__init__.py @@ -42,6 +42,15 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig ) client_mock.get_vehicles = AsyncMock(return_value=get_vehicles_fixture) client_mock.get_vehicle_status = AsyncMock(return_value=get_vehicle_status_fixture) + client_mock.lock_doors = AsyncMock() + client_mock.unlock_doors = AsyncMock() + client_mock.send_poi = AsyncMock() + client_mock.start_charging = AsyncMock() + client_mock.start_engine = AsyncMock() + client_mock.stop_charging = AsyncMock() + client_mock.stop_engine = AsyncMock() + client_mock.turn_off_hazard_lights = AsyncMock() + client_mock.turn_on_hazard_lights = AsyncMock() with patch( "homeassistant.components.mazda.config_flow.MazdaAPI", @@ -50,4 +59,4 @@ async def init_integration(hass: HomeAssistant, use_nickname=True) -> MockConfig assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - return config_entry + return client_mock diff --git a/tests/components/mazda/test_init.py b/tests/components/mazda/test_init.py index 1b062dd84f1..0280e8f34fa 100644 --- a/tests/components/mazda/test_init.py +++ b/tests/components/mazda/test_init.py @@ -4,16 +4,19 @@ import json from unittest.mock import patch from pymazda import MazdaAuthenticationException, MazdaException +import pytest +import voluptuous as vol -from homeassistant.components.mazda.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, +from homeassistant.components.mazda.const import DOMAIN, SERVICES +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_EMAIL, + CONF_PASSWORD, + CONF_REGION, + STATE_UNAVAILABLE, ) -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, CONF_REGION from homeassistant.core import HomeAssistant +from homeassistant.exceptions import HomeAssistantError from homeassistant.helpers import device_registry as dr from homeassistant.util import dt as dt_util @@ -39,7 +42,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_init_auth_failure(hass: HomeAssistant): @@ -56,7 +59,7 @@ async def test_init_auth_failure(hass: HomeAssistant): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -88,7 +91,7 @@ async def test_update_auth_failure(hass: HomeAssistant): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_LOADED + assert entries[0].state is ConfigEntryState.LOADED with patch( "homeassistant.components.mazda.MazdaAPI.get_vehicles", @@ -102,14 +105,57 @@ async def test_update_auth_failure(hass: HomeAssistant): assert flows[0]["step_id"] == "user" +async def test_update_general_failure(hass: HomeAssistant): + """Test general failure during data update.""" + get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) + get_vehicle_status_fixture = json.loads( + load_fixture("mazda/get_vehicle_status.json") + ) + + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + return_value=True, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + return_value=get_vehicles_fixture, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", + return_value=get_vehicle_status_fixture, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + with patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + side_effect=Exception("Unknown exception"), + ): + async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=61)) + await hass.async_block_till_done() + + entity = hass.states.get("sensor.my_mazda3_fuel_remaining_percentage") + assert entity is not None + assert entity.state == STATE_UNAVAILABLE + + async def test_unload_config_entry(hass: HomeAssistant) -> None: """Test the Mazda configuration entry unloading.""" - entry = await init_integration(hass) + await init_integration(hass) assert hass.data[DOMAIN] - await hass.config_entries.async_unload(entry.entry_id) + entries = hass.config_entries.async_entries(DOMAIN) + assert len(entries) == 1 + assert entries[0].state is ConfigEntryState.LOADED + + await hass.config_entries.async_unload(entries[0].entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED async def test_device_nickname(hass): @@ -138,3 +184,130 @@ async def test_device_no_nickname(hass): assert reg_device.model == "2021 MAZDA3 2.5 S SE AWD" assert reg_device.manufacturer == "Mazda" assert reg_device.name == "2021 MAZDA3 2.5 S SE AWD" + + +async def test_services(hass): + """Test service calls.""" + client_mock = await init_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + device_id = reg_device.id + + for service in SERVICES: + service_data = {"device_id": device_id} + if service == "send_poi": + service_data["latitude"] = 1.2345 + service_data["longitude"] = 2.3456 + service_data["poi_name"] = "Work" + + await hass.services.async_call(DOMAIN, service, service_data, blocking=True) + await hass.async_block_till_done() + + api_method = getattr(client_mock, service) + if service == "send_poi": + api_method.assert_called_once_with(12345, 1.2345, 2.3456, "Work") + else: + api_method.assert_called_once_with(12345) + + +async def test_service_invalid_device_id(hass): + """Test service call when the specified device ID is invalid.""" + await init_integration(hass) + + with pytest.raises(vol.error.MultipleInvalid) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": "invalid"}, blocking=True + ) + await hass.async_block_till_done() + + assert "Invalid device ID" in str(err.value) + + +async def test_service_device_id_not_mazda_vehicle(hass): + """Test service call when the specified device ID is not the device ID of a Mazda vehicle.""" + await init_integration(hass) + + device_registry = dr.async_get(hass) + # Create another device and pass its device ID. + # Service should fail because device is from wrong domain. + other_device = device_registry.async_get_or_create( + config_entry_id="test_config_entry_id", + identifiers={("OTHER_INTEGRATION", "ID_FROM_OTHER_INTEGRATION")}, + ) + + with pytest.raises(vol.error.MultipleInvalid) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": other_device.id}, blocking=True + ) + await hass.async_block_till_done() + + assert "Device ID is not a Mazda vehicle" in str(err.value) + + +async def test_service_vehicle_id_not_found(hass): + """Test service call when the vehicle ID is not found.""" + await init_integration(hass) + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + device_id = reg_device.id + + entries = hass.config_entries.async_entries(DOMAIN) + entry_id = entries[0].entry_id + + # Remove vehicle info from hass.data so that vehicle ID will not be found + hass.data[DOMAIN][entry_id]["vehicles"] = [] + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": device_id}, blocking=True + ) + await hass.async_block_till_done() + + assert str(err.value) == "Vehicle ID not found" + + +async def test_service_mazda_api_error(hass): + """Test the Mazda API raising an error when a service is called.""" + get_vehicles_fixture = json.loads(load_fixture("mazda/get_vehicles.json")) + get_vehicle_status_fixture = json.loads( + load_fixture("mazda/get_vehicle_status.json") + ) + + with patch( + "homeassistant.components.mazda.MazdaAPI.validate_credentials", + return_value=True, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicles", + return_value=get_vehicles_fixture, + ), patch( + "homeassistant.components.mazda.MazdaAPI.get_vehicle_status", + return_value=get_vehicle_status_fixture, + ): + config_entry = MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT) + config_entry.add_to_hass(hass) + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + device_registry = dr.async_get(hass) + reg_device = device_registry.async_get_device( + identifiers={(DOMAIN, "JM000000000000000")}, + ) + device_id = reg_device.id + + with patch( + "homeassistant.components.mazda.MazdaAPI.start_engine", + side_effect=MazdaException("Test error"), + ), pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + DOMAIN, "start_engine", {"device_id": device_id}, blocking=True + ) + await hass.async_block_till_done() + + assert str(err.value) == "Test error" diff --git a/tests/components/mazda/test_lock.py b/tests/components/mazda/test_lock.py new file mode 100644 index 00000000000..1230e624cdd --- /dev/null +++ b/tests/components/mazda/test_lock.py @@ -0,0 +1,58 @@ +"""The lock tests for the Mazda Connected Services integration.""" + +from homeassistant.components.lock import ( + DOMAIN as LOCK_DOMAIN, + SERVICE_LOCK, + SERVICE_UNLOCK, + STATE_LOCKED, +) +from homeassistant.const import ATTR_ENTITY_ID, ATTR_FRIENDLY_NAME +from homeassistant.helpers import entity_registry as er + +from tests.components.mazda import init_integration + + +async def test_lock_setup(hass): + """Test locking and unlocking the vehicle.""" + await init_integration(hass) + + entity_registry = er.async_get(hass) + entry = entity_registry.async_get("lock.my_mazda3_lock") + assert entry + assert entry.unique_id == "JM000000000000000" + + state = hass.states.get("lock.my_mazda3_lock") + assert state + assert state.attributes.get(ATTR_FRIENDLY_NAME) == "My Mazda3 Lock" + + assert state.state == STATE_LOCKED + + +async def test_locking(hass): + """Test locking the vehicle.""" + client_mock = await init_integration(hass) + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_LOCK, + {ATTR_ENTITY_ID: "lock.my_mazda3_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.lock_doors.assert_called_once() + + +async def test_unlocking(hass): + """Test unlocking the vehicle.""" + client_mock = await init_integration(hass) + + await hass.services.async_call( + LOCK_DOMAIN, + SERVICE_UNLOCK, + {ATTR_ENTITY_ID: "lock.my_mazda3_lock"}, + blocking=True, + ) + await hass.async_block_till_done() + + client_mock.unlock_doors.assert_called_once() diff --git a/tests/components/met/test_init.py b/tests/components/met/test_init.py index 074293249c8..64323af56ce 100644 --- a/tests/components/met/test_init.py +++ b/tests/components/met/test_init.py @@ -5,11 +5,7 @@ from homeassistant.components.met.const import ( DOMAIN, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, -) +from homeassistant.config_entries import ConfigEntryState from . import init_integration @@ -19,12 +15,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -41,7 +37,7 @@ async def test_fail_default_home_entry(hass, caplog): entry = await init_integration(hass, track_home=True) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR assert ( "Skip setting up met.no integration; No Home location has been set" diff --git a/tests/components/met_eireann/test_init.py b/tests/components/met_eireann/test_init.py index 8f95013cd72..c5d95ca14ca 100644 --- a/tests/components/met_eireann/test_init.py +++ b/tests/components/met_eireann/test_init.py @@ -1,6 +1,6 @@ """Test the Met Éireann integration init.""" from homeassistant.components.met_eireann.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from . import init_integration @@ -10,10 +10,10 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/meteoclimatic/__init__.py b/tests/components/meteoclimatic/__init__.py new file mode 100644 index 00000000000..29ba2ef18c3 --- /dev/null +++ b/tests/components/meteoclimatic/__init__.py @@ -0,0 +1 @@ +"""Tests for the Meteoclimatic component.""" diff --git a/tests/components/meteoclimatic/conftest.py b/tests/components/meteoclimatic/conftest.py new file mode 100644 index 00000000000..964f67d6473 --- /dev/null +++ b/tests/components/meteoclimatic/conftest.py @@ -0,0 +1,13 @@ +"""Meteoclimatic generic test utils.""" +from unittest.mock import patch + +import pytest + + +@pytest.fixture(autouse=True) +def patch_requests(): + """Stub out services that makes requests.""" + patch_client = patch("homeassistant.components.meteoclimatic.MeteoclimaticClient") + + with patch_client: + yield diff --git a/tests/components/meteoclimatic/test_config_flow.py b/tests/components/meteoclimatic/test_config_flow.py new file mode 100644 index 00000000000..e5daaea1978 --- /dev/null +++ b/tests/components/meteoclimatic/test_config_flow.py @@ -0,0 +1,88 @@ +"""Tests for the Meteoclimatic config flow.""" +from unittest.mock import patch + +from meteoclimatic.exceptions import MeteoclimaticError, StationNotFound +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.meteoclimatic.const import CONF_STATION_CODE, DOMAIN +from homeassistant.config_entries import SOURCE_USER + +TEST_STATION_CODE = "ESCAT4300000043206B" +TEST_STATION_NAME = "Reus (Tarragona)" + + +@pytest.fixture(name="client") +def mock_controller_client(): + """Mock a successful client.""" + with patch( + "homeassistant.components.meteoclimatic.config_flow.MeteoclimaticClient", + update=False, + ) as service_mock: + service_mock.return_value.get_data.return_value = { + "station_code": TEST_STATION_CODE + } + weather = service_mock.return_value.weather_at_station.return_value + weather.station.name = TEST_STATION_NAME + yield service_mock + + +@pytest.fixture(autouse=True) +def mock_setup(): + """Prevent setup.""" + with patch( + "homeassistant.components.meteoclimatic.async_setup_entry", + return_value=True, + ): + yield + + +async def test_user(hass, client): + """Test user config.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_STATION_CODE: TEST_STATION_CODE}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["result"].unique_id == TEST_STATION_CODE + assert result["title"] == TEST_STATION_NAME + assert result["data"][CONF_STATION_CODE] == TEST_STATION_CODE + + +async def test_not_found(hass): + """Test when we have the station code is not found.""" + with patch( + "homeassistant.components.meteoclimatic.config_flow.MeteoclimaticClient.weather_at_station", + side_effect=StationNotFound(TEST_STATION_CODE), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_STATION_CODE: TEST_STATION_CODE}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + assert result["errors"]["base"] == "not_found" + + +async def test_unknown_error(hass): + """Test when we have an unknown error fetching station data.""" + with patch( + "homeassistant.components.meteoclimatic.config_flow.MeteoclimaticClient.weather_at_station", + side_effect=MeteoclimaticError, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_STATION_CODE: TEST_STATION_CODE}, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" diff --git a/tests/components/mikrotik/test_hub.py b/tests/components/mikrotik/test_hub.py index 27c53786519..859c7d20d04 100644 --- a/tests/components/mikrotik/test_hub.py +++ b/tests/components/mikrotik/test_hub.py @@ -86,7 +86,7 @@ async def test_hub_setup_failed(hass): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert config_entry.state is config_entries.ConfigEntryState.SETUP_RETRY # error when username or password is invalid config_entry = MockConfigEntry(domain=mikrotik.DOMAIN, data=MOCK_DATA) diff --git a/tests/components/mobile_app/test_device_tracker.py b/tests/components/mobile_app/test_device_tracker.py index 164b90a5290..b755a0a8d09 100644 --- a/tests/components/mobile_app/test_device_tracker.py +++ b/tests/components/mobile_app/test_device_tracker.py @@ -48,6 +48,7 @@ async def test_sending_location(hass, create_registrations, webhook_client): "course": 6, "speed": 7, "vertical_accuracy": 8, + "location_name": "", }, }, ) @@ -82,7 +83,6 @@ async def test_restoring_location(hass, create_registrations, webhook_client): "course": 60, "speed": 70, "vertical_accuracy": 80, - "location_name": "bar", }, }, ) @@ -104,6 +104,7 @@ async def test_restoring_location(hass, create_registrations, webhook_client): assert state_1 is not state_2 assert state_2.name == "Test 1" + assert state_2.state == "not_home" assert state_2.attributes["source_type"] == "gps" assert state_2.attributes["latitude"] == 10 assert state_2.attributes["longitude"] == 20 diff --git a/tests/components/mobile_app/test_notify.py b/tests/components/mobile_app/test_notify.py index 5041b2453d9..9c4ca146898 100644 --- a/tests/components/mobile_app/test_notify.py +++ b/tests/components/mobile_app/test_notify.py @@ -1,5 +1,6 @@ """Notify platform tests for mobile_app.""" -# pylint: disable=redefined-outer-name +from datetime import datetime, timedelta + import pytest from homeassistant.components.mobile_app.const import DOMAIN @@ -9,12 +10,10 @@ from tests.common import MockConfigEntry @pytest.fixture -async def setup_push_receiver(hass, aioclient_mock): +async def setup_push_receiver(hass, aioclient_mock, hass_admin_user): """Fixture that sets up a mocked push receiver.""" push_url = "https://mobile-push.home-assistant.dev/push" - from datetime import datetime, timedelta - now = datetime.now() + timedelta(hours=24) iso_time = now.strftime("%Y-%m-%dT%H:%M:%SZ") @@ -34,7 +33,6 @@ async def setup_push_receiver(hass, aioclient_mock): ) entry = MockConfigEntry( - connection_class="cloud_push", data={ "app_data": {"push_token": "PUSH_TOKEN", "push_url": push_url}, "app_id": "io.homeassistant.mobile_app", @@ -48,8 +46,8 @@ async def setup_push_receiver(hass, aioclient_mock): "os_version": "5.0.6", "secret": "123abc", "supports_encryption": False, - "user_id": "1a2b3c", - "webhook_id": "webhook_id", + "user_id": hass_admin_user.id, + "webhook_id": "mock-webhook_id", }, domain=DOMAIN, source="registration", @@ -62,7 +60,6 @@ async def setup_push_receiver(hass, aioclient_mock): await hass.async_block_till_done() loaded_late_entry = MockConfigEntry( - connection_class="cloud_push", data={ "app_data": {"push_token": "PUSH_TOKEN2", "push_url": f"{push_url}2"}, "app_id": "io.homeassistant.mobile_app", @@ -120,3 +117,77 @@ async def test_notify_works(hass, aioclient_mock, setup_push_receiver): assert call_json["message"] == "Hello world" assert call_json["registration_info"]["app_id"] == "io.homeassistant.mobile_app" assert call_json["registration_info"]["app_version"] == "1.0" + + +async def test_notify_ws_works( + hass, aioclient_mock, setup_push_receiver, hass_ws_client +): + """Test notify works.""" + client = await hass_ws_client(hass) + + await client.send_json( + { + "id": 5, + "type": "mobile_app/push_notification_channel", + "webhook_id": "mock-webhook_id", + } + ) + + sub_result = await client.receive_json() + assert sub_result["success"] + + assert await hass.services.async_call( + "notify", "mobile_app_test", {"message": "Hello world"}, blocking=True + ) + + assert len(aioclient_mock.mock_calls) == 0 + + msg_result = await client.receive_json() + assert msg_result["event"] == {"message": "Hello world"} + + # Unsubscribe, now it should go over http + await client.send_json( + { + "id": 6, + "type": "unsubscribe_events", + "subscription": 5, + } + ) + sub_result = await client.receive_json() + assert sub_result["success"] + + assert await hass.services.async_call( + "notify", "mobile_app_test", {"message": "Hello world 2"}, blocking=True + ) + + assert len(aioclient_mock.mock_calls) == 1 + + # Test non-existing webhook ID + await client.send_json( + { + "id": 7, + "type": "mobile_app/push_notification_channel", + "webhook_id": "non-existing", + } + ) + sub_result = await client.receive_json() + assert not sub_result["success"] + assert sub_result["error"] == { + "code": "not_found", + "message": "Webhook ID not found", + } + + # Test webhook ID linked to other user + await client.send_json( + { + "id": 8, + "type": "mobile_app/push_notification_channel", + "webhook_id": "webhook_id_2", + } + ) + sub_result = await client.receive_json() + assert not sub_result["success"] + assert sub_result["error"] == { + "code": "unauthorized", + "message": "User not linked to this webhook ID", + } diff --git a/tests/components/modbus/conftest.py b/tests/components/modbus/conftest.py index cbfddb4488b..d3ae1286ef1 100644 --- a/tests/components/modbus/conftest.py +++ b/tests/components/modbus/conftest.py @@ -20,9 +20,43 @@ import homeassistant.util.dt as dt_util from tests.common import async_fire_time_changed +TEST_MODBUS_NAME = "modbusTest" _LOGGER = logging.getLogger(__name__) +@pytest.fixture +def mock_pymodbus(): + """Mock pymodbus.""" + mock_pb = mock.MagicMock() + with mock.patch( + "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb + ), mock.patch( + "homeassistant.components.modbus.modbus.ModbusSerialClient", + return_value=mock_pb, + ), mock.patch( + "homeassistant.components.modbus.modbus.ModbusUdpClient", return_value=mock_pb + ): + yield mock_pb + + +@pytest.fixture +async def mock_modbus(hass, mock_pymodbus): + """Load integration modbus using mocked pymodbus.""" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + } + ] + } + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + yield mock_pymodbus + + class ReadResult: """Storage class for register read results.""" @@ -167,3 +201,21 @@ async def base_config_test( config_modbus=config_modbus, expect_init_to_fail=expect_init_to_fail, ) + + +async def prepare_service_update(hass, config): + """Run test for service write_coil.""" + + config_modbus = { + DOMAIN: { + CONF_NAME: DEFAULT_HUB, + CONF_TYPE: "tcp", + CONF_HOST: "modbusTest", + CONF_PORT: 5001, + **config, + }, + } + assert await async_setup_component(hass, DOMAIN, config_modbus) + await hass.async_block_till_done() + assert await async_setup_component(hass, "homeassistant", {}) + await hass.async_block_till_done() diff --git a/tests/components/modbus/test_modbus_binary_sensor.py b/tests/components/modbus/test_binary_sensor.py similarity index 55% rename from tests/components/modbus/test_modbus_binary_sensor.py rename to tests/components/modbus/test_binary_sensor.py index 4ce423b2f16..5089d0271dd 100644 --- a/tests/components/modbus/test_modbus_binary_sensor.py +++ b/tests/components/modbus/test_binary_sensor.py @@ -18,8 +18,11 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.core import State -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache @pytest.mark.parametrize("do_discovery", [False, True]) @@ -99,3 +102,57 @@ async def test_all_binary_sensor(hass, do_type, regs, expected): scan_interval=5, ) assert state == expected + + +async def test_service_binary_sensor_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "binary_sensor.test" + config = { + CONF_BINARY_SENSORS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_COIL, + } + ] + } + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + + +async def test_restore_state_binary_sensor(hass): + """Run test for binary sensor restore state.""" + + sensor_name = "test_binary_sensor" + test_value = STATE_ON + config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),), + ) + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + CONF_BINARY_SENSORS, + None, + method_discovery=True, + ) + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + assert hass.states.get(entity_id).state == test_value diff --git a/tests/components/modbus/test_climate.py b/tests/components/modbus/test_climate.py new file mode 100644 index 00000000000..c73a73e47e8 --- /dev/null +++ b/tests/components/modbus/test_climate.py @@ -0,0 +1,143 @@ +"""The tests for the Modbus climate component.""" +import pytest + +from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN +from homeassistant.components.climate.const import HVAC_MODE_AUTO +from homeassistant.components.modbus.const import ( + CONF_CLIMATES, + CONF_CURRENT_TEMP, + CONF_DATA_COUNT, + CONF_TARGET_TEMP, +) +from homeassistant.const import ( + ATTR_TEMPERATURE, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, +) +from homeassistant.core import State + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache + + +@pytest.mark.parametrize( + "do_options", + [ + {}, + { + CONF_SCAN_INTERVAL: 20, + CONF_DATA_COUNT: 2, + }, + ], +) +async def test_config_climate(hass, do_options): + """Run test for climate.""" + device_name = "test_climate" + device_config = { + CONF_NAME: device_name, + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + CONF_SLAVE: 10, + **do_options, + } + await base_config_test( + hass, + device_config, + device_name, + CLIMATE_DOMAIN, + CONF_CLIMATES, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize( + "regs,expected", + [ + ( + [0x00], + "auto", + ), + ], +) +async def test_temperature_climate(hass, regs, expected): + """Run test for given config.""" + climate_name = "modbus_test_climate" + return + state = await base_test( + hass, + { + CONF_NAME: climate_name, + CONF_SLAVE: 1, + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + CONF_DATA_COUNT: 2, + }, + climate_name, + CLIMATE_DOMAIN, + CONF_CLIMATES, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +async def test_service_climate_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "climate.test" + config = { + CONF_CLIMATES: [ + { + CONF_NAME: "test", + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + CONF_SLAVE: 10, + } + ] + } + mock_pymodbus.read_input_registers.return_value = ReadResult([0x00]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "auto" + + +async def test_restore_state_climate(hass): + """Run test for sensor restore state.""" + + climate_name = "test_climate" + test_temp = 37 + entity_id = f"{CLIMATE_DOMAIN}.{climate_name}" + test_value = State(entity_id, 35) + test_value.attributes = {ATTR_TEMPERATURE: test_temp} + config_sensor = { + CONF_NAME: climate_name, + CONF_TARGET_TEMP: 117, + CONF_CURRENT_TEMP: 117, + } + mock_restore_cache( + hass, + (test_value,), + ) + await base_config_test( + hass, + config_sensor, + climate_name, + CLIMATE_DOMAIN, + CONF_CLIMATES, + None, + method_discovery=True, + ) + state = hass.states.get(entity_id) + assert state.state == HVAC_MODE_AUTO + assert state.attributes[ATTR_TEMPERATURE] == test_temp diff --git a/tests/components/modbus/test_cover.py b/tests/components/modbus/test_cover.py new file mode 100644 index 00000000000..8fbb45fde8e --- /dev/null +++ b/tests/components/modbus/test_cover.py @@ -0,0 +1,297 @@ +"""The tests for the Modbus cover component.""" +import logging + +from pymodbus.exceptions import ModbusException +import pytest + +from homeassistant.components.cover import DOMAIN as COVER_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_REGISTER_HOLDING, + CONF_REGISTER, + CONF_STATE_CLOSED, + CONF_STATE_CLOSING, + CONF_STATE_OPEN, + CONF_STATE_OPENING, + CONF_STATUS_REGISTER, + CONF_STATUS_REGISTER_TYPE, +) +from homeassistant.const import ( + CONF_COVERS, + CONF_NAME, + CONF_SCAN_INTERVAL, + CONF_SLAVE, + STATE_CLOSED, + STATE_CLOSING, + STATE_OPEN, + STATE_OPENING, + STATE_UNAVAILABLE, +) +from homeassistant.core import State + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache + + +@pytest.mark.parametrize( + "do_options", + [ + {}, + { + CONF_SLAVE: 10, + CONF_SCAN_INTERVAL: 20, + }, + ], +) +@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) +async def test_config_cover(hass, do_options, read_type): + """Run test for cover.""" + device_name = "test_cover" + device_config = { + CONF_NAME: device_name, + read_type: 1234, + **do_options, + } + await base_config_test( + hass, + device_config, + device_name, + COVER_DOMAIN, + CONF_COVERS, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize( + "regs,expected", + [ + ( + [0x00], + STATE_CLOSED, + ), + ( + [0x80], + STATE_CLOSED, + ), + ( + [0xFE], + STATE_CLOSED, + ), + ( + [0xFF], + STATE_OPEN, + ), + ( + [0x01], + STATE_OPEN, + ), + ], +) +async def test_coil_cover(hass, regs, expected): + """Run test for given config.""" + cover_name = "modbus_test_cover" + state = await base_test( + hass, + { + CONF_NAME: cover_name, + CALL_TYPE_COIL: 1234, + CONF_SLAVE: 1, + }, + cover_name, + COVER_DOMAIN, + CONF_COVERS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +@pytest.mark.parametrize( + "regs,expected", + [ + ( + [0x00], + STATE_CLOSED, + ), + ( + [0x80], + STATE_OPEN, + ), + ( + [0xFE], + STATE_OPEN, + ), + ( + [0xFF], + STATE_OPEN, + ), + ( + [0x01], + STATE_OPEN, + ), + ], +) +async def test_register_cover(hass, regs, expected): + """Run test for given config.""" + cover_name = "modbus_test_cover" + state = await base_test( + hass, + { + CONF_NAME: cover_name, + CONF_REGISTER: 1234, + CONF_SLAVE: 1, + }, + cover_name, + COVER_DOMAIN, + CONF_COVERS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) +async def test_unsupported_config_cover(hass, read_type, caplog): + """ + Run test for cover. + + Initialize the Cover in the legacy manner via platform. + This test expects that the Cover won't be initialized, and that we get a config warning. + """ + device_name = "test_cover" + device_config = {CONF_NAME: device_name, read_type: 1234} + + caplog.set_level(logging.WARNING) + caplog.clear() + + await base_config_test( + hass, + device_config, + device_name, + COVER_DOMAIN, + CONF_COVERS, + None, + method_discovery=False, + expect_init_to_fail=True, + ) + + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "WARNING" + + +async def test_service_cover_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "cover.test" + config = { + CONF_COVERS: [ + { + CONF_NAME: "test", + CONF_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_CLOSED + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OPEN + + +@pytest.mark.parametrize( + "state", [STATE_CLOSED, STATE_CLOSING, STATE_OPENING, STATE_OPEN] +) +async def test_restore_state_cover(hass, state): + """Run test for cover restore state.""" + + entity_id = "cover.test" + cover_name = "test" + config = { + CONF_NAME: cover_name, + CALL_TYPE_COIL: 1234, + CONF_STATE_OPEN: 1, + CONF_STATE_CLOSED: 0, + CONF_STATE_OPENING: 2, + CONF_STATE_CLOSING: 3, + CONF_STATUS_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + } + mock_restore_cache( + hass, + (State(f"{entity_id}", state),), + ) + await base_config_test( + hass, + config, + cover_name, + COVER_DOMAIN, + CONF_COVERS, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == state + + +async def test_service_cover_move(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "cover.test" + entity_id2 = "cover.test2" + config = { + CONF_COVERS: [ + { + CONF_NAME: "test", + CONF_REGISTER: 1234, + CONF_STATUS_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "test2", + CALL_TYPE_COIL: 1234, + }, + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "cover", "open_cover", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OPEN + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_CLOSED + + mock_pymodbus.read_holding_registers.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + mock_pymodbus.read_coils.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "cover", "close_cover", {"entity_id": entity_id2}, blocking=True + ) + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE diff --git a/tests/components/modbus/test_fan.py b/tests/components/modbus/test_fan.py new file mode 100644 index 00000000000..2a9414d2277 --- /dev/null +++ b/tests/components/modbus/test_fan.py @@ -0,0 +1,289 @@ +"""The tests for the Modbus fan component.""" +from pymodbus.exceptions import ModbusException +import pytest + +from homeassistant.components.fan import DOMAIN as FAN_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_FANS, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SLAVE, + CONF_TYPE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 1234, + }, + { + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: None, + }, + ], +) +async def test_config_fan(hass, do_config): + """Run test for fan.""" + device_name = "test_fan" + + device_config = { + CONF_NAME: device_name, + **do_config, + } + + await base_config_test( + hass, + device_config, + device_name, + FAN_DOMAIN, + CONF_FANS, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) +@pytest.mark.parametrize( + "regs,verify,expected", + [ + ( + [0x00], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + [0x01], + {CONF_VERIFY: {}}, + STATE_ON, + ), + ( + [0xFE], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + None, + {CONF_VERIFY: {}}, + STATE_UNAVAILABLE, + ), + ( + None, + {}, + STATE_OFF, + ), + ], +) +async def test_all_fan(hass, call_type, regs, verify, expected): + """Run test for given config.""" + fan_name = "modbus_test_fan" + state = await base_test( + hass, + { + CONF_NAME: fan_name, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_WRITE_TYPE: call_type, + **verify, + }, + fan_name, + FAN_DOMAIN, + CONF_FANS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +async def test_restore_state_fan(hass): + """Run test for fan restore state.""" + + fan_name = "test_fan" + entity_id = f"{FAN_DOMAIN}.{fan_name}" + test_value = STATE_ON + config_fan = {CONF_NAME: fan_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{entity_id}", test_value),), + ) + await base_config_test( + hass, + config_fan, + fan_name, + FAN_DOMAIN, + CONF_FANS, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == test_value + + +async def test_fan_service_turn(hass, caplog, mock_pymodbus): + """Run test for service turn_on/turn_off.""" + + entity_id1 = f"{FAN_DOMAIN}.fan1" + entity_id2 = f"{FAN_DOMAIN}.fan2" + config = { + MODBUS_DOMAIN: { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_FANS: [ + { + CONF_NAME: "fan1", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "fan2", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_VERIFY: {}, + }, + ], + }, + } + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + assert MODBUS_DOMAIN in hass.config.components + + assert hass.states.get(entity_id1).state == STATE_OFF + await hass.services.async_call( + "fan", "turn_on", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_ON + await hass.services.async_call( + "fan", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_OFF + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + assert hass.states.get(entity_id2).state == STATE_OFF + await hass.services.async_call( + "fan", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_ON + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "fan", "turn_off", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_OFF + + mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "fan", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "fan", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + + +async def test_service_fan_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "fan.test" + config = { + CONF_FANS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + } + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/modbus/test_init.py b/tests/components/modbus/test_init.py index 2da3a753505..0819e5a3e89 100644 --- a/tests/components/modbus/test_init.py +++ b/tests/components/modbus/test_init.py @@ -1,11 +1,24 @@ -"""The tests for the Modbus init.""" +"""The tests for the Modbus init. + +This file is responsible for testing: +- pymodbus API +- Functionality of class ModbusHub +- Coverage 100%: + __init__.py + base_platform.py + const.py + modbus.py +""" +from datetime import timedelta import logging from unittest import mock from pymodbus.exceptions import ModbusException +from pymodbus.pdu import ExceptionResponse, IllegalFunctionRequest import pytest import voluptuous as vol +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN from homeassistant.components.modbus import number from homeassistant.components.modbus.const import ( ATTR_ADDRESS, @@ -13,29 +26,56 @@ from homeassistant.components.modbus.const import ( ATTR_STATE, ATTR_UNIT, ATTR_VALUE, + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CALL_TYPE_WRITE_COIL, + CALL_TYPE_WRITE_COILS, + CALL_TYPE_WRITE_REGISTER, + CALL_TYPE_WRITE_REGISTERS, CONF_BAUDRATE, CONF_BYTESIZE, + CONF_INPUT_TYPE, CONF_PARITY, CONF_STOPBITS, + DEFAULT_SCAN_INTERVAL, MODBUS_DOMAIN as DOMAIN, SERVICE_WRITE_COIL, SERVICE_WRITE_REGISTER, ) +from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.const import ( + CONF_ADDRESS, + CONF_BINARY_SENSORS, CONF_DELAY, CONF_HOST, CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SENSORS, CONF_TIMEOUT, CONF_TYPE, + STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from .conftest import TEST_MODBUS_NAME, ReadResult + +from tests.common import async_fire_time_changed + +TEST_SENSOR_NAME = "testSensor" +TEST_ENTITY_ID = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" +TEST_HOST = "modbusTestHost" -@pytest.mark.parametrize( - "value,value_type", - [ +async def test_number_validator(): + """Test number validator.""" + + for value, value_type in [ (15, int), (15.1, float), ("15", int), @@ -44,80 +84,55 @@ from homeassistant.setup import async_setup_component (-15.1, float), ("-15", int), ("-15.1", float), - ], -) -async def test_number_validator(value, value_type): - """Test number validator.""" - - assert isinstance(number(value), value_type) - - -async def test_number_exception(): - """Test number exception.""" + ]: + assert isinstance(number(value), value_type) try: number("x15.1") except (vol.Invalid): return - pytest.fail("Number not throwing exception") -async def _config_helper(hass, do_config): - """Run test for modbus.""" - - config = {DOMAIN: do_config} - - with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient" - ), mock.patch( - "homeassistant.components.modbus.modbus.ModbusSerialClient" - ), mock.patch( - "homeassistant.components.modbus.modbus.ModbusUdpClient" - ): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - - @pytest.mark.parametrize( "do_config", [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, }, { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, - CONF_NAME: "modbusTest", + CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { CONF_TYPE: "udp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, }, { CONF_TYPE: "udp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, - CONF_NAME: "modbusTest", + CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, { CONF_TYPE: "rtuovertcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, }, { CONF_TYPE: "rtuovertcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, - CONF_NAME: "modbusTest", + CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, @@ -138,153 +153,223 @@ async def _config_helper(hass, do_config): CONF_PORT: "usb01", CONF_PARITY: "E", CONF_STOPBITS: 1, - CONF_NAME: "modbusTest", + CONF_NAME: TEST_MODBUS_NAME, CONF_TIMEOUT: 30, CONF_DELAY: 10, }, + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_DELAY: 5, + }, + [ + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + }, + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME + "2", + }, + { + CONF_TYPE: "serial", + CONF_BAUDRATE: 9600, + CONF_BYTESIZE: 8, + CONF_METHOD: "rtu", + CONF_PORT: "usb01", + CONF_PARITY: "E", + CONF_STOPBITS: 1, + CONF_NAME: TEST_MODBUS_NAME + "3", + }, + ], + ], +) +async def test_config_modbus(hass, caplog, do_config, mock_pymodbus): + """Run configuration test for modbus.""" + config = {DOMAIN: do_config} + caplog.set_level(logging.ERROR) + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + assert DOMAIN in hass.config.components + assert len(caplog.records) == 0 + + +VALUE = "value" +FUNC = "func" +DATA = "data" +SERVICE = "service" + + +@pytest.mark.parametrize( + "do_write", + [ + { + DATA: ATTR_VALUE, + VALUE: 15, + SERVICE: SERVICE_WRITE_REGISTER, + FUNC: CALL_TYPE_WRITE_REGISTER, + }, + { + DATA: ATTR_VALUE, + VALUE: [1, 2, 3], + SERVICE: SERVICE_WRITE_REGISTER, + FUNC: CALL_TYPE_WRITE_REGISTERS, + }, + { + DATA: ATTR_STATE, + VALUE: False, + SERVICE: SERVICE_WRITE_COIL, + FUNC: CALL_TYPE_WRITE_COIL, + }, + { + DATA: ATTR_STATE, + VALUE: [True, False, True], + SERVICE: SERVICE_WRITE_COIL, + FUNC: CALL_TYPE_WRITE_COILS, + }, ], ) -async def test_config_modbus(hass, caplog, do_config): - """Run test for modbus.""" - - caplog.set_level(logging.ERROR) - await _config_helper(hass, do_config) - assert DOMAIN in hass.config.components - assert len(caplog.records) == 0 - - -async def test_config_multiple_modbus(hass, caplog): - """Run test for multiple modbus.""" - - do_config = [ - { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: "modbusTest1", - }, - { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: "modbusTest2", - }, - { - CONF_TYPE: "serial", - CONF_BAUDRATE: 9600, - CONF_BYTESIZE: 8, - CONF_METHOD: "rtu", - CONF_PORT: "usb01", - CONF_PARITY: "E", - CONF_STOPBITS: 1, - CONF_NAME: "modbusTest3", - }, - ] - - caplog.set_level(logging.ERROR) - await _config_helper(hass, do_config) - assert DOMAIN in hass.config.components - assert len(caplog.records) == 0 - - -async def test_pb_service_write_register(hass): +async def test_pb_service_write(hass, do_write, caplog, mock_modbus): """Run test for service write_register.""" - conf_name = "myModbus" - config = { - DOMAIN: [ - { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: conf_name, - } - ] + func_name = { + CALL_TYPE_WRITE_COIL: mock_modbus.write_coil, + CALL_TYPE_WRITE_COILS: mock_modbus.write_coils, + CALL_TYPE_WRITE_REGISTER: mock_modbus.write_register, + CALL_TYPE_WRITE_REGISTERS: mock_modbus.write_registers, } - mock_pb = mock.MagicMock() - with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb - ): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - - data = {ATTR_HUB: conf_name, ATTR_UNIT: 17, ATTR_ADDRESS: 16, ATTR_VALUE: 15} - await hass.services.async_call( - DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True - ) - assert mock_pb.write_register.called - assert mock_pb.write_register.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_VALUE], - ) - mock_pb.write_register.side_effect = ModbusException("fail write_") - await hass.services.async_call( - DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True - ) - - data[ATTR_VALUE] = [1, 2, 3] - await hass.services.async_call( - DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True - ) - assert mock_pb.write_registers.called - assert mock_pb.write_registers.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_VALUE], - ) - mock_pb.write_registers.side_effect = ModbusException("fail write_") - await hass.services.async_call( - DOMAIN, SERVICE_WRITE_REGISTER, data, blocking=True - ) - - -async def test_pb_service_write_coil(hass, caplog): - """Run test for service write_coil.""" - - conf_name = "myModbus" - config = { - DOMAIN: [ - { - CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", - CONF_PORT: 5501, - CONF_NAME: conf_name, - } - ] + data = { + ATTR_HUB: TEST_MODBUS_NAME, + ATTR_UNIT: 17, + ATTR_ADDRESS: 16, + do_write[DATA]: do_write[VALUE], } + await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) + assert func_name[do_write[FUNC]].called + assert func_name[do_write[FUNC]].call_args[0] == ( + data[ATTR_ADDRESS], + data[do_write[DATA]], + ) + mock_modbus.reset_mock() - mock_pb = mock.MagicMock() - with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb - ): - assert await async_setup_component(hass, DOMAIN, config) is True - await hass.async_block_till_done() - - data = {ATTR_HUB: conf_name, ATTR_UNIT: 17, ATTR_ADDRESS: 16, ATTR_STATE: False} - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_pb.write_coil.called - assert mock_pb.write_coil.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_STATE], - ) - mock_pb.write_coil.side_effect = ModbusException("fail write_") - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - - data[ATTR_STATE] = [True, False, True] - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert mock_pb.write_coils.called - assert mock_pb.write_coils.call_args[0] == ( - data[ATTR_ADDRESS], - data[ATTR_STATE], - ) - + for return_value in [ + ExceptionResponse(0x06), + IllegalFunctionRequest(0x06), + ModbusException("fail write_"), + ]: caplog.set_level(logging.DEBUG) - caplog.clear - mock_pb.write_coils.side_effect = ModbusException("fail write_") - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert caplog.records[-1].levelname == "ERROR" - await hass.services.async_call(DOMAIN, SERVICE_WRITE_COIL, data, blocking=True) - assert caplog.records[-1].levelname == "DEBUG" + func_name[do_write[FUNC]].return_value = return_value + await hass.services.async_call(DOMAIN, do_write[SERVICE], data, blocking=True) + assert func_name[do_write[FUNC]].called + assert caplog.messages[-1].startswith("Pymodbus:") + mock_modbus.reset_mock() + + +async def _read_helper(hass, do_group, do_type, do_return, do_exception, mock_pymodbus): + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + do_group: [ + { + CONF_INPUT_TYPE: do_type, + CONF_NAME: TEST_SENSOR_NAME, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 1, + } + ], + } + ] + } + mock_pymodbus.read_coils.side_effect = do_exception + mock_pymodbus.read_discrete_inputs.side_effect = do_exception + mock_pymodbus.read_input_registers.side_effect = do_exception + mock_pymodbus.read_holding_registers.side_effect = do_exception + mock_pymodbus.read_coils.return_value = do_return + mock_pymodbus.read_discrete_inputs.return_value = do_return + mock_pymodbus.read_input_registers.return_value = do_return + mock_pymodbus.read_holding_registers.return_value = do_return + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + now = now + timedelta(seconds=DEFAULT_SCAN_INTERVAL + 60) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + + +@pytest.mark.parametrize( + "do_return,do_exception,do_expect", + [ + [ReadResult([7]), None, "7"], + [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE], + [ExceptionResponse(0x99), None, STATE_UNAVAILABLE], + [ReadResult([7]), ModbusException("fail read_"), STATE_UNAVAILABLE], + ], +) +@pytest.mark.parametrize( + "do_type", + [CALL_TYPE_REGISTER_HOLDING, CALL_TYPE_REGISTER_INPUT], +) +async def test_pb_read_value( + hass, caplog, do_type, do_return, do_exception, do_expect, mock_pymodbus +): + """Run test for different read.""" + + # the purpose of this test is to test the special + # return values from pymodbus: + # ExceptionResponse, IllegalResponse + # and exceptions. + # We "hijiack" binary_sensor and sensor in order + # to make a proper blackbox test. + await _read_helper( + hass, CONF_SENSORS, do_type, do_return, do_exception, mock_pymodbus + ) + + # Check state + entity_id = f"{SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + assert hass.states.get(entity_id).state + + +@pytest.mark.parametrize( + "do_return,do_exception,do_expect", + [ + [ReadResult([0x01]), None, STATE_ON], + [IllegalFunctionRequest(0x99), None, STATE_UNAVAILABLE], + [ExceptionResponse(0x99), None, STATE_UNAVAILABLE], + [ReadResult([7]), ModbusException("fail read_"), STATE_UNAVAILABLE], + ], +) +@pytest.mark.parametrize("do_type", [CALL_TYPE_DISCRETE, CALL_TYPE_COIL]) +async def test_pb_read_state( + hass, caplog, do_type, do_return, do_exception, do_expect, mock_pymodbus +): + """Run test for different read.""" + + # the purpose of this test is to test the special + # return values from pymodbus: + # ExceptionResponse, IllegalResponse + # and exceptions. + # We "hijiack" binary_sensor and sensor in order + # to make a proper blackbox test. + await _read_helper( + hass, CONF_BINARY_SENSORS, do_type, do_return, do_exception, mock_pymodbus + ) + + # Check state + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + state = hass.states.get(entity_id).state + assert state == do_expect async def test_pymodbus_constructor_fail(hass, caplog): @@ -293,7 +378,7 @@ async def test_pymodbus_constructor_fail(hass, caplog): DOMAIN: [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, } ] @@ -310,25 +395,77 @@ async def test_pymodbus_constructor_fail(hass, caplog): assert mock_pb.called -async def test_pymodbus_connect_fail(hass, caplog): +async def test_pymodbus_connect_fail(hass, caplog, mock_pymodbus): """Run test for failing pymodbus constructor.""" config = { DOMAIN: [ { CONF_TYPE: "tcp", - CONF_HOST: "modbusTestHost", + CONF_HOST: TEST_HOST, CONF_PORT: 5501, } ] } - mock_pb = mock.MagicMock() - with mock.patch( - "homeassistant.components.modbus.modbus.ModbusTcpClient", return_value=mock_pb - ): - caplog.set_level(logging.ERROR) - mock_pb.connect.side_effect = ModbusException("test connect fail") - mock_pb.close.side_effect = ModbusException("test connect fail") + caplog.set_level(logging.ERROR) + mock_pymodbus.connect.side_effect = ModbusException("test connect fail") + mock_pymodbus.close.side_effect = ModbusException("test connect fail") + assert await async_setup_component(hass, DOMAIN, config) is True + await hass.async_block_till_done() + assert len(caplog.records) == 1 + assert caplog.records[0].levelname == "ERROR" + + +async def test_delay(hass, mock_pymodbus): + """Run test for startup delay.""" + + # the purpose of this test is to test startup delay + # We "hijiack" a binary_sensor to make a proper blackbox test. + test_delay = 15 + test_scan_interval = 5 + entity_id = f"{BINARY_SENSOR_DOMAIN}.{TEST_SENSOR_NAME}" + config = { + DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: TEST_HOST, + CONF_PORT: 5501, + CONF_NAME: TEST_MODBUS_NAME, + CONF_DELAY: test_delay, + CONF_BINARY_SENSORS: [ + { + CONF_INPUT_TYPE: CALL_TYPE_COIL, + CONF_NAME: f"{TEST_SENSOR_NAME}", + CONF_ADDRESS: 52, + CONF_SCAN_INTERVAL: test_scan_interval, + }, + ], + } + ] + } + mock_pymodbus.read_coils.return_value = ReadResult([0x01]) + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): assert await async_setup_component(hass, DOMAIN, config) is True await hass.async_block_till_done() - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "ERROR" + + # pass first scan_interval + start_time = now + now = now + timedelta(seconds=(test_scan_interval + 1)) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + + stop_time = start_time + timedelta(seconds=(test_delay + 1)) + step_timedelta = timedelta(seconds=1) + while now < stop_time: + now = now + step_timedelta + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_UNAVAILABLE + now = now + step_timedelta + timedelta(seconds=2) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/components/modbus/test_light.py b/tests/components/modbus/test_light.py new file mode 100644 index 00000000000..12e72e54155 --- /dev/null +++ b/tests/components/modbus/test_light.py @@ -0,0 +1,289 @@ +"""The tests for the Modbus light component.""" +from pymodbus.exceptions import ModbusException +import pytest + +from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_HOST, + CONF_LIGHTS, + CONF_NAME, + CONF_PORT, + CONF_SLAVE, + CONF_TYPE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import State +from homeassistant.setup import async_setup_component + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 1234, + }, + { + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_VERIFY: None, + }, + ], +) +async def test_config_light(hass, do_config): + """Run test for light.""" + device_name = "test_light" + + device_config = { + CONF_NAME: device_name, + **do_config, + } + + await base_config_test( + hass, + device_config, + device_name, + LIGHT_DOMAIN, + CONF_LIGHTS, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) +@pytest.mark.parametrize( + "regs,verify,expected", + [ + ( + [0x00], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + [0x01], + {CONF_VERIFY: {}}, + STATE_ON, + ), + ( + [0xFE], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + None, + {CONF_VERIFY: {}}, + STATE_UNAVAILABLE, + ), + ( + None, + {}, + STATE_OFF, + ), + ], +) +async def test_all_light(hass, call_type, regs, verify, expected): + """Run test for given config.""" + light_name = "modbus_test_light" + state = await base_test( + hass, + { + CONF_NAME: light_name, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_WRITE_TYPE: call_type, + **verify, + }, + light_name, + LIGHT_DOMAIN, + CONF_LIGHTS, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +async def test_restore_state_light(hass): + """Run test for sensor restore state.""" + + light_name = "test_light" + entity_id = f"{LIGHT_DOMAIN}.{light_name}" + test_value = STATE_ON + config_light = {CONF_NAME: light_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{entity_id}", test_value),), + ) + await base_config_test( + hass, + config_light, + light_name, + LIGHT_DOMAIN, + CONF_LIGHTS, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == test_value + + +async def test_light_service_turn(hass, caplog, mock_pymodbus): + """Run test for service turn_on/turn_off.""" + + entity_id1 = f"{LIGHT_DOMAIN}.light1" + entity_id2 = f"{LIGHT_DOMAIN}.light2" + config = { + MODBUS_DOMAIN: { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_LIGHTS: [ + { + CONF_NAME: "light1", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "light2", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_VERIFY: {}, + }, + ], + }, + } + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + assert MODBUS_DOMAIN in hass.config.components + + assert hass.states.get(entity_id1).state == STATE_OFF + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_ON + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_OFF + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + assert hass.states.get(entity_id2).state == STATE_OFF + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_ON + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_OFF + + mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "light", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "light", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + + +async def test_service_light_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "light.test" + config = { + CONF_LIGHTS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + } + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF diff --git a/tests/components/modbus/test_modbus_climate.py b/tests/components/modbus/test_modbus_climate.py deleted file mode 100644 index f932817b12e..00000000000 --- a/tests/components/modbus/test_modbus_climate.py +++ /dev/null @@ -1,78 +0,0 @@ -"""The tests for the Modbus climate component.""" -import pytest - -from homeassistant.components.climate import DOMAIN as CLIMATE_DOMAIN -from homeassistant.components.modbus.const import ( - CONF_CLIMATES, - CONF_CURRENT_TEMP, - CONF_DATA_COUNT, - CONF_TARGET_TEMP, -) -from homeassistant.const import CONF_NAME, CONF_SCAN_INTERVAL, CONF_SLAVE - -from .conftest import base_config_test, base_test - - -@pytest.mark.parametrize( - "do_options", - [ - {}, - { - CONF_SCAN_INTERVAL: 20, - CONF_DATA_COUNT: 2, - }, - ], -) -async def test_config_climate(hass, do_options): - """Run test for climate.""" - device_name = "test_climate" - device_config = { - CONF_NAME: device_name, - CONF_TARGET_TEMP: 117, - CONF_CURRENT_TEMP: 117, - CONF_SLAVE: 10, - **do_options, - } - await base_config_test( - hass, - device_config, - device_name, - CLIMATE_DOMAIN, - CONF_CLIMATES, - None, - method_discovery=True, - ) - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x00], - "auto", - ), - ], -) -async def test_temperature_climate(hass, regs, expected): - """Run test for given config.""" - climate_name = "modbus_test_climate" - return - state = await base_test( - hass, - { - CONF_NAME: climate_name, - CONF_SLAVE: 1, - CONF_TARGET_TEMP: 117, - CONF_CURRENT_TEMP: 117, - CONF_DATA_COUNT: 2, - }, - climate_name, - CLIMATE_DOMAIN, - CONF_CLIMATES, - None, - regs, - expected, - method_discovery=True, - scan_interval=5, - ) - assert state == expected diff --git a/tests/components/modbus/test_modbus_cover.py b/tests/components/modbus/test_modbus_cover.py deleted file mode 100644 index eddaa6099d7..00000000000 --- a/tests/components/modbus/test_modbus_cover.py +++ /dev/null @@ -1,170 +0,0 @@ -"""The tests for the Modbus cover component.""" -import logging - -import pytest - -from homeassistant.components.cover import DOMAIN as COVER_DOMAIN -from homeassistant.components.modbus.const import CALL_TYPE_COIL, CONF_REGISTER -from homeassistant.const import ( - CONF_COVERS, - CONF_NAME, - CONF_SCAN_INTERVAL, - CONF_SLAVE, - STATE_OPEN, - STATE_OPENING, -) - -from .conftest import base_config_test, base_test - - -@pytest.mark.parametrize( - "do_options", - [ - {}, - { - CONF_SLAVE: 10, - CONF_SCAN_INTERVAL: 20, - }, - ], -) -@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) -async def test_config_cover(hass, do_options, read_type): - """Run test for cover.""" - device_name = "test_cover" - device_config = { - CONF_NAME: device_name, - read_type: 1234, - **do_options, - } - await base_config_test( - hass, - device_config, - device_name, - COVER_DOMAIN, - CONF_COVERS, - None, - method_discovery=True, - ) - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x00], - STATE_OPENING, - ), - ( - [0x80], - STATE_OPENING, - ), - ( - [0xFE], - STATE_OPENING, - ), - ( - [0xFF], - STATE_OPENING, - ), - ( - [0x01], - STATE_OPENING, - ), - ], -) -async def test_coil_cover(hass, regs, expected): - """Run test for given config.""" - cover_name = "modbus_test_cover" - state = await base_test( - hass, - { - CONF_NAME: cover_name, - CALL_TYPE_COIL: 1234, - CONF_SLAVE: 1, - }, - cover_name, - COVER_DOMAIN, - CONF_COVERS, - None, - regs, - expected, - method_discovery=True, - scan_interval=5, - ) - assert state == expected - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x00], - STATE_OPEN, - ), - ( - [0x80], - STATE_OPEN, - ), - ( - [0xFE], - STATE_OPEN, - ), - ( - [0xFF], - STATE_OPEN, - ), - ( - [0x01], - STATE_OPEN, - ), - ], -) -async def test_register_cover(hass, regs, expected): - """Run test for given config.""" - cover_name = "modbus_test_cover" - state = await base_test( - hass, - { - CONF_NAME: cover_name, - CONF_REGISTER: 1234, - CONF_SLAVE: 1, - }, - cover_name, - COVER_DOMAIN, - CONF_COVERS, - None, - regs, - expected, - method_discovery=True, - scan_interval=5, - ) - assert state == expected - - -@pytest.mark.parametrize("read_type", [CALL_TYPE_COIL, CONF_REGISTER]) -async def test_unsupported_config_cover(hass, read_type, caplog): - """ - Run test for cover. - - Initialize the Cover in the legacy manner via platform. - This test expects that the Cover won't be initialized, and that we get a config warning. - """ - device_name = "test_cover" - device_config = {CONF_NAME: device_name, read_type: 1234} - - caplog.set_level(logging.WARNING) - caplog.clear() - - await base_config_test( - hass, - device_config, - device_name, - COVER_DOMAIN, - CONF_COVERS, - None, - method_discovery=False, - expect_init_to_fail=True, - ) - - assert len(caplog.records) == 1 - assert caplog.records[0].levelname == "WARNING" diff --git a/tests/components/modbus/test_modbus_sensor.py b/tests/components/modbus/test_modbus_sensor.py index b8ab10953c8..cb784ac46b3 100644 --- a/tests/components/modbus/test_modbus_sensor.py +++ b/tests/components/modbus/test_modbus_sensor.py @@ -1,6 +1,5 @@ """The tests for the Modbus sensor component.""" import logging -from unittest import mock import pytest @@ -40,7 +39,9 @@ from homeassistant.const import ( ) from homeassistant.core import State -from .conftest import base_config_test, base_test +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import mock_restore_cache @pytest.mark.parametrize( @@ -573,24 +574,21 @@ async def test_restore_state_sensor(hass): sensor_name = "test_sensor" test_value = "117" config_sensor = {CONF_NAME: sensor_name, CONF_ADDRESS: 17} - with mock.patch( - "homeassistant.components.modbus.sensor.ModbusRegisterSensor.async_get_last_state" - ) as mock_get_last_state: - mock_get_last_state.return_value = State( - f"{SENSOR_DOMAIN}.{sensor_name}", f"{test_value}" - ) - - await base_config_test( - hass, - config_sensor, - sensor_name, - SENSOR_DOMAIN, - CONF_SENSORS, - None, - method_discovery=True, - ) - entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" - assert hass.states.get(entity_id).state == test_value + mock_restore_cache( + hass, + (State(f"{SENSOR_DOMAIN}.{sensor_name}", test_value),), + ) + await base_config_test( + hass, + config_sensor, + sensor_name, + SENSOR_DOMAIN, + CONF_SENSORS, + None, + method_discovery=True, + ) + entity_id = f"{SENSOR_DOMAIN}.{sensor_name}" + assert hass.states.get(entity_id).state == test_value @pytest.mark.parametrize( @@ -621,3 +619,32 @@ async def test_swap_sensor_wrong_config(hass, caplog, swap_type): expect_init_to_fail=True, ) assert caplog.messages[-1].startswith("Error in sensor " + sensor_name + " swap") + + +async def test_service_sensor_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "sensor.test" + config = { + CONF_SENSORS: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + } + ] + } + mock_pymodbus.read_input_registers.return_value = ReadResult([27]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "27" + mock_pymodbus.read_input_registers.return_value = ReadResult([32]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == "32" diff --git a/tests/components/modbus/test_modbus_switch.py b/tests/components/modbus/test_modbus_switch.py deleted file mode 100644 index 91ab5bf97df..00000000000 --- a/tests/components/modbus/test_modbus_switch.py +++ /dev/null @@ -1,308 +0,0 @@ -"""The tests for the Modbus switch component.""" -import pytest - -from homeassistant.components.modbus.const import ( - CALL_TYPE_COIL, - CALL_TYPE_REGISTER_HOLDING, - CALL_TYPE_REGISTER_INPUT, - CONF_COILS, - CONF_INPUT_TYPE, - CONF_REGISTER, - CONF_REGISTER_TYPE, - CONF_REGISTERS, - CONF_STATE_OFF, - CONF_STATE_ON, - CONF_VERIFY_REGISTER, - CONF_VERIFY_STATE, -) -from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.const import ( - CONF_ADDRESS, - CONF_COMMAND_OFF, - CONF_COMMAND_ON, - CONF_DEVICE_CLASS, - CONF_NAME, - CONF_SLAVE, - CONF_SWITCHES, - STATE_OFF, - STATE_ON, -) - -from .conftest import base_config_test, base_test - - -@pytest.mark.parametrize( - "array_type, do_config", - [ - ( - None, - { - CONF_ADDRESS: 1234, - }, - ), - ( - None, - { - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, - ), - ( - None, - { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - CONF_VERIFY_REGISTER: 1235, - CONF_VERIFY_STATE: False, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_DEVICE_CLASS: "switch", - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, - }, - ), - ( - None, - { - CONF_ADDRESS: 1234, - CONF_SLAVE: 1, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - CONF_VERIFY_REGISTER: 1235, - CONF_VERIFY_STATE: True, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_DEVICE_CLASS: "switch", - CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, - }, - ), - ( - None, - { - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_COIL, - CONF_SLAVE: 1, - CONF_DEVICE_CLASS: "switch", - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, - ), - ( - CONF_REGISTERS, - { - CONF_REGISTER: 1234, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - }, - ), - ( - CONF_REGISTERS, - { - CONF_REGISTER: 1234, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - }, - ), - ( - CONF_COILS, - { - CALL_TYPE_COIL: 1234, - CONF_SLAVE: 1, - }, - ), - ( - CONF_REGISTERS, - { - CONF_REGISTER: 1234, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_SLAVE: 1, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - CONF_VERIFY_REGISTER: 1235, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY_STATE: True, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_INPUT, - }, - ), - ( - CONF_REGISTERS, - { - CONF_REGISTER: 1234, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_SLAVE: 1, - CONF_STATE_OFF: 0, - CONF_STATE_ON: 1, - CONF_VERIFY_REGISTER: 1235, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - CONF_VERIFY_STATE: True, - CONF_REGISTER_TYPE: CALL_TYPE_REGISTER_HOLDING, - }, - ), - ( - CONF_COILS, - { - CALL_TYPE_COIL: 1234, - CONF_SLAVE: 1, - }, - ), - ], -) -async def test_config_switch(hass, array_type, do_config): - """Run test for switch.""" - device_name = "test_switch" - - device_config = { - CONF_NAME: device_name, - **do_config, - } - - await base_config_test( - hass, - device_config, - device_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - array_type, - method_discovery=(array_type is None), - ) - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x00], - STATE_OFF, - ), - ( - [0x80], - STATE_OFF, - ), - ( - [0xFE], - STATE_OFF, - ), - ( - [0xFF], - STATE_ON, - ), - ( - [0x01], - STATE_ON, - ), - ], -) -async def test_coil_switch(hass, regs, expected): - """Run test for given config.""" - switch_name = "modbus_test_switch" - state = await base_test( - hass, - { - CONF_NAME: switch_name, - CONF_ADDRESS: 1234, - CONF_INPUT_TYPE: CALL_TYPE_COIL, - }, - switch_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - CONF_COILS, - regs, - expected, - method_discovery=True, - scan_interval=5, - ) - assert state == expected - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x00], - STATE_OFF, - ), - ( - [0x80], - STATE_OFF, - ), - ( - [0xFE], - STATE_OFF, - ), - ( - [0xFF], - STATE_OFF, - ), - ( - [0x01], - STATE_ON, - ), - ], -) -async def test_register_switch(hass, regs, expected): - """Run test for given config.""" - switch_name = "modbus_test_switch" - state = await base_test( - hass, - { - CONF_NAME: switch_name, - CONF_REGISTER: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x00, - CONF_COMMAND_ON: 0x01, - }, - switch_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - CONF_REGISTERS, - regs, - expected, - method_discovery=False, - scan_interval=5, - ) - assert state == expected - - -@pytest.mark.parametrize( - "regs,expected", - [ - ( - [0x40], - STATE_ON, - ), - ( - [0x04], - STATE_OFF, - ), - ( - [0xFF], - STATE_OFF, - ), - ], -) -async def test_register_state_switch(hass, regs, expected): - """Run test for given config.""" - switch_name = "modbus_test_switch" - state = await base_test( - hass, - { - CONF_NAME: switch_name, - CONF_REGISTER: 1234, - CONF_SLAVE: 1, - CONF_COMMAND_OFF: 0x04, - CONF_COMMAND_ON: 0x40, - }, - switch_name, - SWITCH_DOMAIN, - CONF_SWITCHES, - CONF_REGISTERS, - regs, - expected, - method_discovery=False, - scan_interval=5, - ) - assert state == expected diff --git a/tests/components/modbus/test_switch.py b/tests/components/modbus/test_switch.py new file mode 100644 index 00000000000..37ddfec2b4d --- /dev/null +++ b/tests/components/modbus/test_switch.py @@ -0,0 +1,345 @@ +"""The tests for the Modbus switch component.""" +from datetime import timedelta +from unittest import mock + +from pymodbus.exceptions import ModbusException +import pytest + +from homeassistant.components.modbus.const import ( + CALL_TYPE_COIL, + CALL_TYPE_DISCRETE, + CALL_TYPE_REGISTER_HOLDING, + CALL_TYPE_REGISTER_INPUT, + CONF_INPUT_TYPE, + CONF_STATE_OFF, + CONF_STATE_ON, + CONF_VERIFY, + CONF_WRITE_TYPE, + MODBUS_DOMAIN, +) +from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN +from homeassistant.const import ( + CONF_ADDRESS, + CONF_COMMAND_OFF, + CONF_COMMAND_ON, + CONF_DELAY, + CONF_DEVICE_CLASS, + CONF_HOST, + CONF_NAME, + CONF_PORT, + CONF_SCAN_INTERVAL, + CONF_SLAVE, + CONF_SWITCHES, + CONF_TYPE, + STATE_OFF, + STATE_ON, + STATE_UNAVAILABLE, +) +from homeassistant.core import State +from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util + +from .conftest import ReadResult, base_config_test, base_test, prepare_service_update + +from tests.common import async_fire_time_changed, mock_restore_cache + + +@pytest.mark.parametrize( + "do_config", + [ + { + CONF_ADDRESS: 1234, + }, + { + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_INPUT, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + CONF_DELAY: 10, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_VERIFY: { + CONF_INPUT_TYPE: CALL_TYPE_DISCRETE, + CONF_ADDRESS: 1235, + CONF_STATE_OFF: 0, + CONF_STATE_ON: 1, + }, + }, + { + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_COMMAND_OFF: 0x00, + CONF_COMMAND_ON: 0x01, + CONF_DEVICE_CLASS: "switch", + CONF_SCAN_INTERVAL: 0, + CONF_VERIFY: None, + }, + ], +) +async def test_config_switch(hass, do_config): + """Run test for switch.""" + device_name = "test_switch" + + device_config = { + CONF_NAME: device_name, + **do_config, + } + + await base_config_test( + hass, + device_config, + device_name, + SWITCH_DOMAIN, + CONF_SWITCHES, + None, + method_discovery=True, + ) + + +@pytest.mark.parametrize("call_type", [CALL_TYPE_COIL, CALL_TYPE_REGISTER_HOLDING]) +@pytest.mark.parametrize( + "regs,verify,expected", + [ + ( + [0x00], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + [0x01], + {CONF_VERIFY: {}}, + STATE_ON, + ), + ( + [0xFE], + {CONF_VERIFY: {}}, + STATE_OFF, + ), + ( + None, + {CONF_VERIFY: {}}, + STATE_UNAVAILABLE, + ), + ( + None, + {}, + STATE_OFF, + ), + ], +) +async def test_all_switch(hass, call_type, regs, verify, expected): + """Run test for given config.""" + switch_name = "modbus_test_switch" + state = await base_test( + hass, + { + CONF_NAME: switch_name, + CONF_ADDRESS: 1234, + CONF_SLAVE: 1, + CONF_WRITE_TYPE: call_type, + **verify, + }, + switch_name, + SWITCH_DOMAIN, + CONF_SWITCHES, + None, + regs, + expected, + method_discovery=True, + scan_interval=5, + ) + assert state == expected + + +async def test_restore_state_switch(hass): + """Run test for sensor restore state.""" + + switch_name = "test_switch" + entity_id = f"{SWITCH_DOMAIN}.{switch_name}" + test_value = STATE_ON + config_switch = {CONF_NAME: switch_name, CONF_ADDRESS: 17} + mock_restore_cache( + hass, + (State(f"{entity_id}", test_value),), + ) + await base_config_test( + hass, + config_switch, + switch_name, + SWITCH_DOMAIN, + CONF_SWITCHES, + None, + method_discovery=True, + ) + assert hass.states.get(entity_id).state == test_value + + +async def test_switch_service_turn(hass, caplog, mock_pymodbus): + """Run test for service turn_on/turn_off.""" + + entity_id1 = f"{SWITCH_DOMAIN}.switch1" + entity_id2 = f"{SWITCH_DOMAIN}.switch2" + config = { + MODBUS_DOMAIN: { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_SWITCHES: [ + { + CONF_NAME: "switch1", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + { + CONF_NAME: "switch2", + CONF_ADDRESS: 17, + CONF_WRITE_TYPE: CALL_TYPE_REGISTER_HOLDING, + CONF_VERIFY: {}, + }, + ], + }, + } + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + assert MODBUS_DOMAIN in hass.config.components + + assert hass.states.get(entity_id1).state == STATE_OFF + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_ON + await hass.services.async_call( + "switch", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_OFF + + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + assert hass.states.get(entity_id2).state == STATE_OFF + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_ON + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x00]) + await hass.services.async_call( + "switch", "turn_off", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_OFF + + mock_pymodbus.write_register.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id2} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id2).state == STATE_UNAVAILABLE + mock_pymodbus.write_coil.side_effect = ModbusException("fail write_") + await hass.services.async_call( + "switch", "turn_off", service_data={"entity_id": entity_id1} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id1).state == STATE_UNAVAILABLE + + +async def test_service_switch_update(hass, mock_pymodbus): + """Run test for service homeassistant.update_entity.""" + + entity_id = "switch.test" + config = { + CONF_SWITCHES: [ + { + CONF_NAME: "test", + CONF_ADDRESS: 1234, + CONF_WRITE_TYPE: CALL_TYPE_COIL, + CONF_VERIFY: {}, + } + ] + } + mock_pymodbus.read_discrete_inputs.return_value = ReadResult([0x01]) + await prepare_service_update( + hass, + config, + ) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_ON + mock_pymodbus.read_coils.return_value = ReadResult([0x00]) + await hass.services.async_call( + "homeassistant", "update_entity", {"entity_id": entity_id}, blocking=True + ) + assert hass.states.get(entity_id).state == STATE_OFF + + +async def test_delay_switch(hass, mock_pymodbus): + """Run test for switch verify delay.""" + + switch_name = "test_switch" + entity_id = f"{SWITCH_DOMAIN}.{switch_name}" + + config = { + MODBUS_DOMAIN: [ + { + CONF_TYPE: "tcp", + CONF_HOST: "modbusTestHost", + CONF_PORT: 5501, + CONF_SWITCHES: [ + { + CONF_NAME: switch_name, + CONF_ADDRESS: 51, + CONF_SCAN_INTERVAL: 0, + CONF_VERIFY: { + CONF_DELAY: 1, + CONF_INPUT_TYPE: CALL_TYPE_REGISTER_HOLDING, + }, + } + ], + } + ] + } + mock_pymodbus.read_holding_registers.return_value = ReadResult([0x01]) + now = dt_util.utcnow() + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + assert await async_setup_component(hass, MODBUS_DOMAIN, config) is True + await hass.async_block_till_done() + await hass.services.async_call( + "switch", "turn_on", service_data={"entity_id": entity_id} + ) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_OFF + now = now + timedelta(seconds=2) + with mock.patch("homeassistant.helpers.event.dt_util.utcnow", return_value=now): + async_fire_time_changed(hass, now) + await hass.async_block_till_done() + assert hass.states.get(entity_id).state == STATE_ON diff --git a/tests/components/motioneye/test_camera.py b/tests/components/motioneye/test_camera.py index 921dc9df920..f1ddcea4386 100644 --- a/tests/components/motioneye/test_camera.py +++ b/tests/components/motioneye/test_camera.py @@ -4,7 +4,7 @@ import logging from typing import Any from unittest.mock import AsyncMock, Mock -from aiohttp import web # type: ignore +from aiohttp import web from aiohttp.web_exceptions import HTTPBadGateway from motioneye_client.client import ( MotionEyeClientError, @@ -165,6 +165,7 @@ async def test_setup_camera_new_data_error(hass: HomeAssistant) -> None: async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state assert entity_state.state == "unavailable" @@ -173,6 +174,7 @@ async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> N client = create_mock_motioneye_client() await setup_mock_motioneye_config_entry(hass, client=client) entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state assert entity_state.state == "idle" cameras = copy.deepcopy(TEST_CAMERAS) @@ -181,6 +183,7 @@ async def test_setup_camera_new_data_without_streaming(hass: HomeAssistant) -> N async_fire_time_changed(hass, dt_util.utcnow() + DEFAULT_SCAN_INTERVAL) await hass.async_block_till_done() entity_state = hass.states.get(TEST_CAMERA_ENTITY_ID) + assert entity_state assert entity_state.state == "unavailable" diff --git a/tests/components/motioneye/test_config_flow.py b/tests/components/motioneye/test_config_flow.py index d8700e162c4..fbdabdadb41 100644 --- a/tests/components/motioneye/test_config_flow.py +++ b/tests/components/motioneye/test_config_flow.py @@ -69,6 +69,58 @@ async def test_user_success(hass: HomeAssistant) -> None: assert mock_client.async_client_close.called +async def test_hassio_success(hass: HomeAssistant) -> None: + """Test successful Supervisor flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "hassio_confirm" + assert result.get("description_placeholders") == {"addon": "motionEye"} + assert "flow_id" in result + + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result2.get("step_id") == "user" + assert "flow_id" in result2 + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "Add-on" + assert result3.get("data") == { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + } + assert len(mock_setup_entry.mock_calls) == 1 + assert mock_client.async_client_close.called + + async def test_user_invalid_auth(hass: HomeAssistant) -> None: """Test invalid auth is handled correctly.""" result = await hass.config_entries.flow.async_init( @@ -235,7 +287,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "reauth_successful" - assert config_entry.data == new_data + assert dict(config_entry.data) == new_data assert len(mock_setup_entry.mock_calls) == 1 assert mock_client.async_client_close.called @@ -287,3 +339,95 @@ async def test_duplicate(hass: HomeAssistant) -> None: assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" assert mock_client.async_client_close.called + + +async def test_hassio_already_configured(hass: HomeAssistant) -> None: + """Test we don't discover when already configured.""" + MockConfigEntry( + domain=DOMAIN, + data={CONF_URL: TEST_URL}, + ).add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_hassio_ignored(hass: HomeAssistant) -> None: + """Test Supervisor discovered instance can be ignored.""" + MockConfigEntry(domain=DOMAIN, source=config_entries.SOURCE_IGNORE).add_to_hass( + hass + ) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_hassio_abort_if_already_in_progress(hass: HomeAssistant) -> None: + """Test Supervisor discovered flow aborts if user flow in progress.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_ABORT + assert result2.get("reason") == "already_in_progress" + + +async def test_hassio_clean_up_on_user_flow(hass: HomeAssistant) -> None: + """Test Supervisor discovered flow is clean up when doing user flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data={"addon": "motionEye", "url": TEST_URL}, + context={"source": config_entries.SOURCE_HASSIO}, + ) + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result2.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert "flow_id" in result2 + + mock_client = create_mock_motioneye_client() + + with patch( + "homeassistant.components.motioneye.MotionEyeClient", + return_value=mock_client, + ), patch( + "homeassistant.components.motioneye.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_URL: TEST_URL, + CONF_ADMIN_USERNAME: "admin-username", + CONF_ADMIN_PASSWORD: "admin-password", + CONF_SURVEILLANCE_USERNAME: "surveillance-username", + CONF_SURVEILLANCE_PASSWORD: "surveillance-password", + }, + ) + await hass.async_block_till_done() + + assert result3.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert len(mock_setup_entry.mock_calls) == 1 + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 diff --git a/tests/components/mqtt/test_common.py b/tests/components/mqtt/test_common.py index 3d58cf834e9..fc8d26843d0 100644 --- a/tests/components/mqtt/test_common.py +++ b/tests/components/mqtt/test_common.py @@ -24,7 +24,7 @@ DEFAULT_CONFIG_DEVICE_INFO_ID = { } DEFAULT_CONFIG_DEVICE_INFO_MAC = { - "connections": [["mac", "02:5b:26:a8:dc:12"]], + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", @@ -760,9 +760,11 @@ async def help_test_entity_device_info_with_connection(hass, mqtt_mock, domain, async_fire_mqtt_message(hass, f"homeassistant/{domain}/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + device = registry.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + ) assert device is not None - assert device.connections == {("mac", "02:5b:26:a8:dc:12")} + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" diff --git a/tests/components/mqtt/test_cover.py b/tests/components/mqtt/test_cover.py index 8d729ca9dde..c5a8552dc20 100644 --- a/tests/components/mqtt/test_cover.py +++ b/tests/components/mqtt/test_cover.py @@ -10,10 +10,22 @@ from homeassistant.components.cover import ( ATTR_POSITION, ATTR_TILT_POSITION, ) -from homeassistant.components.mqtt.cover import MqttCover +from homeassistant.components.mqtt.const import CONF_STATE_TOPIC +from homeassistant.components.mqtt.cover import ( + CONF_GET_POSITION_TEMPLATE, + CONF_GET_POSITION_TOPIC, + CONF_SET_POSITION_TEMPLATE, + CONF_SET_POSITION_TOPIC, + CONF_TILT_COMMAND_TEMPLATE, + CONF_TILT_COMMAND_TOPIC, + CONF_TILT_STATUS_TEMPLATE, + CONF_TILT_STATUS_TOPIC, + MqttCover, +) from homeassistant.const import ( ATTR_ASSUMED_STATE, ATTR_ENTITY_ID, + CONF_VALUE_TEMPLATE, SERVICE_CLOSE_COVER, SERVICE_CLOSE_COVER_TILT, SERVICE_OPEN_COVER, @@ -260,6 +272,45 @@ async def test_state_via_template(hass, mqtt_mock): assert state.state == STATE_CLOSED +async def test_state_via_template_and_entity_id(hass, mqtt_mock): + """Test the controlling state via topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "qos": 0, + "value_template": '\ + {% if value == "open" or value == "closed" %}\ + {{ value }}\ + {% else %}\ + {{ states(entity_id) }}\ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("cover.test") + assert state.state == STATE_UNKNOWN + + async_fire_mqtt_message(hass, "state-topic", "open") + async_fire_mqtt_message(hass, "state-topic", "invalid") + + state = hass.states.get("cover.test") + assert state.state == STATE_OPEN + + async_fire_mqtt_message(hass, "state-topic", "closed") + async_fire_mqtt_message(hass, "state-topic", "invalid") + + state = hass.states.get("cover.test") + assert state.state == STATE_CLOSED + + async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): """Test the controlling state via topic with JSON value.""" assert await async_setup_component( @@ -299,7 +350,7 @@ async def test_state_via_template_with_json_value(hass, mqtt_mock, caplog): ) in caplog.text -async def test_position_via_template(hass, mqtt_mock): +async def test_position_via_template_and_entity_id(hass, mqtt_mock): """Test the controlling state via topic.""" assert await async_setup_component( hass, @@ -311,7 +362,12 @@ async def test_position_via_template(hass, mqtt_mock): "position_topic": "get-position-topic", "command_topic": "command-topic", "qos": 0, - "value_template": "{{ (value | multiply(0.01)) | int }}", + "position_template": '\ + {% if state_attr(entity_id, "current_position") == None %}\ + {{ value }}\ + {% else %}\ + {{ state_attr(entity_id, "current_position") + value | int }}\ + {% endif %}', } }, ) @@ -320,20 +376,19 @@ async def test_position_via_template(hass, mqtt_mock): state = hass.states.get("cover.test") assert state.state == STATE_UNKNOWN - async_fire_mqtt_message(hass, "get-position-topic", "10000") + async_fire_mqtt_message(hass, "get-position-topic", "10") - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 10 - async_fire_mqtt_message(hass, "get-position-topic", "5000") + async_fire_mqtt_message(hass, "get-position-topic", "10") - state = hass.states.get("cover.test") - assert state.state == STATE_OPEN - - async_fire_mqtt_message(hass, "get-position-topic", "99") - - state = hass.states.get("cover.test") - assert state.state == STATE_CLOSED + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 20 async def test_optimistic_state_change(hass, mqtt_mock): @@ -712,7 +767,13 @@ async def test_position_update(hass, mqtt_mock): assert current_cover_position == 22 -async def test_set_position_templated(hass, mqtt_mock): +@pytest.mark.parametrize( + "pos_template,pos_call,pos_message", + [("{{position-1}}", 43, "42"), ("{{100-62}}", 100, "38")], +) +async def test_set_position_templated( + hass, mqtt_mock, pos_template, pos_call, pos_message +): """Test setting cover position via template.""" assert await async_setup_component( hass, @@ -726,7 +787,51 @@ async def test_set_position_templated(hass, mqtt_mock): "position_open": 100, "position_closed": 0, "set_position_topic": "set-position-topic", - "set_position_template": "{{100-62}}", + "set_position_template": pos_template, + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_POSITION: pos_call}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "set-position-topic", pos_message, 0, False + ) + + +async def test_set_position_templated_and_attributes(hass, mqtt_mock): + """Test setting cover position via template and using entities attributes.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": '\ + {% if position > 99 %}\ + {% if state_attr(entity_id, "current_position") == None %}\ + {{ 5 }}\ + {% else %}\ + {{ 23 }}\ + {% endif %}\ + {% else %}\ + {{ 42 }}\ + {% endif %}', "payload_open": "OPEN", "payload_close": "CLOSE", "payload_stop": "STOP", @@ -742,8 +847,85 @@ async def test_set_position_templated(hass, mqtt_mock): blocking=True, ) + mqtt_mock.async_publish.assert_called_once_with("set-position-topic", "5", 0, False) + + +async def test_set_tilt_templated(hass, mqtt_mock): + """Test setting cover tilt position via template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": "{{tilt_position+1}}", + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 41}, + blocking=True, + ) + mqtt_mock.async_publish.assert_called_once_with( - "set-position-topic", "38", 0, False + "tilt-command-topic", "42", 0, False + ) + + +async def test_set_tilt_templated_and_attributes(hass, mqtt_mock): + """Test setting cover tilt position via template and using entities attributes.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "position_topic": "get-position-topic", + "command_topic": "command-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 100, + "position_closed": 0, + "set_position_topic": "set-position-topic", + "set_position_template": "{{position-1}}", + "tilt_command_template": '\ + {% if state_attr(entity_id, "friendly_name") != "test" %}\ + {{ 5 }}\ + {% else %}\ + {{ 23 }}\ + {% endif %}', + "payload_open": "OPEN", + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + await hass.services.async_call( + cover.DOMAIN, + SERVICE_SET_COVER_TILT_POSITION, + {ATTR_ENTITY_ID: "cover.test", ATTR_TILT_POSITION: 99}, + blocking=True, + ) + + mqtt_mock.async_publish.assert_called_once_with( + "tilt-command-topic", "23", 0, False ) @@ -832,6 +1014,50 @@ async def test_no_command_topic(hass, mqtt_mock): assert hass.states.get("cover.test").attributes["supported_features"] == 240 +async def test_no_payload_close(hass, mqtt_mock): + """Test with no close payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": "OPEN", + "payload_close": None, + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("cover.test").attributes["supported_features"] == 9 + + +async def test_no_payload_open(hass, mqtt_mock): + """Test with no open payload.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "qos": 0, + "payload_open": None, + "payload_close": "CLOSE", + "payload_stop": "STOP", + } + }, + ) + await hass.async_block_till_done() + + assert hass.states.get("cover.test").attributes["supported_features"] == 10 + + async def test_no_payload_stop(hass, mqtt_mock): """Test with no stop payload.""" assert await async_setup_component( @@ -1692,7 +1918,6 @@ async def test_find_percentage_in_range_defaults(hass, mqtt_mock): "tilt_min": 0, "tilt_max": 100, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1736,7 +1961,6 @@ async def test_find_percentage_in_range_altered(hass, mqtt_mock): "tilt_min": 80, "tilt_max": 180, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1777,10 +2001,9 @@ async def test_find_percentage_in_range_defaults_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 100, "tilt_closed_position": 0, - "tilt_min": 0, - "tilt_max": 100, + "tilt_min": 100, + "tilt_max": 0, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1821,10 +2044,9 @@ async def test_find_percentage_in_range_altered_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 180, "tilt_closed_position": 80, - "tilt_min": 80, - "tilt_max": 180, + "tilt_min": 180, + "tilt_max": 80, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1868,7 +2090,6 @@ async def test_find_in_range_defaults(hass, mqtt_mock): "tilt_min": 0, "tilt_max": 100, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1912,7 +2133,6 @@ async def test_find_in_range_altered(hass, mqtt_mock): "tilt_min": 80, "tilt_max": 180, "tilt_optimistic": False, - "tilt_invert_state": False, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1953,10 +2173,9 @@ async def test_find_in_range_defaults_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 100, "tilt_closed_position": 0, - "tilt_min": 0, - "tilt_max": 100, + "tilt_min": 100, + "tilt_max": 0, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -1997,10 +2216,9 @@ async def test_find_in_range_altered_inverted(hass, mqtt_mock): "value_template": None, "tilt_open_position": 180, "tilt_closed_position": 80, - "tilt_min": 80, - "tilt_max": 180, + "tilt_min": 180, + "tilt_max": 80, "tilt_optimistic": False, - "tilt_invert_state": True, "set_position_topic": None, "set_position_template": None, "unique_id": None, @@ -2223,105 +2441,6 @@ async def test_entity_debug_info_message(hass, mqtt_mock): ) -async def test_deprecated_value_template_for_position_topic_warning( - hass, caplog, mqtt_mock -): - """Test warning when value_template is used for position_topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "position-topic", - "value_template": "{{100-62}}", - } - }, - ) - await hass.async_block_till_done() - - assert ( - "Using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please replace it with 'position_template'" - ) in caplog.text - - -async def test_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock): - """Test warning when tilt_invert_state is used.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "tilt_invert_state": True, - } - }, - ) - await hass.async_block_till_done() - - assert ( - "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please invert tilt using 'tilt_min' & 'tilt_max'" - ) in caplog.text - - -async def test_no_deprecated_tilt_invert_state_warning(hass, caplog, mqtt_mock): - """Test warning when tilt_invert_state is used.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - } - }, - ) - await hass.async_block_till_done() - - assert ( - "'tilt_invert_state' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please invert tilt using 'tilt_min' & 'tilt_max'" - ) not in caplog.text - - -async def test_no_deprecated_warning_for_position_topic_using_position_template( - hass, caplog, mqtt_mock -): - """Test no warning when position_template is used for position_topic.""" - assert await async_setup_component( - hass, - cover.DOMAIN, - { - cover.DOMAIN: { - "platform": "mqtt", - "name": "test", - "command_topic": "command-topic", - "set_position_topic": "set-position-topic", - "position_topic": "position-topic", - "position_template": "{{100-62}}", - } - }, - ) - await hass.async_block_till_done() - - assert ( - "using 'value_template' for 'position_topic' is deprecated " - "and will be removed from Home Assistant in version 2021.6, " - "please replace it with 'position_template'" - ) not in caplog.text - - async def test_state_and_position_topics_state_not_set_via_position_topic( hass, mqtt_mock ): @@ -2508,6 +2627,187 @@ async def test_position_via_position_topic_template_json_value(hass, mqtt_mock, ) in caplog.text +async def test_position_template_with_entity_id(hass, mqtt_mock): + """Test position by updating status via position template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ + {% if state_attr(entity_id, "current_position") != None %}\ + {{ value | int + state_attr(entity_id, "current_position") }} \ + {% else %} \ + {{ value }} \ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "10") + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 10 + + async_fire_mqtt_message(hass, "get-position-topic", "10") + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 20 + + +async def test_position_via_position_topic_template_return_json(hass, mqtt_mock): + """Test position by updating status via position template and returning json.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : value} | tojson }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "55") + + current_cover_position_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position_position == 55 + + +async def test_position_via_position_topic_template_return_json_warning( + hass, caplog, mqtt_mock +): + """Test position by updating status via position template returning json without position attribute.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"pos" : value} | tojson }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "55") + + assert ( + "Template (position_template) returned JSON without position attribute" + in caplog.text + ) + + +async def test_position_and_tilt_via_position_topic_template_return_json( + hass, mqtt_mock +): + """Test position and tilt by updating the position via position template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '\ + {{ {"position" : value, "tilt_position" : (value | int / 2)| int } | tojson }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + current_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_position == 0 and current_tilt_position == 0 + + async_fire_mqtt_message(hass, "get-position-topic", "99") + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + current_tilt_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_TILT_POSITION + ] + assert current_cover_position == 99 and current_tilt_position == 49 + + +async def test_position_via_position_topic_template_all_variables(hass, mqtt_mock): + """Test position by updating status via position template.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "tilt_command_topic": "tilt-command-topic", + "position_open": 99, + "position_closed": 1, + "tilt_min": 11, + "tilt_max": 22, + "position_template": "\ + {% if value | int < tilt_max %}\ + {{ tilt_min }}\ + {% endif %}\ + {% if value | int > position_closed %}\ + {{ position_open }}\ + {% endif %}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "0") + + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 10 + + async_fire_mqtt_message(hass, "get-position-topic", "55") + current_cover_position = hass.states.get("cover.test").attributes[ + ATTR_CURRENT_POSITION + ] + assert current_cover_position == 100 + + async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): """Test the controlling state via stopped state when no position topic.""" assert await async_setup_component( @@ -2555,3 +2855,168 @@ async def test_set_state_via_stopped_state_no_position_topic(hass, mqtt_mock): state = hass.states.get("cover.test") assert state.state == STATE_CLOSED + + +async def test_position_via_position_topic_template_return_invalid_json( + hass, caplog, mqtt_mock +): + """Test position by updating status via position template and returning invalid json.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "state-topic", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "position_topic": "get-position-topic", + "position_template": '{{ {"position" : invalid_json} }}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "get-position-topic", "55") + + assert ("Payload '{'position': Undefined}' is not numeric") in caplog.text + + +async def test_set_position_topic_without_get_position_topic_error( + hass, caplog, mqtt_mock +): + """Test error when set_position_topic is used without position_topic.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_topic": "set-position-topic", + "value_template": "{{100-62}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_SET_POSITION_TOPIC}' must be set together with '{CONF_GET_POSITION_TOPIC}'." + ) in caplog.text + + +async def test_value_template_without_state_topic_error(hass, caplog, mqtt_mock): + """Test error when value_template is used and state_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "value_template": "{{100-62}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_VALUE_TEMPLATE}' must be set together with '{CONF_STATE_TOPIC}'." + ) in caplog.text + + +async def test_position_template_without_position_topic_error(hass, caplog, mqtt_mock): + """Test error when position_template is used and position_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "position_template": "{{100-52}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_GET_POSITION_TEMPLATE}' must be set together with '{CONF_GET_POSITION_TOPIC}'." + in caplog.text + ) + + +async def test_set_position_template_without_set_position_topic( + hass, caplog, mqtt_mock +): + """Test error when set_position_template is used and set_position_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "set_position_template": "{{100-42}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_SET_POSITION_TEMPLATE}' must be set together with '{CONF_SET_POSITION_TOPIC}'." + in caplog.text + ) + + +async def test_tilt_command_template_without_tilt_command_topic( + hass, caplog, mqtt_mock +): + """Test error when tilt_command_template is used and tilt_command_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "tilt_command_template": "{{100-32}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_TILT_COMMAND_TEMPLATE}' must be set together with '{CONF_TILT_COMMAND_TOPIC}'." + in caplog.text + ) + + +async def test_tilt_status_template_without_tilt_status_topic_topic( + hass, caplog, mqtt_mock +): + """Test error when tilt_status_template is used and tilt_status_topic is missing.""" + assert await async_setup_component( + hass, + cover.DOMAIN, + { + cover.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "command-topic", + "tilt_status_template": "{{100-22}}", + } + }, + ) + await hass.async_block_till_done() + + assert ( + f"'{CONF_TILT_STATUS_TEMPLATE}' must be set together with '{CONF_TILT_STATUS_TOPIC}'." + in caplog.text + ) diff --git a/tests/components/mqtt/test_device_tracker_discovery.py b/tests/components/mqtt/test_device_tracker_discovery.py index f158a878fcd..174db8f017a 100644 --- a/tests/components/mqtt/test_device_tracker_discovery.py +++ b/tests/components/mqtt/test_device_tracker_discovery.py @@ -359,4 +359,4 @@ async def test_setting_device_tracker_location_via_lat_lon_message( async_fire_mqtt_message(hass, "attributes-topic", '{"latitude":32.87336}') state = hass.states.get("device_tracker.test") assert state.attributes["latitude"] == 32.87336 - assert state.state == STATE_NOT_HOME + assert state.state == STATE_UNKNOWN diff --git a/tests/components/mqtt/test_device_trigger.py b/tests/components/mqtt/test_device_trigger.py index 62ffade11f4..61c7f73b5fb 100644 --- a/tests/components/mqtt/test_device_trigger.py +++ b/tests/components/mqtt/test_device_trigger.py @@ -846,7 +846,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): "type": "foo", "subtype": "bar", "device": { - "connections": [["mac", "02:5b:26:a8:dc:12"]], + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", @@ -857,9 +857,11 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + device = registry.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + ) assert device is not None - assert device.connections == {("mac", "02:5b:26:a8:dc:12")} + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" @@ -908,7 +910,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): "subtype": "bar", "device": { "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", @@ -1162,32 +1164,68 @@ async def test_trigger_debug_info(hass, mqtt_mock): """ registry = dr.async_get(hass) - config = { + config1 = { "platform": "mqtt", "automation_type": "trigger", "topic": "test-topic", "type": "foo", "subtype": "bar", "device": { - "connections": [["mac", "02:5b:26:a8:dc:12"]], + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", "sw_version": "0.1-beta", }, } - data = json.dumps(config) - async_fire_mqtt_message(hass, "homeassistant/device_automation/bla/config", data) + config2 = { + "platform": "mqtt", + "automation_type": "trigger", + "topic": "test-topic2", + "type": "foo", + "subtype": "bar", + "device": { + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], + }, + } + data = json.dumps(config1) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", data) + data = json.dumps(config2) + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla2/config", data) await hass.async_block_till_done() - device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + device = registry.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + ) assert device is not None + debug_info_data = await debug_info.info_for_device(hass, device.id) + assert len(debug_info_data["entities"]) == 0 + assert len(debug_info_data["triggers"]) == 2 + topic_map = { + "homeassistant/device_automation/bla1/config": config1, + "homeassistant/device_automation/bla2/config": config2, + } + assert ( + topic_map[debug_info_data["triggers"][0]["discovery_data"]["topic"]] + != topic_map[debug_info_data["triggers"][1]["discovery_data"]["topic"]] + ) + assert ( + debug_info_data["triggers"][0]["discovery_data"]["payload"] + == topic_map[debug_info_data["triggers"][0]["discovery_data"]["topic"]] + ) + assert ( + debug_info_data["triggers"][1]["discovery_data"]["payload"] + == topic_map[debug_info_data["triggers"][1]["discovery_data"]["topic"]] + ) + + async_fire_mqtt_message(hass, "homeassistant/device_automation/bla1/config", "") + await hass.async_block_till_done() debug_info_data = await debug_info.info_for_device(hass, device.id) assert len(debug_info_data["entities"]) == 0 assert len(debug_info_data["triggers"]) == 1 assert ( debug_info_data["triggers"][0]["discovery_data"]["topic"] - == "homeassistant/device_automation/bla/config" + == "homeassistant/device_automation/bla2/config" ) - assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config + assert debug_info_data["triggers"][0]["discovery_data"]["payload"] == config2 diff --git a/tests/components/mqtt/test_fan.py b/tests/components/mqtt/test_fan.py index ee12a7ce03c..dad365e3c66 100644 --- a/tests/components/mqtt/test_fan.py +++ b/tests/components/mqtt/test_fan.py @@ -100,6 +100,8 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): "payload_low_speed": "speed_lOw", "payload_medium_speed": "speed_mEdium", "payload_high_speed": "speed_High", + "payload_reset_percentage": "rEset_percentage", + "payload_reset_preset_mode": "rEset_preset_mode", } }, ) @@ -168,6 +170,10 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" + async_fire_mqtt_message(hass, "preset-mode-state-topic", "rEset_preset_mode") + state = hass.states.get("fan.test") + assert state.attributes.get("preset_mode") is None + async_fire_mqtt_message(hass, "preset-mode-state-topic", "ModeUnknown") assert "not a valid preset mode" in caplog.text caplog.clear() @@ -191,6 +197,11 @@ async def test_controlling_state_via_topic(hass, mqtt_mock, caplog): state = hass.states.get("fan.test") assert state.attributes.get("speed") == fan.SPEED_OFF + async_fire_mqtt_message(hass, "percentage-state-topic", "rEset_percentage") + state = hass.states.get("fan.test") + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None + assert state.attributes.get(fan.ATTR_SPEED) is None + async_fire_mqtt_message(hass, "speed-state-topic", "speed_very_high") assert "not a valid speed" in caplog.text caplog.clear() @@ -408,6 +419,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get(fan.ATTR_PERCENTAGE) == 100 + async_fire_mqtt_message(hass, "percentage-state-topic", '{"val": "None"}') + state = hass.states.get("fan.test") + assert state.attributes.get(fan.ATTR_PERCENTAGE) is None + async_fire_mqtt_message(hass, "percentage-state-topic", '{"otherval": 100}') assert "Ignoring empty speed from" in caplog.text caplog.clear() @@ -428,6 +443,10 @@ async def test_controlling_state_via_topic_and_json_message(hass, mqtt_mock, cap state = hass.states.get("fan.test") assert state.attributes.get("preset_mode") == "silent" + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"val": "None"}') + state = hass.states.get("fan.test") + assert state.attributes.get("preset_mode") is None + async_fire_mqtt_message(hass, "preset-mode-state-topic", '{"otherval": 100}') assert "Ignoring empty preset_mode from" in caplog.text caplog.clear() @@ -1895,6 +1914,21 @@ async def test_supported_features(hass, mqtt_mock): "speed_range_min": 0, "speed_range_max": 40, }, + { + "platform": "mqtt", + "name": "test7reset_payload_in_preset_modes_a", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["auto", "smart", "normal", "None"], + }, + { + "platform": "mqtt", + "name": "test7reset_payload_in_preset_modes_b", + "command_topic": "command-topic", + "preset_mode_command_topic": "preset-mode-command-topic", + "preset_modes": ["whoosh", "silent", "auto", "None"], + "payload_reset_preset_mode": "normal", + }, ] }, ) @@ -1962,6 +1996,11 @@ async def test_supported_features(hass, mqtt_mock): state = hass.states.get("fan.test6spd_range_c") assert state is None + state = hass.states.get("fan.test7reset_payload_in_preset_modes_a") + assert state is None + state = hass.states.get("fan.test7reset_payload_in_preset_modes_b") + assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == fan.SUPPORT_PRESET_MODE + async def test_availability_when_connection_lost(hass, mqtt_mock): """Test availability after MQTT disconnection.""" diff --git a/tests/components/mqtt/test_init.py b/tests/components/mqtt/test_init.py index 15401a6d02f..ab0c58fb3b6 100644 --- a/tests/components/mqtt/test_init.py +++ b/tests/components/mqtt/test_init.py @@ -245,12 +245,17 @@ def test_entity_device_info_schema(): MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": ["abcd"]}) MQTT_ENTITY_DEVICE_INFO_SCHEMA({"identifiers": "abcd"}) # just connection - MQTT_ENTITY_DEVICE_INFO_SCHEMA({"connections": [["mac", "02:5b:26:a8:dc:12"]]}) + MQTT_ENTITY_DEVICE_INFO_SCHEMA( + {"connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]]} + ) # full device info MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], - "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], + "connections": [ + [dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"], + [dr.CONNECTION_ZIGBEE, "zigbee_id"], + ], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", @@ -261,7 +266,10 @@ def test_entity_device_info_schema(): MQTT_ENTITY_DEVICE_INFO_SCHEMA( { "identifiers": ["helloworld", "hello"], - "connections": [["mac", "02:5b:26:a8:dc:12"], ["zigbee", "zigbee_id"]], + "connections": [ + [dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"], + [dr.CONNECTION_ZIGBEE, "zigbee_id"], + ], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", diff --git a/tests/components/mqtt/test_light.py b/tests/components/mqtt/test_light.py index e995b373d03..e419743bd87 100644 --- a/tests/components/mqtt/test_light.py +++ b/tests/components/mqtt/test_light.py @@ -212,8 +212,8 @@ async def test_fail_setup_if_no_command_topic(hass, mqtt_mock): assert hass.states.get("light.test") is None -async def test_rgb_light(hass, mqtt_mock): - """Test RGB light flags brightness support.""" +async def test_legacy_rgb_white_light(hass, mqtt_mock): + """Test legacy RGB + white light flags brightness support.""" assert await async_setup_component( hass, light.DOMAIN, @@ -223,14 +223,19 @@ async def test_rgb_light(hass, mqtt_mock): "name": "test", "command_topic": "test_light_rgb/set", "rgb_command_topic": "test_light_rgb/rgb/set", + "white_value_command_topic": "test_light_rgb/white/set", } }, ) await hass.async_block_till_done() state = hass.states.get("light.test") - expected_features = light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS + expected_features = ( + light.SUPPORT_COLOR | light.SUPPORT_BRIGHTNESS | light.SUPPORT_WHITE_VALUE + ) assert state.attributes.get(ATTR_SUPPORTED_FEATURES) == expected_features + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["hs", "rgbw"] async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqtt_mock): @@ -255,8 +260,13 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] async_fire_mqtt_message(hass, "test_light_rgb/status", "ON") @@ -266,12 +276,17 @@ async def test_no_color_brightness_color_temp_hs_white_xy_if_no_topics(hass, mqt assert state.attributes.get("brightness") is None assert state.attributes.get("color_temp") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "onoff" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == ["onoff"] -async def test_controlling_state_via_topic(hass, mqtt_mock): - """Test the controlling of the state via topic.""" +async def test_legacy_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling of the state via topic for legacy light (white_value).""" config = { light.DOMAIN: { "platform": "mqtt", @@ -297,6 +312,7 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): "payload_off": 0, } } + color_modes = ["color_temp", "hs", "rgbw"] assert await async_setup_component(hass, light.DOMAIN, config) await hass.async_block_till_done() @@ -308,8 +324,13 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes assert not state.attributes.get(ATTR_ASSUMED_STATE) async_fire_mqtt_message(hass, "test_light_rgb/status", "1") @@ -321,8 +342,13 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("effect") is None assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None assert state.attributes.get("white_value") is None assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/status", "0") @@ -335,20 +361,28 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes["brightness"] == 100 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") light_state = hass.states.get("light.test") assert light_state.attributes.get("color_temp") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "100") light_state = hass.states.get("light.test") assert light_state.attributes["white_value"] == 100 assert light_state.attributes["color_temp"] == 300 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") light_state = hass.states.get("light.test") assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/status", "1") @@ -356,23 +390,152 @@ async def test_controlling_state_via_topic(hass, mqtt_mock): light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/white_value/status", "0") light_state = hass.states.get("light.test") assert light_state.attributes.get("rgb_color") == (255, 255, 255) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") light_state = hass.states.get("light.test") assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") light_state = hass.states.get("light.test") assert light_state.attributes.get("xy_color") == (0.672, 0.324) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes -async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): +async def test_controlling_state_via_topic(hass, mqtt_mock): + """Test the controlling of the state via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") == 100 + assert light_state.attributes["color_temp"] == 300 + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (125, 125, 125) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "80,40,20,10") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "80,40,20,10,8") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.675, 0.322) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_legacy_invalid_state_via_topic(hass, mqtt_mock, caplog): """Test handling of empty data via topic.""" config = { light.DOMAIN: { @@ -488,6 +651,140 @@ async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): assert light_state.attributes["white_value"] == 255 +async def test_invalid_state_via_topic(hass, mqtt_mock, caplog): + """Test handling of empty data via topic.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("xy_color") is None + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgb") + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "255,255,255") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "255") + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "none") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") == (255, 255, 255) + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") == (0, 0) + assert state.attributes.get("xy_color") == (0.323, 0.329) + assert state.attributes.get("color_mode") == "rgb" + + async_fire_mqtt_message(hass, "test_light_rgb/status", "") + assert "Ignoring empty state message" in caplog.text + light_state = hass.states.get("light.test") + assert state.state == STATE_ON + + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "") + assert "Ignoring empty brightness message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["brightness"] == 255 + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "") + assert "Ignoring empty color mode message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "") + assert "Ignoring empty effect message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "none" + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "") + assert "Ignoring empty rgb message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (255, 255, 255) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "") + assert "Ignoring empty hs message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "bad,bad") + assert "Failed to parse hs state update" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (0, 0) + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "") + assert "Ignoring empty xy-color message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.323, 0.329) + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "255,255,255,1") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "") + assert "Ignoring empty rgbw message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (255, 255, 255, 1) + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "255,255,255,1,2") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "") + assert "Ignoring empty rgbww message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (255, 255, 255, 1, 2) + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "153") + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "color_temp") + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") == 255 + assert state.attributes.get("color_temp") == 153 + assert state.attributes.get("effect") == "none" + assert state.attributes.get("hs_color") is None + assert state.attributes.get("xy_color") is None + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "") + assert "Ignoring empty color temp message" in caplog.text + light_state = hass.states.get("light.test") + assert light_state.attributes["color_temp"] == 153 + + async def test_brightness_controlling_scale(hass, mqtt_mock): """Test the brightness controlling scale.""" with assert_setup_component(1, light.DOMAIN): @@ -574,7 +871,7 @@ async def test_brightness_from_rgb_controlling_scale(hass, mqtt_mock): assert state.attributes.get("brightness") == 127 -async def test_white_value_controlling_scale(hass, mqtt_mock): +async def test_legacy_white_value_controlling_scale(hass, mqtt_mock): """Test the white_value controlling scale.""" with assert_setup_component(1, light.DOMAIN): assert await async_setup_component( @@ -621,7 +918,7 @@ async def test_white_value_controlling_scale(hass, mqtt_mock): assert light_state.attributes["white_value"] == 255 -async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): +async def test_legacy_controlling_state_via_topic_with_templates(hass, mqtt_mock): """Test the setting of the state with a template.""" config = { light.DOMAIN: { @@ -706,6 +1003,106 @@ async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): assert state.attributes.get("xy_color") == (0.14, 0.131) +async def test_controlling_state_via_topic_with_templates(hass, mqtt_mock): + """Test the setting of the state with a template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_command_topic": "test_light_rgb/rgbw/set", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_command_topic": "test_light_rgb/xy/set", + "brightness_state_topic": "test_light_rgb/brightness/status", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "effect_state_topic": "test_light_rgb/effect/status", + "hs_state_topic": "test_light_rgb/hs/status", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "xy_state_topic": "test_light_rgb/xy/status", + "state_value_template": "{{ value_json.hello }}", + "brightness_value_template": "{{ value_json.hello }}", + "color_temp_value_template": "{{ value_json.hello }}", + "effect_value_template": "{{ value_json.hello }}", + "hs_value_template": '{{ value_json.hello | join(",") }}', + "rgb_value_template": '{{ value_json.hello | join(",") }}', + "rgbw_value_template": '{{ value_json.hello | join(",") }}', + "rgbww_value_template": '{{ value_json.hello | join(",") }}', + "xy_value_template": '{{ value_json.hello | join(",") }}', + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("rgb_color") is None + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", '{"hello": [1, 2, 3]}') + async_fire_mqtt_message(hass, "test_light_rgb/status", '{"hello": "ON"}') + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", '{"hello": "50"}') + async_fire_mqtt_message( + hass, "test_light_rgb/effect/status", '{"hello": "rainbow"}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 50 + assert state.attributes.get("rgb_color") == (1, 2, 3) + assert state.attributes.get("effect") == "rainbow" + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/rgbw/status", '{"hello": [1, 2, 3, 4]}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgbw_color") == (1, 2, 3, 4) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/rgbww/status", '{"hello": [1, 2, 3, 4, 5]}' + ) + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgbww_color") == (1, 2, 3, 4, 5) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_temp/status", '{"hello": "300"}' + ) + state = hass.states.get("light.test") + assert state.attributes.get("color_temp") == 300 + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", '{"hello": [100,50]}') + state = hass.states.get("light.test") + assert state.attributes.get("hs_color") == (100, 50) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/xy/status", '{"hello": [0.123,0.123]}' + ) + state = hass.states.get("light.test") + assert state.attributes.get("xy_color") == (0.123, 0.123) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): """Test the setting of the state with undocumented value_template.""" config = { @@ -735,7 +1132,7 @@ async def test_controlling_state_via_topic_with_value_template(hass, mqtt_mock): assert state.state == STATE_OFF -async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): +async def test_legacy_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): """Test the sending of command in optimistic mode.""" config = { light.DOMAIN: { @@ -755,6 +1152,7 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): "payload_off": "off", } } + color_modes = ["color_temp", "hs", "rgbw"] fake_state = ha.State( "light.test", "on", @@ -782,18 +1180,20 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None assert state.attributes.get("white_value") is None assert state.attributes.get(ATTR_ASSUMED_STATE) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes await common.async_turn_on(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "on", 2, False ) mqtt_mock.async_publish.reset_mock() state = hass.states.get("light.test") assert state.state == STATE_ON + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes await common.async_turn_off(hass, "light.test") - mqtt_mock.async_publish.assert_called_once_with( "test_light_rgb/set", "off", 2, False ) @@ -805,8 +1205,19 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): await common.async_turn_on( hass, "light.test", brightness=50, xy_color=[0.123, 0.123] ) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes mqtt_mock.async_publish.assert_has_calls( [ @@ -829,6 +1240,9 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes.get("color_temp") is None await common.async_turn_on(hass, "light.test", white_value=80, color_temp=125) + state = hass.states.get("light.test") + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes mqtt_mock.async_publish.assert_has_calls( [ @@ -848,6 +1262,195 @@ async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): assert state.attributes["color_temp"] == 125 +async def test_sending_mqtt_commands_and_optimistic(hass, mqtt_mock): + """Test the sending of command in optimistic mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_command_topic": "test_light_rgb/xy/set", + "effect_list": ["colorloop", "random"], + "qos": 2, + "payload_on": "on", + "payload_off": "off", + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + fake_state = ha.State( + "light.test", + "on", + { + "brightness": 95, + "hs_color": [100, 100], + "effect": "random", + "color_temp": 100, + "color_mode": "hs", + }, + ) + with patch( + "homeassistant.helpers.restore_state.RestoreEntity.async_get_last_state", + return_value=fake_state, + ), assert_setup_component(1, light.DOMAIN): + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 95 + assert state.attributes.get("hs_color") == (100, 100) + assert state.attributes.get("effect") == "random" + assert state.attributes.get("color_temp") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert state.attributes.get(ATTR_ASSUMED_STATE) + + await common.async_turn_on(hass, "light.test", effect="colorloop") + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/effect/set", "colorloop", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("effect") == "colorloop" + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_off(hass, "light.test") + mqtt_mock.async_publish.assert_called_once_with( + "test_light_rgb/set", "off", 2, False + ) + mqtt_mock.async_publish.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=10, rgb_color=[80, 40, 20] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "10", 2, False), + call("test_light_rgb/rgb/set", "80,40,20", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 10 + assert state.attributes.get("rgb_color") == (80, 40, 20) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=20, rgbw_color=[80, 40, 20, 10] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "20", 2, False), + call("test_light_rgb/rgbw/set", "80,40,20,10", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 20 + assert state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on( + hass, "light.test", brightness=40, rgbww_color=[80, 40, 20, 10, 8] + ) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "40", 2, False), + call("test_light_rgb/rgbww/set", "80,40,20,10,8", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 40 + assert state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", brightness=50, hs_color=[359, 78]) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "50", 2, False), + call("test_light_rgb/hs/set", "359.0,78.0", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 50 + assert state.attributes.get("hs_color") == (359.0, 78.0) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", brightness=60, xy_color=[0.2, 0.3]) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 2, False), + call("test_light_rgb/brightness/set", "60", 2, False), + call("test_light_rgb/xy/set", "0.2,0.3", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 3 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 60 + assert state.attributes.get("xy_color") == (0.2, 0.3) + assert state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + await common.async_turn_on(hass, "light.test", color_temp=125) + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/color_temp/set", "125", 2, False), + ], + any_order=True, + ) + assert mqtt_mock.async_publish.call_count == 2 + mqtt_mock.reset_mock() + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") == 60 + assert state.attributes.get("color_temp") == 125 + assert state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): """Test the sending of RGB command with template.""" config = { @@ -875,14 +1478,88 @@ async def test_sending_mqtt_rgb_command_with_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_has_calls( [ call("test_light_rgb/set", "on", 0, False), - call("test_light_rgb/rgb/set", "#ff803f", 0, False), + call("test_light_rgb/rgb/set", "#ff8040", 0, False), ], any_order=True, ) state = hass.states.get("light.test") assert state.state == STATE_ON - assert state.attributes["rgb_color"] == (255, 128, 63) + assert state.attributes["rgb_color"] == (255, 128, 64) + + +async def test_sending_mqtt_rgbw_command_with_template(hass, mqtt_mock): + """Test the sending of RGBW command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbw_command_template": '{{ "#%02x%02x%02x%02x" | ' + "format(red, green, blue, white)}}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 64, 32]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgbw/set", "#ff804020", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["rgbw_color"] == (255, 128, 64, 32) + + +async def test_sending_mqtt_rgbww_command_with_template(hass, mqtt_mock): + """Test the sending of RGBWW command with template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light_rgb/set", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "rgbww_command_template": '{{ "#%02x%02x%02x%02x%02x" | ' + "format(red, green, blue, cold_white, warm_white)}}", + "payload_on": "on", + "payload_off": "off", + "qos": 0, + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 64, 32, 16]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light_rgb/set", "on", 0, False), + call("test_light_rgb/rgbww/set", "#ff80402010", 0, False), + ], + any_order=True, + ) + + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes["rgbww_color"] == (255, 128, 64, 32, 16) async def test_sending_mqtt_color_temp_command_with_template(hass, mqtt_mock): @@ -1117,6 +1794,97 @@ async def test_on_command_brightness_scaled(hass, mqtt_mock): ) +async def test_legacy_on_command_rgb(hass, mqtt_mock): + """Test on command in RGB brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgb_command_topic": "test_light/rgb", + "white_value_command_topic": "test_light/white_value", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgb: '1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgb_color=[255, 128, 0]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "1,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgb: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgb", "255,128,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_on_command_rgb(hass, mqtt_mock): """Test on command in RGB brightness mode.""" config = { @@ -1207,6 +1975,186 @@ async def test_on_command_rgb(hass, mqtt_mock): mqtt_mock.async_publish.reset_mock() +async def test_on_command_rgbw(hass, mqtt_mock): + """Test on command in RGBW brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbw_command_topic": "test_light/rgbw", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgbw: '127,127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "127,127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbw: '255,255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "255,255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgbw: '1,1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "1,1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgbw_color=[255, 128, 0, 16]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "1,0,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbw: '255,128,0' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "255,128,0,16", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + +async def test_on_command_rgbww(hass, mqtt_mock): + """Test on command in RGBWW brightness mode.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbww_command_topic": "test_light/rgbww", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgbww: '127,127,127,127,127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "127,127,127,127,127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbww: '255,255,255,255,255' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "255,255,255,255,255", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=1) + + # Should get the following MQTT messages. + # test_light/rgbww: '1,1,1,1,1' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "1,1,1,1,1", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + # Ensure color gets scaled with brightness. + await common.async_turn_on(hass, "light.test", rgbww_color=[255, 128, 0, 16, 32]) + + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "1,0,0,0,0", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_on(hass, "light.test", brightness=255) + + # Should get the following MQTT messages. + # test_light/rgbww: '255,128,0,16,32' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "255,128,0,16,32", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + async def test_on_command_rgb_template(hass, mqtt_mock): """Test on command in RGB brightness mode with RGB template.""" config = { @@ -1228,7 +2176,7 @@ async def test_on_command_rgb_template(hass, mqtt_mock): await common.async_turn_on(hass, "light.test", brightness=127) # Should get the following MQTT messages. - # test_light/rgb: '127,127,127' + # test_light/rgb: '127/127/127' # test_light/set: 'ON' mqtt_mock.async_publish.assert_has_calls( [ @@ -1244,6 +2192,311 @@ async def test_on_command_rgb_template(hass, mqtt_mock): mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) +async def test_on_command_rgbw_template(hass, mqtt_mock): + """Test on command in RGBW brightness mode with RGBW template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbw_command_topic": "test_light/rgbw", + "rgbw_command_template": "{{ red }}/{{ green }}/{{ blue }}/{{ white }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbw", "127/127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_on_command_rgbww_template(hass, mqtt_mock): + """Test on command in RGBWW brightness mode with RGBWW template.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "command_topic": "test_light/set", + "rgbww_command_topic": "test_light/rgbww", + "rgbww_command_template": "{{ red }}/{{ green }}/{{ blue }}/{{ cold_white }}/{{ warm_white }}", + } + } + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + await common.async_turn_on(hass, "light.test", brightness=127) + + # Should get the following MQTT messages. + # test_light/rgb: '127/127/127/127/127' + # test_light/set: 'ON' + mqtt_mock.async_publish.assert_has_calls( + [ + call("test_light/rgbww", "127/127/127/127/127", 0, False), + call("test_light/set", "ON", 0, False), + ], + any_order=True, + ) + mqtt_mock.async_publish.reset_mock() + + await common.async_turn_off(hass, "light.test") + + mqtt_mock.async_publish.assert_called_once_with("test_light/set", "OFF", 0, False) + + +async def test_explicit_color_mode(hass, mqtt_mock): + """Test explicit color mode over mqtt.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "rgb_state_topic": "test_light_rgb/rgb/status", + "rgb_command_topic": "test_light_rgb/rgb/set", + "rgbw_state_topic": "test_light_rgb/rgbw/status", + "rgbw_command_topic": "test_light_rgb/rgbw/set", + "rgbww_state_topic": "test_light_rgb/rgbww/status", + "rgbww_command_topic": "test_light_rgb/rgbww/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "effect_state_topic": "test_light_rgb/effect/status", + "effect_command_topic": "test_light_rgb/effect/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "xy_state_topic": "test_light_rgb/xy/status", + "xy_command_topic": "test_light_rgb/xy/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs", "rgb", "rgbw", "rgbww", "xy"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("effect") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get("rgb_color") is None + assert state.attributes.get("rgbw_color") is None + assert state.attributes.get("rgbww_color") is None + assert state.attributes.get("white_value") is None + assert state.attributes.get("xy_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/effect/status", "rainbow") + light_state = hass.states.get("light.test") + assert light_state.attributes["effect"] == "rainbow" + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgb/status", "125,125,125") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbw/status", "80,40,20,10") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/rgbww/status", "80,40,20,10,8") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/xy/status", "0.675,0.322") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "color_temp") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgb") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgb_color") == (125, 125, 125) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgb" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbw") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbw_color") == (80, 40, 20, 10) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbw" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "rgbww") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("rgbww_color") == (80, 40, 20, 10, 8) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "rgbww" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "hs") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_mode/status", "xy") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("xy_color") == (0.675, 0.322) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "xy" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + +async def test_explicit_color_mode_templated(hass, mqtt_mock): + """Test templated explicit color mode over mqtt.""" + config = { + light.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test_light_rgb/status", + "command_topic": "test_light_rgb/set", + "color_mode_state_topic": "test_light_rgb/color_mode/status", + "color_mode_value_template": "{{ value_json.color_mode }}", + "brightness_state_topic": "test_light_rgb/brightness/status", + "brightness_command_topic": "test_light_rgb/brightness/set", + "color_temp_state_topic": "test_light_rgb/color_temp/status", + "color_temp_command_topic": "test_light_rgb/color_temp/set", + "hs_state_topic": "test_light_rgb/hs/status", + "hs_command_topic": "test_light_rgb/hs/set", + "qos": "0", + "payload_on": 1, + "payload_off": 0, + } + } + color_modes = ["color_temp", "hs"] + + assert await async_setup_component(hass, light.DOMAIN, config) + await hass.async_block_till_done() + + state = hass.states.get("light.test") + assert state.state == STATE_OFF + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) is None + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + state = hass.states.get("light.test") + assert state.state == STATE_ON + assert state.attributes.get("brightness") is None + assert state.attributes.get("color_temp") is None + assert state.attributes.get("hs_color") is None + assert state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/status", "0") + state = hass.states.get("light.test") + assert state.state == STATE_OFF + + async_fire_mqtt_message(hass, "test_light_rgb/status", "1") + async_fire_mqtt_message(hass, "test_light_rgb/brightness/status", "100") + light_state = hass.states.get("light.test") + assert light_state.attributes.get("brightness") is None + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/color_temp/status", "300") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message(hass, "test_light_rgb/hs/status", "200,50") + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "unknown" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_mode/status", '{"color_mode":"color_temp"}' + ) + light_state = hass.states.get("light.test") + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "color_temp" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async_fire_mqtt_message( + hass, "test_light_rgb/color_mode/status", '{"color_mode":"hs"}' + ) + light_state = hass.states.get("light.test") + assert light_state.attributes.get("hs_color") == (200, 50) + assert light_state.attributes.get(light.ATTR_COLOR_MODE) == "hs" + assert light_state.attributes.get(light.ATTR_SUPPORTED_COLOR_MODES) == color_modes + + async def test_effect(hass, mqtt_mock): """Test effect.""" config = { diff --git a/tests/components/mqtt/test_light_json.py b/tests/components/mqtt/test_light_json.py index f892b6a3bbd..f4bf11df026 100644 --- a/tests/components/mqtt/test_light_json.py +++ b/tests/components/mqtt/test_light_json.py @@ -945,6 +945,7 @@ async def test_sending_hs_color(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "brightness": True, "hs": True, + "white_value": True, } }, ) @@ -1139,6 +1140,7 @@ async def test_sending_rgb_color_with_brightness(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "brightness": True, "rgb": True, + "white_value": True, } }, ) @@ -1209,6 +1211,7 @@ async def test_sending_rgb_color_with_scaled_brightness(hass, mqtt_mock): "brightness": True, "brightness_scale": 100, "rgb": True, + "white_value": True, } }, ) @@ -1278,6 +1281,7 @@ async def test_sending_xy_color(hass, mqtt_mock): "command_topic": "test_light_rgb/set", "brightness": True, "xy": True, + "white_value": True, } }, ) diff --git a/tests/components/mqtt/test_number.py b/tests/components/mqtt/test_number.py index ac5285e9855..d93b0483865 100644 --- a/tests/components/mqtt/test_number.py +++ b/tests/components/mqtt/test_number.py @@ -5,7 +5,11 @@ from unittest.mock import patch import pytest from homeassistant.components import number +from homeassistant.components.mqtt.number import CONF_MAX, CONF_MIN from homeassistant.components.number import ( + ATTR_MAX, + ATTR_MIN, + ATTR_STEP, ATTR_VALUE, DOMAIN as NUMBER_DOMAIN, SERVICE_SET_VALUE, @@ -357,3 +361,103 @@ async def test_entity_debug_info_message(hass, mqtt_mock): await help_test_entity_debug_info_message( hass, mqtt_mock, number.DOMAIN, DEFAULT_CONFIG, payload=b"1" ) + + +async def test_min_max_step_attributes(hass, mqtt_mock): + """Test min/max/step attributes.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + "step": 20, + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("number.test_number") + assert state.attributes.get(ATTR_MIN) == 5 + assert state.attributes.get(ATTR_MAX) == 110 + assert state.attributes.get(ATTR_STEP) == 20 + + +async def test_invalid_min_max_attributes(hass, caplog, mqtt_mock): + """Test invalid min/max attributes.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 35, + "max": 10, + } + }, + ) + await hass.async_block_till_done() + + assert f"'{CONF_MAX}'' must be > '{CONF_MIN}'" in caplog.text + + +async def test_mqtt_payload_not_a_number_warning(hass, caplog, mqtt_mock): + """Test warning for MQTT payload which is not a number.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "not_a_number") + + await hass.async_block_till_done() + + assert "Payload 'not_a_number' is not a Number" in caplog.text + + +async def test_mqtt_payload_out_of_range_error(hass, caplog, mqtt_mock): + """Test error when MQTT payload is out of min/max range.""" + topic = "test/number" + await async_setup_component( + hass, + "number", + { + "number": { + "platform": "mqtt", + "state_topic": topic, + "command_topic": topic, + "name": "Test Number", + "min": 5, + "max": 110, + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, topic, "115.5") + + await hass.async_block_till_done() + + assert ( + "Invalid value for number.test_number: 115.5 (range 5.0 - 110.0)" in caplog.text + ) diff --git a/tests/components/mqtt/test_sensor.py b/tests/components/mqtt/test_sensor.py index 373048f6f1a..7d732849906 100644 --- a/tests/components/mqtt/test_sensor.py +++ b/tests/components/mqtt/test_sensor.py @@ -206,6 +206,104 @@ async def test_setting_sensor_value_via_mqtt_json_message(hass, mqtt_mock): assert state.state == "100" +async def test_setting_sensor_last_reset_via_mqtt_message(hass, mqtt_mock): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "last-reset-topic", "2020-01-02 08:11:00") + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") == "2020-01-02T08:11:00" + + +@pytest.mark.parametrize("datestring", ["2020-21-02 08:11:00", "Hello there!"]) +async def test_setting_sensor_bad_last_reset_via_mqtt_message( + hass, caplog, datestring, mqtt_mock +): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "last-reset-topic", datestring) + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") is None + assert "Invalid last_reset message" in caplog.text + + +async def test_setting_sensor_empty_last_reset_via_mqtt_message( + hass, caplog, mqtt_mock +): + """Test the setting of the last_reset property via MQTT.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "last-reset-topic", "") + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") is None + assert "Ignoring empty last_reset message" in caplog.text + + +async def test_setting_sensor_last_reset_via_mqtt_json_message(hass, mqtt_mock): + """Test the setting of the value via MQTT with JSON payload.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "last_reset_topic": "last-reset-topic", + "last_reset_value_template": "{{ value_json.last_reset }}", + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message( + hass, "last-reset-topic", '{ "last_reset": "2020-01-02 08:11:00" }' + ) + state = hass.states.get("sensor.test") + assert state.attributes.get("last_reset") == "2020-01-02T08:11:00" + + async def test_force_update_disabled(hass, mqtt_mock): """Test force update option.""" assert await async_setup_component( @@ -381,6 +479,51 @@ async def test_valid_device_class(hass, mqtt_mock): assert "device_class" not in state.attributes +async def test_invalid_state_class(hass, mqtt_mock): + """Test state_class option with invalid value.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "state_class": "foobarnotreal", + } + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test") + assert state is None + + +async def test_valid_state_class(hass, mqtt_mock): + """Test state_class option with valid values.""" + assert await async_setup_component( + hass, + "sensor", + { + "sensor": [ + { + "platform": "mqtt", + "name": "Test 1", + "state_topic": "test-topic", + "state_class": "measurement", + }, + {"platform": "mqtt", "name": "Test 2", "state_topic": "test-topic"}, + ] + }, + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.test_1") + assert state.attributes["state_class"] == "measurement" + state = hass.states.get("sensor.test_2") + assert "state_class" not in state.attributes + + async def test_setting_attribute_via_mqtt_json_message(hass, mqtt_mock): """Test the setting of attribute via MQTT with JSON payload.""" await help_test_setting_attribute_via_mqtt_json_message( @@ -640,3 +783,31 @@ async def test_entity_disabled_by_default(hass, mqtt_mock): await help_test_entity_disabled_by_default( hass, mqtt_mock, sensor.DOMAIN, DEFAULT_CONFIG ) + + +async def test_value_template_with_entity_id(hass, mqtt_mock): + """Test the access to attributes in value_template via the entity_id.""" + assert await async_setup_component( + hass, + sensor.DOMAIN, + { + sensor.DOMAIN: { + "platform": "mqtt", + "name": "test", + "state_topic": "test-topic", + "unit_of_measurement": "fav unit", + "value_template": '\ + {% if state_attr(entity_id, "friendly_name") == "test" %} \ + {{ value | int + 1 }} \ + {% else %} \ + {{ value }} \ + {% endif %}', + } + }, + ) + await hass.async_block_till_done() + + async_fire_mqtt_message(hass, "test-topic", "100") + state = hass.states.get("sensor.test") + + assert state.state == "101" diff --git a/tests/components/mqtt/test_tag.py b/tests/components/mqtt/test_tag.py index 98dcfa050e5..5e518c561b3 100644 --- a/tests/components/mqtt/test_tag.py +++ b/tests/components/mqtt/test_tag.py @@ -386,7 +386,7 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): { "topic": "test-topic", "device": { - "connections": [["mac", "02:5b:26:a8:dc:12"]], + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", @@ -397,9 +397,11 @@ async def test_entity_device_info_with_connection(hass, mqtt_mock): async_fire_mqtt_message(hass, "homeassistant/tag/bla/config", data) await hass.async_block_till_done() - device = registry.async_get_device(set(), {("mac", "02:5b:26:a8:dc:12")}) + device = registry.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} + ) assert device is not None - assert device.connections == {("mac", "02:5b:26:a8:dc:12")} + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12")} assert device.manufacturer == "Whatever" assert device.name == "Beer" assert device.model == "Glass" @@ -442,7 +444,7 @@ async def test_entity_device_info_update(hass, mqtt_mock): "topic": "test-topic", "device": { "identifiers": ["helloworld"], - "connections": [["mac", "02:5b:26:a8:dc:12"]], + "connections": [[dr.CONNECTION_NETWORK_MAC, "02:5b:26:a8:dc:12"]], "manufacturer": "Whatever", "name": "Beer", "model": "Glass", diff --git a/tests/components/myq/test_config_flow.py b/tests/components/myq/test_config_flow.py index 3ae2da82f46..3b0d79f6f03 100644 --- a/tests/components/myq/test_config_flow.py +++ b/tests/components/myq/test_config_flow.py @@ -57,7 +57,7 @@ async def test_form_invalid_auth(hass): ) assert result2["type"] == "form" - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {"password": "invalid_auth"} async def test_form_cannot_connect(hass): @@ -79,32 +79,87 @@ async def test_form_cannot_connect(hass): assert result2["errors"] == {"base": "cannot_connect"} -async def test_form_homekit(hass): - """Test that we abort from homekit if myq is already setup.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - +async def test_form_unknown_exception(hass): + """Test we handle unknown exceptions.""" result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + DOMAIN, context={"source": config_entries.SOURCE_USER} ) - assert result["type"] == "form" - assert result["errors"] == {} - flow = next( - flow - for flow in hass.config_entries.flow.async_progress() - if flow["flow_id"] == result["flow_id"] - ) - assert flow["context"]["unique_id"] == "AA:BB:CC:DD:EE:FF" + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"username": "test-username", "password": "test-password"}, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth(hass): + """Test we can reauth.""" entry = MockConfigEntry( - domain=DOMAIN, data={CONF_USERNAME: "mock", CONF_PASSWORD: "mock"} + domain=DOMAIN, + data={ + CONF_USERNAME: "test@test.org", + CONF_PASSWORD: "secret", + }, + unique_id="test@test.org", ) entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, - context={"source": config_entries.SOURCE_HOMEKIT}, - data={"properties": {"id": "AA:BB:CC:DD:EE:FF"}}, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": "test@test.org"}, ) - assert result["type"] == "abort" + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=InvalidCredentialsError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"password": "invalid_auth"} + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + side_effect=MyQError, + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + + with patch( + "homeassistant.components.myq.config_flow.pymyq.login", + return_value=True, + ), patch( + "homeassistant.components.myq.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_PASSWORD: "test-password", + }, + ) + + assert mock_setup_entry.called + assert result4["type"] == "abort" + assert result4["reason"] == "reauth_successful" diff --git a/tests/components/nam/__init__.py b/tests/components/nam/__init__.py new file mode 100644 index 00000000000..b4a6ebbf792 --- /dev/null +++ b/tests/components/nam/__init__.py @@ -0,0 +1,61 @@ +"""Tests for the Nettigo Air Monitor integration.""" +from unittest.mock import patch + +from homeassistant.components.nam.const import DOMAIN + +from tests.common import MockConfigEntry + +INCOMPLETE_NAM_DATA = { + "software_version": "NAMF-2020-36", + "sensordatavalues": [], +} + +nam_data = { + "software_version": "NAMF-2020-36", + "uptime": "456987", + "sensordatavalues": [ + {"value_type": "SDS_P1", "value": "18.65"}, + {"value_type": "SDS_P2", "value": "11.03"}, + {"value_type": "SPS30_P0", "value": "31.23"}, + {"value_type": "SPS30_P1", "value": "21.23"}, + {"value_type": "SPS30_P2", "value": "34.32"}, + {"value_type": "SPS30_P4", "value": "24.72"}, + {"value_type": "conc_co2_ppm", "value": "865"}, + {"value_type": "BME280_temperature", "value": "7.56"}, + {"value_type": "BME280_humidity", "value": "45.69"}, + {"value_type": "BME280_pressure", "value": "101101.17"}, + {"value_type": "BMP280_temperature", "value": "5.56"}, + {"value_type": "BMP280_pressure", "value": "102201.18"}, + {"value_type": "SHT3X_temperature", "value": "6.28"}, + {"value_type": "SHT3X_humidity", "value": "34.69"}, + {"value_type": "humidity", "value": "46.23"}, + {"value_type": "temperature", "value": "6.26"}, + {"value_type": "HECA_temperature", "value": "7.95"}, + {"value_type": "HECA_humidity", "value": "49.97"}, + {"value_type": "signal", "value": "-72"}, + ], +} + + +async def init_integration(hass, co2_sensor=True) -> MockConfigEntry: + """Set up the Nettigo Air Monitor integration in Home Assistant.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + + if not co2_sensor: + # Remove conc_co2_ppm value + nam_data["sensordatavalues"].pop(6) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + return entry diff --git a/tests/components/nam/test_air_quality.py b/tests/components/nam/test_air_quality.py new file mode 100644 index 00000000000..f9a213cec3e --- /dev/null +++ b/tests/components/nam/test_air_quality.py @@ -0,0 +1,148 @@ +"""Test air_quality of Nettigo Air Monitor integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nettigo_air_monitor import ApiError + +from homeassistant.components.air_quality import ATTR_CO2, ATTR_PM_2_5, ATTR_PM_10 +from homeassistant.const import ( + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, + STATE_UNAVAILABLE, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import INCOMPLETE_NAM_DATA, nam_data + +from tests.common import async_fire_time_changed +from tests.components.nam import init_integration + + +async def test_air_quality(hass): + """Test states of the air_quality.""" + await init_integration(hass) + registry = er.async_get(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_PM_10) == 19 + assert state.attributes.get(ATTR_PM_2_5) == 11 + assert state.attributes.get(ATTR_CO2) == 865 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sds011") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sds" + + state = hass.states.get("air_quality.nettigo_air_monitor_sps30") + assert state + assert state.state == "34" + assert state.attributes.get(ATTR_PM_10) == 21 + assert state.attributes.get(ATTR_PM_2_5) == 34 + assert state.attributes.get(ATTR_CO2) == 865 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + entry = registry.async_get("air_quality.nettigo_air_monitor_sps30") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sps30" + + +async def test_air_quality_without_co2_value(hass): + """Test states of the air_quality.""" + await init_integration(hass, co2_sensor=False) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.attributes.get(ATTR_CO2) is None + + +async def test_incompleta_data_after_device_restart(hass): + """Test states of the air_quality after device restart.""" + await init_integration(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == "11" + assert state.attributes.get(ATTR_PM_10) == 19 + assert state.attributes.get(ATTR_PM_2_5) == 11 + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == CONCENTRATION_MICROGRAMS_PER_CUBIC_METER + ) + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=INCOMPLETE_NAM_DATA, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when device causes an error.""" + await init_integration(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "11" + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=12) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "11" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ) as mock_get_data: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["air_quality.nettigo_air_monitor_sds011"]}, + blocking=True, + ) + + assert mock_get_data.call_count == 1 diff --git a/tests/components/nam/test_config_flow.py b/tests/components/nam/test_config_flow.py new file mode 100644 index 00000000000..99a252ada0a --- /dev/null +++ b/tests/components/nam/test_config_flow.py @@ -0,0 +1,175 @@ +"""Define tests for the Nettigo Air Monitor config flow.""" +import asyncio +from unittest.mock import patch + +from nettigo_air_monitor import ApiError, CannotGetMac +import pytest + +from homeassistant import data_entry_flow +from homeassistant.components.nam.const import DOMAIN +from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF + +from tests.common import MockConfigEntry + +DISCOVERY_INFO = {"host": "10.10.2.3", "name": "NAM-12345"} +VALID_CONFIG = {"host": "10.10.2.3"} + + +async def test_form_create_entry(hass): + """Test that the user step works.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == SOURCE_USER + assert result["errors"] == {} + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ), patch( + "homeassistant.components.nam.async_setup_entry", return_value=True + ) as mock_setup_entry: + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + VALID_CONFIG, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.2.3" + assert result["data"]["host"] == "10.10.2.3" + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (ValueError, "unknown"), + ], +) +async def test_form_errors(hass, error): + """Test we handle errors.""" + exc, base_error = error + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=exc, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["errors"] == {"base": base_error} + + +async def test_form_abort(hass): + """Test we handle abort after error.""" + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=CannotGetMac("Cannot get MAC address from device"), + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data=VALID_CONFIG, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "device_unsupported" + + +async def test_form_already_configured(hass): + """Test that errors are shown when duplicates are added.""" + entry = MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=VALID_CONFIG + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"host": "1.1.1.1"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + # Test config entry got updated with latest IP + assert entry.data["host"] == "1.1.1.1" + + +async def test_zeroconf(hass): + """Test we get the form.""" + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + return_value="aa:bb:cc:dd:ee:ff", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": SOURCE_ZEROCONF}, + ) + context = next( + flow["context"] + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert context["title_placeholders"]["name"] == "NAM-12345" + assert context["confirm_only"] is True + + with patch( + "homeassistant.components.nam.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "10.10.2.3" + assert result["data"] == {"host": "10.10.2.3"} + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "error", + [ + (ApiError("Invalid response from device 10.10.2.3: 404"), "cannot_connect"), + (CannotGetMac("Cannot get MAC address from device"), "device_unsupported"), + ], +) +async def test_zeroconf_errors(hass, error): + """Test we handle errors.""" + exc, reason = error + with patch( + "homeassistant.components.nam.NettigoAirMonitor.async_get_mac_address", + side_effect=exc, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + data=DISCOVERY_INFO, + context={"source": SOURCE_ZEROCONF}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == reason diff --git a/tests/components/nam/test_init.py b/tests/components/nam/test_init.py new file mode 100644 index 00000000000..943ea53f360 --- /dev/null +++ b/tests/components/nam/test_init.py @@ -0,0 +1,53 @@ +"""Test init of Nettigo Air Monitor integration.""" +from unittest.mock import patch + +from nettigo_air_monitor import ApiError + +from homeassistant.components.nam.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import STATE_UNAVAILABLE + +from tests.common import MockConfigEntry +from tests.components.nam import init_integration + + +async def test_async_setup_entry(hass): + """Test a successful setup entry.""" + await init_integration(hass) + + state = hass.states.get("air_quality.nettigo_air_monitor_sds011") + assert state is not None + assert state.state != STATE_UNAVAILABLE + assert state.state == "11" + + +async def test_config_not_ready(hass): + """Test for setup failure if the connection to the device fails.""" + entry = MockConfigEntry( + domain=DOMAIN, + title="10.10.2.3", + unique_id="aa:bb:cc:dd:ee:ff", + data={"host": "10.10.2.3"}, + ) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + side_effect=ApiError("API Error"), + ): + entry.add_to_hass(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_unload_entry(hass): + """Test successful unload of entry.""" + entry = await init_integration(hass) + + assert len(hass.config_entries.async_entries(DOMAIN)) == 1 + assert entry.state is ConfigEntryState.LOADED + + assert await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.NOT_LOADED + assert not hass.data.get(DOMAIN) diff --git a/tests/components/nam/test_sensor.py b/tests/components/nam/test_sensor.py new file mode 100644 index 00000000000..b4c92c92e67 --- /dev/null +++ b/tests/components/nam/test_sensor.py @@ -0,0 +1,304 @@ +"""Test sensor of Nettigo Air Monitor integration.""" +from datetime import timedelta +from unittest.mock import patch + +from nettigo_air_monitor import ApiError + +from homeassistant.components.nam.const import DOMAIN +from homeassistant.components.sensor import ( + ATTR_STATE_CLASS, + DOMAIN as SENSOR_DOMAIN, + STATE_CLASS_MEASUREMENT, +) +from homeassistant.const import ( + ATTR_DEVICE_CLASS, + ATTR_ENTITY_ID, + ATTR_UNIT_OF_MEASUREMENT, + DEVICE_CLASS_HUMIDITY, + DEVICE_CLASS_PRESSURE, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TEMPERATURE, + DEVICE_CLASS_TIMESTAMP, + PERCENTAGE, + PRESSURE_HPA, + SIGNAL_STRENGTH_DECIBELS_MILLIWATT, + STATE_UNAVAILABLE, + TEMP_CELSIUS, +) +from homeassistant.helpers import entity_registry as er +from homeassistant.setup import async_setup_component +from homeassistant.util.dt import utcnow + +from . import INCOMPLETE_NAM_DATA, nam_data + +from tests.common import async_fire_time_changed +from tests.components.nam import init_integration + + +async def test_sensor(hass): + """Test states of the air_quality.""" + registry = er.async_get(hass) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-signal", + suggested_object_id="nettigo_air_monitor_signal_strength", + disabled_by=None, + ) + + registry.async_get_or_create( + SENSOR_DOMAIN, + DOMAIN, + "aa:bb:cc:dd:ee:ff-uptime", + suggested_object_id="nettigo_air_monitor_uptime", + disabled_by=None, + ) + + await init_integration(hass) + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_humidity") + assert state + assert state.state == "45.7" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_bme280_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state == "7.6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_bme280_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_pressure") + assert state + assert state.state == "1011" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + + entry = registry.async_get("sensor.nettigo_air_monitor_bme280_pressure") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bme280_pressure" + + state = hass.states.get("sensor.nettigo_air_monitor_bmp280_temperature") + assert state + assert state.state == "5.6" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_bmp280_pressure") + assert state + assert state.state == "1022" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_PRESSURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PRESSURE_HPA + + entry = registry.async_get("sensor.nettigo_air_monitor_bmp280_pressure") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-bmp280_pressure" + + state = hass.states.get("sensor.nettigo_air_monitor_sht3x_humidity") + assert state + assert state.state == "34.7" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_sht3x_temperature") + assert state + assert state.state == "6.3" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_sht3x_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-sht3x_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_dht22_humidity") + assert state + assert state.state == "46.2" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_dht22_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_dht22_temperature") + assert state + assert state.state == "6.3" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_dht22_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_heca_humidity") + assert state + assert state.state == "50.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_HUMIDITY + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == PERCENTAGE + + entry = registry.async_get("sensor.nettigo_air_monitor_heca_humidity") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_humidity" + + state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") + assert state + assert state.state == "8.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + entry = registry.async_get("sensor.nettigo_air_monitor_heca_temperature") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-heca_temperature" + + state = hass.states.get("sensor.nettigo_air_monitor_signal_strength") + assert state + assert state.state == "-72" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert ( + state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) + == SIGNAL_STRENGTH_DECIBELS_MILLIWATT + ) + + entry = registry.async_get("sensor.nettigo_air_monitor_signal_strength") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" + + state = hass.states.get("sensor.nettigo_air_monitor_uptime") + assert state + assert ( + state.state + == (utcnow() - timedelta(seconds=456987)).replace(microsecond=0).isoformat() + ) + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP + assert state.attributes.get(ATTR_STATE_CLASS) is None + + entry = registry.async_get("sensor.nettigo_air_monitor_uptime") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-uptime" + + +async def test_sensor_disabled(hass): + """Test sensor disabled by default.""" + await init_integration(hass) + registry = er.async_get(hass) + + entry = registry.async_get("sensor.nettigo_air_monitor_signal_strength") + assert entry + assert entry.unique_id == "aa:bb:cc:dd:ee:ff-signal" + assert entry.disabled + assert entry.disabled_by == er.DISABLED_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_incompleta_data_after_device_restart(hass): + """Test states of the air_quality after device restart.""" + await init_integration(hass) + + state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") + assert state + assert state.state == "8.0" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TEMPERATURE + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == TEMP_CELSIUS + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=INCOMPLETE_NAM_DATA, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.nettigo_air_monitor_heca_temperature") + assert state + assert state.state == STATE_UNAVAILABLE + + +async def test_availability(hass): + """Ensure that we mark the entities unavailable correctly when device causes an error.""" + await init_integration(hass) + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "7.6" + + future = utcnow() + timedelta(minutes=6) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + side_effect=ApiError("API Error"), + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state == STATE_UNAVAILABLE + + future = utcnow() + timedelta(minutes=12) + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ): + async_fire_time_changed(hass, future) + await hass.async_block_till_done() + + state = hass.states.get("sensor.nettigo_air_monitor_bme280_temperature") + assert state + assert state.state != STATE_UNAVAILABLE + assert state.state == "7.6" + + +async def test_manual_update_entity(hass): + """Test manual update entity via service homeasasistant/update_entity.""" + await init_integration(hass) + + await async_setup_component(hass, "homeassistant", {}) + + with patch( + "homeassistant.components.nam.NettigoAirMonitor._async_get_data", + return_value=nam_data, + ) as mock_get_data: + await hass.services.async_call( + "homeassistant", + "update_entity", + {ATTR_ENTITY_ID: ["sensor.nettigo_air_monitor_bme280_temperature"]}, + blocking=True, + ) + + assert mock_get_data.call_count == 1 diff --git a/tests/components/neato/test_config_flow.py b/tests/components/neato/test_config_flow.py index 3650dae8a5c..3f07e4c4b0a 100644 --- a/tests/components/neato/test_config_flow.py +++ b/tests/components/neato/test_config_flow.py @@ -152,6 +152,6 @@ async def test_reauth( assert result3["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result3["reason"] == "reauth_successful" - assert new_entry.state == "loaded" + assert new_entry.state == config_entries.ConfigEntryState.LOADED assert len(hass.config_entries.async_entries(NEATO_DOMAIN)) == 1 assert len(mock_setup.mock_calls) == 1 diff --git a/tests/components/nest/test_init_sdm.py b/tests/components/nest/test_init_sdm.py index 27bc02e3ea8..db5e0c2fc33 100644 --- a/tests/components/nest/test_init_sdm.py +++ b/tests/components/nest/test_init_sdm.py @@ -11,12 +11,7 @@ from unittest.mock import patch from google_nest_sdm.exceptions import AuthException, GoogleNestException from homeassistant.components.nest import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from .common import CONFIG, async_setup_sdm_platform, create_config_entry @@ -32,7 +27,7 @@ async def test_setup_success(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_LOADED + assert entries[0].state is ConfigEntryState.LOADED async def async_setup_sdm(hass, config=CONFIG): @@ -54,7 +49,7 @@ async def test_setup_configuration_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR # This error comes from the python google-nest-sdm library, as a check added # to prevent common misconfigurations (e.g. confusing topic and subscriber) @@ -73,7 +68,7 @@ async def test_setup_susbcriber_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY async def test_setup_device_manager_failure(hass, caplog): @@ -89,7 +84,7 @@ async def test_setup_device_manager_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_RETRY + assert entries[0].state is ConfigEntryState.SETUP_RETRY async def test_subscriber_auth_failure(hass, caplog): @@ -103,7 +98,7 @@ async def test_subscriber_auth_failure(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_SETUP_ERROR + assert entries[0].state is ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -121,7 +116,7 @@ async def test_setup_missing_subscriber_id(hass, caplog): entries = hass.config_entries.async_entries(DOMAIN) assert len(entries) == 1 - assert entries[0].state == ENTRY_STATE_NOT_LOADED + assert entries[0].state is ConfigEntryState.NOT_LOADED async def test_empty_config(hass, caplog): diff --git a/tests/components/netatmo/common.py b/tests/components/netatmo/common.py index 54e7610c4e5..32202cb85e5 100644 --- a/tests/components/netatmo/common.py +++ b/tests/components/netatmo/common.py @@ -1,5 +1,7 @@ """Common methods used across tests for Netatmo.""" +from contextlib import contextmanager import json +from unittest.mock import patch from homeassistant.components.webhook import async_handle_webhook from homeassistant.util.aiohttp import MockRequest @@ -35,13 +37,19 @@ FAKE_WEBHOOK_ACTIVATION = { "push_type": "webhook_activation", } +DEFAULT_PLATFORMS = ["camera", "climate", "light", "sensor"] -def fake_post_request(**args): + +async def fake_post_request(*args, **kwargs): """Return fake data.""" - if "url" not in args: + if "url" not in kwargs: return "{}" - endpoint = args["url"].split("/")[-1] + endpoint = kwargs["url"].split("/")[-1] + + if endpoint in "snapshot_720.jpg": + return b"test stream image bytes" + if endpoint in [ "setpersonsaway", "setpersonshome", @@ -55,7 +63,7 @@ def fake_post_request(**args): return json.loads(load_fixture(f"netatmo/{endpoint}.json")) -def fake_post_request_no_data(**args): +async def fake_post_request_no_data(*args, **kwargs): """Fake error during requesting backend data.""" return "{}" @@ -68,3 +76,12 @@ async def simulate_webhook(hass, webhook_id, response): ) await async_handle_webhook(hass, webhook_id, request) await hass.async_block_till_done() + + +@contextmanager +def selected_platforms(platforms): + """Restrict loaded platforms to list given.""" + with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch("homeassistant.components.webhook.async_generate_url"): + yield diff --git a/tests/components/netatmo/conftest.py b/tests/components/netatmo/conftest.py index 9a16391d2a4..d443802a41d 100644 --- a/tests/components/netatmo/conftest.py +++ b/tests/components/netatmo/conftest.py @@ -1,17 +1,16 @@ """Provide common Netatmo fixtures.""" -from contextlib import contextmanager from time import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch import pytest -from .common import ALL_SCOPES, TEST_TIME, fake_post_request, fake_post_request_no_data +from .common import ALL_SCOPES, fake_post_request from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -async def mock_config_entry_fixture(hass): +def mock_config_entry_fixture(hass): """Mock a config entry.""" mock_entry = MockConfigEntry( domain="netatmo", @@ -54,81 +53,13 @@ async def mock_config_entry_fixture(hass): return mock_entry -@contextmanager -def selected_platforms(platforms=["camera", "climate", "light", "sensor"]): +@pytest.fixture +def netatmo_auth(): """Restrict loaded platforms to list given.""" - with patch("homeassistant.components.netatmo.PLATFORMS", platforms), patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.webhook.async_generate_url" - ): - mock_auth.return_value.post_request.side_effect = fake_post_request - yield - - -@pytest.fixture(name="entry") -async def mock_entry_fixture(hass, config_entry): - """Mock setup of all platforms.""" - with selected_platforms(): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="sensor_entry") -async def mock_sensor_entry_fixture(hass, config_entry): - """Mock setup of sensor platform.""" - with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): - await hass.config_entries.async_setup(config_entry.entry_id) - await hass.async_block_till_done() - yield config_entry - - -@pytest.fixture(name="camera_entry") -async def mock_camera_entry_fixture(hass, config_entry): - """Mock setup of camera platform.""" - with selected_platforms(["camera"]): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="light_entry") -async def mock_light_entry_fixture(hass, config_entry): - """Mock setup of light platform.""" - with selected_platforms(["light"]): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="climate_entry") -async def mock_climate_entry_fixture(hass, config_entry): - """Mock setup of climate platform.""" - with selected_platforms(["climate"]): - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - return config_entry - - -@pytest.fixture(name="entry_error") -async def mock_entry_error_fixture(hass, config_entry): - """Mock erroneous setup of platforms.""" with patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" - ) as mock_auth, patch( - "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", - ), patch( - "homeassistant.components.webhook.async_generate_url" - ): - mock_auth.return_value.post_request.side_effect = fake_post_request_no_data - await hass.config_entries.async_setup(config_entry.entry_id) - - await hass.async_block_till_done() - yield config_entry + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth: + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + yield diff --git a/tests/components/netatmo/test_camera.py b/tests/components/netatmo/test_camera.py index 372af748267..4825946beab 100644 --- a/tests/components/netatmo/test_camera.py +++ b/tests/components/netatmo/test_camera.py @@ -1,6 +1,9 @@ """The tests for Netatmo camera.""" from datetime import timedelta -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +import pyatmo +import pytest from homeassistant.components import camera from homeassistant.components.camera import STATE_STREAMING @@ -13,14 +16,19 @@ from homeassistant.components.netatmo.const import ( from homeassistant.const import CONF_WEBHOOK_ID from homeassistant.util import dt -from .common import fake_post_request, simulate_webhook +from .common import fake_post_request, selected_platforms, simulate_webhook from tests.common import async_capture_events, async_fire_time_changed -async def test_setup_component_with_webhook(hass, camera_entry): +async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup with webhook.""" - webhook_id = camera_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await hass.async_block_till_done() camera_entity_indoor = "camera.netatmo_hall" @@ -58,7 +66,7 @@ async def test_setup_component_with_webhook(hass, camera_entry): } await simulate_webhook(hass, webhook_id, response) - assert hass.states.get(camera_entity_indoor).state == "streaming" + assert hass.states.get(camera_entity_outdoor).state == "streaming" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "on" response = { @@ -84,12 +92,39 @@ async def test_setup_component_with_webhook(hass, camera_entry): assert hass.states.get(camera_entity_indoor).state == "streaming" assert hass.states.get(camera_entity_outdoor).attributes["light_state"] == "auto" + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + await hass.services.async_call( + "camera", "turn_off", service_data={"entity_id": "camera.netatmo_hall"} + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + home_id="91763b24c43d3e344f424e8b", + camera_id="12:34:56:00:f1:62", + monitoring="off", + ) + + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: + await hass.services.async_call( + "camera", "turn_on", service_data={"entity_id": "camera.netatmo_hall"} + ) + await hass.async_block_till_done() + mock_set_state.assert_called_once_with( + home_id="91763b24c43d3e344f424e8b", + camera_id="12:34:56:00:f1:62", + monitoring="on", + ) + IMAGE_BYTES_FROM_STREAM = b"test stream image bytes" -async def test_camera_image_local(hass, camera_entry, requests_mock): +async def test_camera_image_local(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval or local camera image.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() uri = "http://192.168.0.123/678460a0d47e5618699fb31169e2b47d" @@ -111,8 +146,13 @@ async def test_camera_image_local(hass, camera_entry, requests_mock): assert image.content == IMAGE_BYTES_FROM_STREAM -async def test_camera_image_vpn(hass, camera_entry, requests_mock): +async def test_camera_image_vpn(hass, config_entry, requests_mock, netatmo_auth): """Test retrieval of remote camera image.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() uri = ( @@ -137,8 +177,13 @@ async def test_camera_image_vpn(hass, camera_entry, requests_mock): assert image.content == IMAGE_BYTES_FROM_STREAM -async def test_service_set_person_away(hass, camera_entry): +async def test_service_set_person_away(hass, config_entry, netatmo_auth): """Test service to set person as away.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() data = { @@ -146,7 +191,9 @@ async def test_service_set_person_away(hass, camera_entry): "person": "Richard Doe", } - with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away: + with patch( + "pyatmo.camera.AsyncCameraData.async_set_persons_away" + ) as mock_set_persons_away: await hass.services.async_call( "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data ) @@ -160,7 +207,9 @@ async def test_service_set_person_away(hass, camera_entry): "entity_id": "camera.netatmo_hall", } - with patch("pyatmo.camera.CameraData.set_persons_away") as mock_set_persons_away: + with patch( + "pyatmo.camera.AsyncCameraData.async_set_persons_away" + ) as mock_set_persons_away: await hass.services.async_call( "netatmo", SERVICE_SET_PERSON_AWAY, service_data=data ) @@ -171,8 +220,13 @@ async def test_service_set_person_away(hass, camera_entry): ) -async def test_service_set_persons_home(hass, camera_entry): +async def test_service_set_persons_home(hass, config_entry, netatmo_auth): """Test service to set persons as home.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() data = { @@ -180,7 +234,9 @@ async def test_service_set_persons_home(hass, camera_entry): "persons": "John Doe", } - with patch("pyatmo.camera.CameraData.set_persons_home") as mock_set_persons_home: + with patch( + "pyatmo.camera.AsyncCameraData.async_set_persons_home" + ) as mock_set_persons_home: await hass.services.async_call( "netatmo", SERVICE_SET_PERSONS_HOME, service_data=data ) @@ -191,8 +247,13 @@ async def test_service_set_persons_home(hass, camera_entry): ) -async def test_service_set_camera_light(hass, camera_entry): +async def test_service_set_camera_light(hass, config_entry, netatmo_auth): """Test service to set the outdoor camera light mode.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.async_block_till_done() data = { @@ -200,7 +261,7 @@ async def test_service_set_camera_light(hass, camera_entry): "camera_light_mode": "on", } - with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( "netatmo", SERVICE_SET_CAMERA_LIGHT, service_data=data ) @@ -214,16 +275,26 @@ async def test_service_set_camera_light(hass, camera_entry): async def test_camera_reconnect_webhook(hass, config_entry): """Test webhook event on camera reconnect.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(*args, **kwargs) + with patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth.post_request" - ) as mock_post, patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( "homeassistant.components.netatmo.PLATFORMS", ["camera"] ), patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( "homeassistant.components.webhook.async_generate_url" ) as mock_webhook: - mock_post.side_effect = fake_post_request + mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() mock_webhook.return_value = "https://example.com" await hass.config_entries.async_setup(config_entry.entry_id) @@ -238,8 +309,9 @@ async def test_camera_reconnect_webhook(hass, config_entry): await simulate_webhook(hass, webhook_id, response) await hass.async_block_till_done() - mock_post.assert_called() - mock_post.reset_mock() + assert fake_post_hits == 5 + + calls = fake_post_hits # Fake camera reconnect response = { @@ -253,11 +325,16 @@ async def test_camera_reconnect_webhook(hass, config_entry): dt.utcnow() + timedelta(seconds=60), ) await hass.async_block_till_done() - mock_post.assert_called() + assert fake_post_hits > calls -async def test_webhook_person_event(hass, camera_entry): +async def test_webhook_person_event(hass, config_entry, netatmo_auth): """Test that person events are handled.""" + with selected_platforms(["camera"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + test_netatmo_event = async_capture_events(hass, NETATMO_EVENT) assert not test_netatmo_event @@ -282,7 +359,80 @@ async def test_webhook_person_event(hass, camera_entry): "push_type": "NACamera-person", } - webhook_id = camera_entry.data[CONF_WEBHOOK_ID] + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await simulate_webhook(hass, webhook_id, fake_webhook_event) assert test_netatmo_event + + +async def test_setup_component_no_devices(hass, config_entry): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post_no_data(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return "{}" + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", ["camera"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = fake_post_no_data + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert fake_post_hits == 1 + + +async def test_camera_image_raises_exception(hass, config_entry, requests_mock): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Return fake data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + + if "url" not in kwargs: + return "{}" + + endpoint = kwargs["url"].split("/")[-1] + + if "snapshot_720.jpg" in endpoint: + raise pyatmo.exceptions.ApiError() + + return await fake_post_request(*args, **kwargs) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", ["camera"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + camera_entity_indoor = "camera.netatmo_hall" + + with pytest.raises(Exception) as excinfo: + await camera.async_get_image(hass, camera_entity_indoor) + + assert excinfo.value.args == ("Unable to get image",) + assert fake_post_hits == 6 diff --git a/tests/components/netatmo/test_climate.py b/tests/components/netatmo/test_climate.py index 16359c85498..ef7f8884e2e 100644 --- a/tests/components/netatmo/test_climate.py +++ b/tests/components/netatmo/test_climate.py @@ -26,12 +26,17 @@ from homeassistant.components.netatmo.const import ( ) from homeassistant.const import ATTR_ENTITY_ID, ATTR_TEMPERATURE, CONF_WEBHOOK_ID -from .common import simulate_webhook +from .common import selected_platforms, simulate_webhook -async def test_webhook_event_handling_thermostats(hass, climate_entry): +async def test_webhook_event_handling_thermostats(hass, config_entry, netatmo_auth): """Test service and webhook event handling with thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" @@ -199,9 +204,16 @@ async def test_webhook_event_handling_thermostats(hass, climate_entry): ) -async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry): +async def test_service_preset_mode_frost_guard_thermostat( + hass, config_entry, netatmo_auth +): """Test service with frost guard preset for thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" @@ -267,9 +279,14 @@ async def test_service_preset_mode_frost_guard_thermostat(hass, climate_entry): ) -async def test_service_preset_modes_thermostat(hass, climate_entry): +async def test_service_preset_modes_thermostat(hass, config_entry, netatmo_auth): """Test service with preset modes for thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" assert hass.states.get(climate_entity_livingroom).state == "auto" @@ -341,10 +358,15 @@ async def test_service_preset_modes_thermostat(hass, climate_entry): assert hass.states.get(climate_entity_livingroom).attributes["temperature"] == 30 -async def test_webhook_event_handling_no_data(hass, climate_entry): +async def test_webhook_event_handling_no_data(hass, config_entry, netatmo_auth): """Test service and webhook event handling with erroneous data.""" + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + # Test webhook without home entry - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + webhook_id = config_entry.data[CONF_WEBHOOK_ID] response = { "push_type": "home_event_changed", @@ -385,14 +407,19 @@ async def test_webhook_event_handling_no_data(hass, climate_entry): await simulate_webhook(hass, webhook_id, response) -async def test_service_schedule_thermostats(hass, climate_entry, caplog): +async def test_service_schedule_thermostats(hass, config_entry, caplog, netatmo_auth): """Test service for selecting Netatmo schedule with thermostats.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_livingroom = "climate.netatmo_livingroom" # Test setting a valid schedule with patch( - "pyatmo.thermostat.HomeData.switch_home_schedule" + "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -421,7 +448,7 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog): # Test setting an invalid schedule with patch( - "pyatmo.thermostat.HomeData.switch_home_schedule" + "pyatmo.thermostat.AsyncHomeData.async_switch_home_schedule" ) as mock_switch_home_schedule: await hass.services.async_call( "netatmo", @@ -435,9 +462,16 @@ async def test_service_schedule_thermostats(hass, climate_entry, caplog): assert "summer is not a valid schedule" in caplog.text -async def test_service_preset_mode_already_boost_valves(hass, climate_entry): +async def test_service_preset_mode_already_boost_valves( + hass, config_entry, netatmo_auth +): """Test service with boost preset for valves when already in boost mode.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -508,9 +542,14 @@ async def test_service_preset_mode_already_boost_valves(hass, climate_entry): assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 -async def test_service_preset_mode_boost_valves(hass, climate_entry): +async def test_service_preset_mode_boost_valves(hass, config_entry, netatmo_auth): """Test service with boost preset for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Test service setting the preset mode to "boost" @@ -553,8 +592,13 @@ async def test_service_preset_mode_boost_valves(hass, climate_entry): assert hass.states.get(climate_entity_entrada).attributes["temperature"] == 30 -async def test_service_preset_mode_invalid(hass, climate_entry, caplog): +async def test_service_preset_mode_invalid(hass, config_entry, caplog, netatmo_auth): """Test service with invalid preset.""" + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + await hass.services.async_call( CLIMATE_DOMAIN, SERVICE_SET_PRESET_MODE, @@ -566,9 +610,14 @@ async def test_service_preset_mode_invalid(hass, climate_entry, caplog): assert "Preset mode 'invalid' not available" in caplog.text -async def test_valves_service_turn_off(hass, climate_entry): +async def test_valves_service_turn_off(hass, config_entry, netatmo_auth): """Test service turn off for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Test turning valve off @@ -606,9 +655,14 @@ async def test_valves_service_turn_off(hass, climate_entry): assert hass.states.get(climate_entity_entrada).state == "off" -async def test_valves_service_turn_on(hass, climate_entry): +async def test_valves_service_turn_on(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Test turning valve on @@ -661,9 +715,14 @@ async def test_get_all_home_ids(): assert climate.get_all_home_ids(home_data) == expected -async def test_webhook_home_id_mismatch(hass, climate_entry): +async def test_webhook_home_id_mismatch(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" assert hass.states.get(climate_entity_entrada).state == "auto" @@ -694,9 +753,14 @@ async def test_webhook_home_id_mismatch(hass, climate_entry): assert hass.states.get(climate_entity_entrada).state == "auto" -async def test_webhook_set_point(hass, climate_entry): +async def test_webhook_set_point(hass, config_entry, netatmo_auth): """Test service turn on for valves.""" - webhook_id = climate_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["climate"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] climate_entity_entrada = "climate.netatmo_entrada" # Fake backend response for valve being turned on diff --git a/tests/components/netatmo/test_init.py b/tests/components/netatmo/test_init.py index 2ec7d83689e..fba85d9d45c 100644 --- a/tests/components/netatmo/test_init.py +++ b/tests/components/netatmo/test_init.py @@ -1,15 +1,26 @@ """The tests for Netatmo component.""" +import asyncio +from datetime import timedelta from time import time -from unittest.mock import patch +from unittest.mock import AsyncMock, patch + +import pyatmo from homeassistant import config_entries from homeassistant.components.netatmo import DOMAIN from homeassistant.const import CONF_WEBHOOK_ID +from homeassistant.core import CoreState from homeassistant.setup import async_setup_component +from homeassistant.util import dt -from .common import FAKE_WEBHOOK_ACTIVATION, fake_post_request, simulate_webhook +from .common import ( + FAKE_WEBHOOK_ACTIVATION, + fake_post_request, + selected_platforms, + simulate_webhook, +) -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed from tests.components.cloud import mock_cloud # Fake webhook thermostat mode change to "Max" @@ -57,13 +68,15 @@ async def test_setup_component(hass): config_entry.add_to_hass(hass) with patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", ) as mock_auth, patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ) as mock_impl, patch( "homeassistant.components.webhook.async_generate_url" ) as mock_webhook: - mock_auth.return_value.post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() assert await async_setup_component(hass, "netatmo", {}) await hass.async_block_till_done() @@ -72,7 +85,7 @@ async def test_setup_component(hass): mock_impl.assert_called_once() mock_webhook.assert_called_once() - assert config_entry.state == config_entries.ENTRY_STATE_LOADED + assert config_entry.state is config_entries.ConfigEntryState.LOADED assert hass.config_entries.async_entries(DOMAIN) assert len(hass.states.async_all()) > 0 @@ -86,38 +99,54 @@ async def test_setup_component(hass): async def test_setup_component_with_config(hass, config_entry): """Test setup of the netatmo component with dev account.""" + fake_post_hits = 0 + + async def fake_post(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return await fake_post_request(*args, **kwargs) + with patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ) as mock_impl, patch( "homeassistant.components.webhook.async_generate_url" ) as mock_webhook, patch( - "pyatmo.auth.NetatmoOAuth2.post_request" - ) as fake_post_requests, patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( "homeassistant.components.netatmo.PLATFORMS", ["sensor"] ): + mock_auth.return_value.async_post_request.side_effect = fake_post + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) await hass.async_block_till_done() - fake_post_requests.assert_called() + assert fake_post_hits == 3 mock_impl.assert_called_once() mock_webhook.assert_called_once() - assert config_entry.state == config_entries.ENTRY_STATE_LOADED - assert hass.config_entries.async_entries(DOMAIN) - assert len(hass.states.async_all()) > 0 + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) > 0 -async def test_setup_component_with_webhook(hass, entry): +async def test_setup_component_with_webhook(hass, config_entry, netatmo_auth): """Test setup and teardown of the netatmo component with webhook registration.""" - webhook_id = entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["camera", "climate", "light", "sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) assert len(hass.states.async_all()) > 0 - webhook_id = entry.data[CONF_WEBHOOK_ID] + webhook_id = config_entry.data[CONF_WEBHOOK_ID] await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) # Assert webhook is established successfully @@ -134,36 +163,30 @@ async def test_setup_component_with_webhook(hass, entry): assert len(hass.config_entries.async_entries(DOMAIN)) == 0 -async def test_setup_without_https(hass, config_entry): +async def test_setup_without_https(hass, config_entry, caplog): """Test if set up with cloud link and without https.""" hass.config.components.add("cloud") with patch( "homeassistant.helpers.network.get_url", - return_value="https://example.nabu.casa", + return_value="http://example.nabu.casa", ), patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth, patch( "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", ), patch( "homeassistant.components.webhook.async_generate_url" - ) as mock_webhook: - mock_auth.return_value.post_request.side_effect = fake_post_request - mock_webhook.return_value = "https://example.com" + ) as mock_async_generate_url: + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_async_generate_url.return_value = "http://example.com" assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) - await hass.async_block_till_done() + await hass.async_block_till_done() + mock_auth.assert_called_once() + mock_async_generate_url.assert_called_once() - webhook_id = config_entry.data[CONF_WEBHOOK_ID] - await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) - - # Assert webhook is established successfully - climate_entity_livingroom = "climate.netatmo_livingroom" - assert hass.states.get(climate_entity_livingroom).state == "auto" - await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK) - await hass.async_block_till_done() - assert hass.states.get(climate_entity_livingroom).state == "heat" + assert "https and port 443 is required to register the webhook" in caplog.text async def test_setup_with_cloud(hass, config_entry): @@ -181,7 +204,7 @@ async def test_setup_with_cloud(hass, config_entry): ) as fake_create_cloudhook, patch( "homeassistant.components.cloud.async_delete_cloudhook" ) as fake_delete_cloudhook, patch( - "homeassistant.components.netatmo.api.ConfigEntryNetatmoAuth" + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" ) as mock_auth, patch( "homeassistant.components.netatmo.PLATFORMS", [] ), patch( @@ -189,7 +212,7 @@ async def test_setup_with_cloud(hass, config_entry): ), patch( "homeassistant.components.webhook.async_generate_url" ): - mock_auth.return_value.post_request.side_effect = fake_post_request + mock_auth.return_value.async_post_request.side_effect = fake_post_request assert await async_setup_component( hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} ) @@ -210,3 +233,199 @@ async def test_setup_with_cloud(hass, config_entry): await hass.async_block_till_done() assert not hass.config_entries.async_entries(DOMAIN) + + +async def test_setup_with_cloudhook(hass): + """Test if set up with active cloud subscription and cloud hook.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "cloudhook_url": "https://hooks.nabu.casa/ABCD", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": "read_station", + }, + }, + ) + config_entry.add_to_hass(hass) + + await mock_cloud(hass) + await hass.async_block_till_done() + + with patch( + "homeassistant.components.cloud.async_is_logged_in", return_value=True + ), patch( + "homeassistant.components.cloud.async_active_subscription", return_value=True + ), patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ) as fake_create_cloudhook, patch( + "homeassistant.components.cloud.async_delete_cloudhook" + ) as fake_delete_cloudhook, patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", [] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = fake_post_request + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + assert hass.components.cloud.async_active_subscription() is True + + assert ( + hass.config_entries.async_entries("netatmo")[0].data["cloudhook_url"] + == "https://hooks.nabu.casa/ABCD" + ) + + await hass.async_block_till_done() + assert hass.config_entries.async_entries(DOMAIN) + fake_create_cloudhook.assert_not_called() + + for config_entry in hass.config_entries.async_entries("netatmo"): + await hass.config_entries.async_remove(config_entry.entry_id) + fake_delete_cloudhook.assert_called_once() + + await hass.async_block_till_done() + assert not hass.config_entries.async_entries(DOMAIN) + + +async def test_setup_component_api_error(hass): + """Test error on setup of the netatmo component.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": "read_station", + }, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = ( + pyatmo.exceptions.ApiError() + ) + + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + + await hass.async_block_till_done() + + mock_auth.assert_called_once() + mock_impl.assert_called_once() + + +async def test_setup_component_api_timeout(hass): + """Test timeout on setup of the netatmo component.""" + config_entry = MockConfigEntry( + domain="netatmo", + data={ + "auth_implementation": "cloud", + "token": { + "refresh_token": "mock-refresh-token", + "access_token": "mock-access-token", + "type": "Bearer", + "expires_in": 60, + "expires_at": time() + 1000, + "scope": "read_station", + }, + }, + ) + config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth", + ) as mock_auth, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = ( + asyncio.exceptions.TimeoutError() + ) + + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + assert await async_setup_component(hass, "netatmo", {}) + + await hass.async_block_till_done() + + mock_auth.assert_called_once() + mock_impl.assert_called_once() + + +async def test_setup_component_with_delay(hass, config_entry): + """Test setup of the netatmo component with delayed startup.""" + hass.state = CoreState.not_running + + with patch( + "pyatmo.AbstractAsyncAuth.async_addwebhook", side_effect=AsyncMock() + ) as mock_addwebhook, patch( + "pyatmo.AbstractAsyncAuth.async_dropwebhook", side_effect=AsyncMock() + ) as mock_dropwebhook, patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ) as mock_impl, patch( + "homeassistant.components.webhook.async_generate_url" + ) as mock_webhook, patch( + "pyatmo.AbstractAsyncAuth.async_post_request", side_effect=fake_post_request + ) as mock_post_request, patch( + "homeassistant.components.netatmo.PLATFORMS", ["light"] + ): + + assert await async_setup_component( + hass, "netatmo", {"netatmo": {"client_id": "123", "client_secret": "abc"}} + ) + + await hass.async_block_till_done() + + assert mock_post_request.call_count == 5 + + mock_impl.assert_called_once() + mock_webhook.assert_not_called() + + await hass.async_start() + await hass.async_block_till_done() + mock_webhook.assert_called_once() + + # Fake webhook activation + await simulate_webhook( + hass, config_entry.data[CONF_WEBHOOK_ID], FAKE_WEBHOOK_ACTIVATION + ) + await hass.async_block_till_done() + + mock_addwebhook.assert_called_once() + mock_dropwebhook.assert_not_awaited() + + async_fire_time_changed( + hass, + dt.utcnow() + timedelta(seconds=60), + ) + await hass.async_block_till_done() + + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) > 0 + + await hass.async_stop() + mock_dropwebhook.assert_called_once() diff --git a/tests/components/netatmo/test_light.py b/tests/components/netatmo/test_light.py index 4d84bc4e5a5..6abbb646055 100644 --- a/tests/components/netatmo/test_light.py +++ b/tests/components/netatmo/test_light.py @@ -1,19 +1,25 @@ """The tests for Netatmo light.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, patch from homeassistant.components.light import ( DOMAIN as LIGHT_DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON, ) +from homeassistant.components.netatmo import DOMAIN from homeassistant.const import ATTR_ENTITY_ID, CONF_WEBHOOK_ID -from .common import FAKE_WEBHOOK_ACTIVATION, simulate_webhook +from .common import FAKE_WEBHOOK_ACTIVATION, selected_platforms, simulate_webhook -async def test_light_setup_and_services(hass, light_entry): +async def test_light_setup_and_services(hass, config_entry, netatmo_auth): """Test setup and services.""" - webhook_id = light_entry.data[CONF_WEBHOOK_ID] + with selected_platforms(["light"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + webhook_id = config_entry.data[CONF_WEBHOOK_ID] # Fake webhook activation await simulate_webhook(hass, webhook_id, FAKE_WEBHOOK_ACTIVATION) @@ -45,7 +51,7 @@ async def test_light_setup_and_services(hass, light_entry): assert hass.states.get(light_entity).state == "on" # Test turning light off - with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_OFF, @@ -60,7 +66,7 @@ async def test_light_setup_and_services(hass, light_entry): ) # Test turning light on - with patch("pyatmo.camera.CameraData.set_state") as mock_set_state: + with patch("pyatmo.camera.AsyncCameraData.async_set_state") as mock_set_state: await hass.services.async_call( LIGHT_DOMAIN, SERVICE_TURN_ON, @@ -73,3 +79,43 @@ async def test_light_setup_and_services(hass, light_entry): camera_id="12:34:56:00:a5:a4", floodlight="on", ) + + +async def test_setup_component_no_devices(hass, config_entry): + """Test setup with no devices.""" + fake_post_hits = 0 + + async def fake_post_request_no_data(*args, **kwargs): + """Fake error during requesting backend data.""" + nonlocal fake_post_hits + fake_post_hits += 1 + return "{}" + + with patch( + "homeassistant.components.netatmo.api.AsyncConfigEntryNetatmoAuth" + ) as mock_auth, patch( + "homeassistant.components.netatmo.PLATFORMS", ["light"] + ), patch( + "homeassistant.helpers.config_entry_oauth2_flow.async_get_config_entry_implementation", + ), patch( + "homeassistant.components.webhook.async_generate_url" + ): + mock_auth.return_value.async_post_request.side_effect = ( + fake_post_request_no_data + ) + mock_auth.return_value.async_addwebhook.side_effect = AsyncMock() + mock_auth.return_value.async_dropwebhook.side_effect = AsyncMock() + + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + # Fake webhook activation + await simulate_webhook( + hass, config_entry.data[CONF_WEBHOOK_ID], FAKE_WEBHOOK_ACTIVATION + ) + await hass.async_block_till_done() + + assert fake_post_hits == 1 + + assert hass.config_entries.async_entries(DOMAIN) + assert len(hass.states.async_all()) == 0 diff --git a/tests/components/netatmo/test_media_source.py b/tests/components/netatmo/test_media_source.py index fd36c57dfd1..2ba70ca9489 100644 --- a/tests/components/netatmo/test_media_source.py +++ b/tests/components/netatmo/test_media_source.py @@ -51,6 +51,16 @@ async def test_async_browse_media(hass): ) assert str(excinfo.value) == "Unknown source directory." + # Test invalid base + with pytest.raises(ValueError) as excinfo: + await media_source.async_browse_media(hass, f"{const.URI_SCHEME}{DOMAIN}/") + assert str(excinfo.value) == "Invalid media source URI" + + # Test successful listing + media = await media_source.async_browse_media( + hass, f"{const.URI_SCHEME}{DOMAIN}/events" + ) + # Test successful listing media = await media_source.async_browse_media( hass, f"{const.URI_SCHEME}{DOMAIN}/events/" diff --git a/tests/components/netatmo/test_sensor.py b/tests/components/netatmo/test_sensor.py index fcb2ce454df..bebd8e0191c 100644 --- a/tests/components/netatmo/test_sensor.py +++ b/tests/components/netatmo/test_sensor.py @@ -1,23 +1,22 @@ """The tests for the Netatmo sensor platform.""" -from datetime import timedelta from unittest.mock import patch import pytest from homeassistant.components.netatmo import sensor from homeassistant.components.netatmo.sensor import MODULE_TYPE_WIND -from homeassistant.config_entries import RELOAD_AFTER_UPDATE_DELAY from homeassistant.helpers import entity_registry as er -from homeassistant.util import dt -from .common import TEST_TIME -from .conftest import selected_platforms - -from tests.common import async_fire_time_changed +from .common import TEST_TIME, selected_platforms -async def test_weather_sensor(hass, sensor_entry): +async def test_weather_sensor(hass, config_entry, netatmo_auth): """Test weather sensor setup.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + prefix = "sensor.netatmo_mystation_" assert hass.states.get(f"{prefix}temperature").state == "24.6" @@ -26,8 +25,15 @@ async def test_weather_sensor(hass, sensor_entry): assert hass.states.get(f"{prefix}pressure").state == "1017.3" -async def test_public_weather_sensor(hass, sensor_entry): +async def test_public_weather_sensor(hass, config_entry, netatmo_auth): """Test public weather sensor setup.""" + with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): + await hass.config_entries.async_setup(config_entry.entry_id) + + await hass.async_block_till_done() + + assert len(hass.states.async_all()) > 0 + prefix = "sensor.netatmo_home_max_" assert hass.states.get(f"{prefix}temperature").state == "27.4" @@ -40,7 +46,6 @@ async def test_public_weather_sensor(hass, sensor_entry): assert hass.states.get(f"{prefix}humidity").state == "63.2" assert hass.states.get(f"{prefix}pressure").state == "1010.3" - assert len(hass.states.async_all()) > 0 entities_before_change = len(hass.states.async_all()) valid_option = { @@ -53,7 +58,7 @@ async def test_public_weather_sensor(hass, sensor_entry): "mode": "max", } - result = await hass.config_entries.options.async_init(sensor_entry.entry_id) + result = await hass.config_entries.options.async_init(config_entry.entry_id) result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={"new_area": "Home avg"} ) @@ -63,18 +68,11 @@ async def test_public_weather_sensor(hass, sensor_entry): result = await hass.config_entries.options.async_configure( result["flow_id"], user_input={} ) - await hass.async_block_till_done() - async_fire_time_changed( - hass, - dt.utcnow() + timedelta(seconds=RELOAD_AFTER_UPDATE_DELAY + 1), - ) - await hass.async_block_till_done() - assert hass.states.get(f"{prefix}temperature").state == "27.4" - assert hass.states.get(f"{prefix}humidity").state == "76" - assert hass.states.get(f"{prefix}pressure").state == "1014.4" + await hass.async_block_till_done() assert len(hass.states.async_all()) == entities_before_change + assert hass.states.get(f"{prefix}temperature").state == "27.4" @pytest.mark.parametrize( @@ -213,7 +211,9 @@ async def test_fix_angle(angle, expected): ), ], ) -async def test_weather_sensor_enabling(hass, config_entry, uid, name, expected): +async def test_weather_sensor_enabling( + hass, config_entry, uid, name, expected, netatmo_auth +): """Test enabling of by default disabled sensors.""" with patch("time.time", return_value=TEST_TIME), selected_platforms(["sensor"]): states_before = len(hass.states.async_all()) diff --git a/tests/components/network/__init__.py b/tests/components/network/__init__.py new file mode 100644 index 00000000000..f3ccacbd064 --- /dev/null +++ b/tests/components/network/__init__.py @@ -0,0 +1 @@ +"""Tests for the Network Configuration integration.""" diff --git a/tests/components/network/test_init.py b/tests/components/network/test_init.py new file mode 100644 index 00000000000..41d87d5a805 --- /dev/null +++ b/tests/components/network/test_init.py @@ -0,0 +1,446 @@ +"""Test the Network Configuration.""" +from unittest.mock import Mock, patch + +import ifaddr + +from homeassistant.components import network +from homeassistant.components.network.const import ( + ATTR_ADAPTERS, + ATTR_CONFIGURED_ADAPTERS, + STORAGE_KEY, + STORAGE_VERSION, +) +from homeassistant.setup import async_setup_component + +_NO_LOOPBACK_IPADDR = "192.168.1.5" +_LOOPBACK_IPADDR = "127.0.0.1" + + +def _generate_mock_adapters(): + mock_lo0 = Mock(spec=ifaddr.Adapter) + mock_lo0.nice_name = "lo0" + mock_lo0.ips = [ifaddr.IP("127.0.0.1", 8, "lo0")] + mock_eth0 = Mock(spec=ifaddr.Adapter) + mock_eth0.nice_name = "eth0" + mock_eth0.ips = [ifaddr.IP(("2001:db8::", 1, 1), 8, "eth0")] + mock_eth1 = Mock(spec=ifaddr.Adapter) + mock_eth1.nice_name = "eth1" + mock_eth1.ips = [ifaddr.IP("192.168.1.5", 23, "eth1")] + mock_vtun0 = Mock(spec=ifaddr.Adapter) + mock_vtun0.nice_name = "vtun0" + mock_vtun0.ips = [ifaddr.IP("169.254.3.2", 16, "vtun0")] + return [mock_eth0, mock_lo0, mock_eth1, mock_vtun0] + + +async def test_async_detect_interfaces_setting_non_loopback_route(hass, hass_storage): + """Test without default interface config and the route returns a non-loopback address.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_NO_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + + assert network_obj.adapters == [ + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_async_detect_interfaces_setting_loopback_route(hass, hass_storage): + """Test without default interface config and the route returns a loopback address.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + assert network_obj.adapters == [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": True, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_async_detect_interfaces_setting_empty_route(hass, hass_storage): + """Test without default interface config and the route returns nothing.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + assert network_obj.adapters == [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_async_detect_interfaces_setting_exception(hass, hass_storage): + """Test without default interface config and the route throws an exception.""" + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + side_effect=AttributeError, + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == [] + assert network_obj.adapters == [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_interfaces_configured_from_storage(hass, hass_storage): + """Test settings from storage are preferred over auto configure.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, + } + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_NO_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] + + assert network_obj.adapters == [ + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + +async def test_interfaces_configured_from_storage_websocket_update( + hass, hass_ws_client, hass_storage +): + """Test settings from storage can be updated via websocket api.""" + hass_storage[STORAGE_KEY] = { + "version": STORAGE_VERSION, + "key": STORAGE_KEY, + "data": {ATTR_CONFIGURED_ADAPTERS: ["eth0", "eth1", "vtun0"]}, + } + with patch( + "homeassistant.components.network.util.socket.socket.getsockname", + return_value=[_NO_LOOPBACK_IPADDR], + ), patch( + "homeassistant.components.network.util.ifaddr.get_adapters", + return_value=_generate_mock_adapters(), + ): + assert await async_setup_component(hass, network.DOMAIN, {network.DOMAIN: {}}) + await hass.async_block_till_done() + + network_obj = hass.data[network.DOMAIN] + assert network_obj.configured_adapters == ["eth0", "eth1", "vtun0"] + ws_client = await hass_ws_client(hass) + await ws_client.send_json({"id": 1, "type": "network"}) + + response = await ws_client.receive_json() + + assert response["success"] + assert response["result"][ATTR_CONFIGURED_ADAPTERS] == ["eth0", "eth1", "vtun0"] + assert response["result"][ATTR_ADAPTERS] == [ + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": True, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] + + await ws_client.send_json( + {"id": 2, "type": "network/configure", "config": {ATTR_CONFIGURED_ADAPTERS: []}} + ) + response = await ws_client.receive_json() + assert response["result"][ATTR_CONFIGURED_ADAPTERS] == [] + + await ws_client.send_json({"id": 3, "type": "network"}) + response = await ws_client.receive_json() + assert response["result"][ATTR_CONFIGURED_ADAPTERS] == [] + assert response["result"][ATTR_ADAPTERS] == [ + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "127.0.0.1", "network_prefix": 8}], + "ipv6": [], + "name": "lo0", + }, + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, + ] diff --git a/tests/components/nexia/test_config_flow.py b/tests/components/nexia/test_config_flow.py index b9726fdd974..2dd5c270c07 100644 --- a/tests/components/nexia/test_config_flow.py +++ b/tests/components/nexia/test_config_flow.py @@ -1,14 +1,17 @@ """Test the nexia config flow.""" from unittest.mock import MagicMock, patch +from nexia.const import BRAND_ASAIR, BRAND_NEXIA +import pytest from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import config_entries, setup -from homeassistant.components.nexia.const import DOMAIN +from homeassistant.components.nexia.const import CONF_BRAND, DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -async def test_form(hass): +@pytest.mark.parametrize("brand", [BRAND_ASAIR, BRAND_NEXIA]) +async def test_form(hass, brand): """Test we get the form.""" await setup.async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( @@ -29,13 +32,14 @@ async def test_form(hass): ) as mock_setup_entry: result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + {CONF_BRAND: brand, CONF_USERNAME: "username", CONF_PASSWORD: "password"}, ) await hass.async_block_till_done() assert result2["type"] == "create_entry" assert result2["title"] == "myhouse" assert result2["data"] == { + CONF_BRAND: brand, CONF_USERNAME: "username", CONF_PASSWORD: "password", } @@ -51,7 +55,11 @@ async def test_form_invalid_auth(hass): with patch("homeassistant.components.nexia.config_flow.NexiaHome.login"): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -70,7 +78,11 @@ async def test_form_cannot_connect(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -91,7 +103,11 @@ async def test_form_invalid_auth_http_401(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -112,7 +128,11 @@ async def test_form_cannot_connect_not_found(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" @@ -131,7 +151,11 @@ async def test_form_broad_exception(hass): ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - {CONF_USERNAME: "username", CONF_PASSWORD: "password"}, + { + CONF_BRAND: BRAND_NEXIA, + CONF_USERNAME: "username", + CONF_PASSWORD: "password", + }, ) assert result2["type"] == "form" diff --git a/tests/components/nexia/util.py b/tests/components/nexia/util.py index 8e132941994..b6d5c697a18 100644 --- a/tests/components/nexia/util.py +++ b/tests/components/nexia/util.py @@ -21,17 +21,18 @@ async def async_init_integration( house_fixture = "nexia/mobile_houses_123456.json" session_fixture = "nexia/session_123456.json" sign_in_fixture = "nexia/sign_in.json" + nexia = NexiaHome(auto_login=False) with requests_mock.mock() as m, patch( "nexia.home.load_or_create_uuid", return_value=uuid.uuid4() ): - m.post(NexiaHome.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) + m.post(nexia.API_MOBILE_SESSION_URL, text=load_fixture(session_fixture)) m.get( - NexiaHome.API_MOBILE_HOUSES_URL.format(house_id=123456), + nexia.API_MOBILE_HOUSES_URL.format(house_id=123456), text=load_fixture(house_fixture), ) m.post( - NexiaHome.API_MOBILE_ACCOUNTS_SIGN_IN_URL, + nexia.API_MOBILE_ACCOUNTS_SIGN_IN_URL, text=load_fixture(sign_in_fixture), ) entry = MockConfigEntry( diff --git a/tests/components/nightscout/test_init.py b/tests/components/nightscout/test_init.py index 88ca141b999..04824139e20 100644 --- a/tests/components/nightscout/test_init.py +++ b/tests/components/nightscout/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import patch from aiohttp import ClientError from homeassistant.components.nightscout.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_URL from tests.common import MockConfigEntry @@ -20,12 +16,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -42,4 +38,4 @@ async def test_async_setup_raises_entry_not_ready(hass): side_effect=ClientError(), ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/nzbget/test_init.py b/tests/components/nzbget/test_init.py index 2dcdab5754e..e83672769da 100644 --- a/tests/components/nzbget/test_init.py +++ b/tests/components/nzbget/test_init.py @@ -4,11 +4,7 @@ from unittest.mock import patch from pynzbgetapi import NZBGetAPIException from homeassistant.components.nzbget.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT from homeassistant.setup import async_setup_component @@ -44,12 +40,12 @@ async def test_unload_entry(hass, nzbget_api): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -64,4 +60,4 @@ async def test_async_setup_raises_entry_not_ready(hass): ): await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/onboarding/test_views.py b/tests/components/onboarding/test_views.py index d8ae50b851f..a921dfe39d4 100644 --- a/tests/components/onboarding/test_views.py +++ b/tests/components/onboarding/test_views.py @@ -73,6 +73,9 @@ async def mock_supervisor_fixture(hass, aioclient_mock): ), patch( "homeassistant.components.hassio.HassIO.get_host_info", return_value={}, + ), patch( + "homeassistant.components.hassio.HassIO.get_store", + return_value={}, ), patch( "homeassistant.components.hassio.HassIO.get_supervisor_info", return_value={"diagnostics": True}, diff --git a/tests/components/onewire/__init__.py b/tests/components/onewire/__init__.py index 088c6a3ad11..1712a5500dd 100644 --- a/tests/components/onewire/__init__.py +++ b/tests/components/onewire/__init__.py @@ -14,7 +14,7 @@ from homeassistant.components.onewire.const import ( DEFAULT_SYSBUS_MOUNT_DIR, DOMAIN, ) -from homeassistant.config_entries import CONN_CLASS_LOCAL_POLL, SOURCE_USER +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from .const import MOCK_OWPROXY_DEVICES, MOCK_SYSBUS_DEVICES @@ -32,7 +32,6 @@ async def setup_onewire_sysbus_integration(hass): CONF_MOUNT_DIR: DEFAULT_SYSBUS_MOUNT_DIR, }, unique_id=f"{CONF_TYPE_SYSBUS}:{DEFAULT_SYSBUS_MOUNT_DIR}", - connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="1", ) @@ -57,7 +56,6 @@ async def setup_onewire_owserver_integration(hass): CONF_HOST: "1.2.3.4", CONF_PORT: 1234, }, - connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", ) @@ -85,7 +83,6 @@ async def setup_onewire_patched_owserver_integration(hass): "10.111111111111": "My DS18B20", }, }, - connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", ) diff --git a/tests/components/onewire/const.py b/tests/components/onewire/const.py index ccae8e695ce..1eb2b4b390a 100644 --- a/tests/components/onewire/const.py +++ b/tests/components/onewire/const.py @@ -31,6 +31,28 @@ MOCK_OWPROXY_DEVICES = { ], SENSOR_DOMAIN: [], }, + "05.111111111111": { + "inject_reads": [ + b"DS2405", # read device type + ], + "device_info": { + "identifiers": {(DOMAIN, "05.111111111111")}, + "manufacturer": "Maxim Integrated", + "model": "DS2405", + "name": "05.111111111111", + }, + SWITCH_DOMAIN: [ + { + "entity_id": "switch.05_111111111111_pio", + "unique_id": "/05.111111111111/PIO", + "injected_value": b" 1", + "result": STATE_ON, + "unit": None, + "class": None, + "disabled": True, + }, + ], + }, "10.111111111111": { "inject_reads": [ b"DS18S20", # read device type @@ -775,6 +797,36 @@ MOCK_OWPROXY_DEVICES = { }, ], }, + "7E.222222222222": { + "inject_reads": [ + b"EDS", # read type + b"EDS0066", # read device_type - note EDS specific + ], + "device_info": { + "identifiers": {(DOMAIN, "7E.222222222222")}, + "manufacturer": "Maxim Integrated", + "model": "EDS", + "name": "7E.222222222222", + }, + SENSOR_DOMAIN: [ + { + "entity_id": "sensor.7e_222222222222_temperature", + "unique_id": "/7E.222222222222/EDS0066/temperature", + "injected_value": b" 13.9375", + "result": "13.9", + "unit": TEMP_CELSIUS, + "class": DEVICE_CLASS_TEMPERATURE, + }, + { + "entity_id": "sensor.7e_222222222222_pressure", + "unique_id": "/7E.222222222222/EDS0066/pressure", + "injected_value": b" 1012.21", + "result": "1012.2", + "unit": PRESSURE_MBAR, + "class": DEVICE_CLASS_PRESSURE, + }, + ], + }, } MOCK_SYSBUS_DEVICES = { @@ -884,7 +936,7 @@ MOCK_SYSBUS_DEVICES = { { "entity_id": "sensor.42_111111111112_temperature", "unique_id": "/sys/bus/w1/devices/42-111111111112/w1_slave", - "injected_value": [UnsupportResponseException] * 9 + ["27.993"], + "injected_value": [UnsupportResponseException] * 9 + [27.993], "result": "28.0", "unit": TEMP_CELSIUS, "class": DEVICE_CLASS_TEMPERATURE, @@ -902,7 +954,7 @@ MOCK_SYSBUS_DEVICES = { { "entity_id": "sensor.42_111111111113_temperature", "unique_id": "/sys/bus/w1/devices/42-111111111113/w1_slave", - "injected_value": [UnsupportResponseException] * 10 + ["27.993"], + "injected_value": [UnsupportResponseException] * 10 + [27.993], "result": "unknown", "unit": TEMP_CELSIUS, "class": DEVICE_CLASS_TEMPERATURE, diff --git a/tests/components/onewire/test_init.py b/tests/components/onewire/test_init.py index 44e8d15bff6..01426e1faf1 100644 --- a/tests/components/onewire/test_init.py +++ b/tests/components/onewire/test_init.py @@ -5,13 +5,7 @@ from pyownet.protocol import ConnError, OwnetError from homeassistant.components.onewire.const import CONF_TYPE_OWSERVER, DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN -from homeassistant.config_entries import ( - CONN_CLASS_LOCAL_POLL, - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, - SOURCE_USER, -) +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TYPE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.setup import async_setup_component @@ -36,7 +30,6 @@ async def test_owserver_connect_failure(hass): CONF_HOST: "1.2.3.4", CONF_PORT: "1234", }, - connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", ) @@ -50,7 +43,7 @@ async def test_owserver_connect_failure(hass): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert config_entry_owserver.state == ENTRY_STATE_SETUP_RETRY + assert config_entry_owserver.state is ConfigEntryState.SETUP_RETRY assert not hass.data.get(DOMAIN) @@ -64,7 +57,6 @@ async def test_failed_owserver_listing(hass): CONF_HOST: "1.2.3.4", CONF_PORT: "1234", }, - connection_class=CONN_CLASS_LOCAL_POLL, options={}, entry_id="2", ) @@ -84,15 +76,15 @@ async def test_unload_entry(hass): config_entry_sysbus = await setup_onewire_sysbus_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 2 - assert config_entry_owserver.state == ENTRY_STATE_LOADED - assert config_entry_sysbus.state == ENTRY_STATE_LOADED + assert config_entry_owserver.state is ConfigEntryState.LOADED + assert config_entry_sysbus.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(config_entry_owserver.entry_id) assert await hass.config_entries.async_unload(config_entry_sysbus.entry_id) await hass.async_block_till_done() - assert config_entry_owserver.state == ENTRY_STATE_NOT_LOADED - assert config_entry_sysbus.state == ENTRY_STATE_NOT_LOADED + assert config_entry_owserver.state is ConfigEntryState.NOT_LOADED + assert config_entry_sysbus.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) diff --git a/tests/components/onvif/test_config_flow.py b/tests/components/onvif/test_config_flow.py index a8827247689..626eec433d1 100644 --- a/tests/components/onvif/test_config_flow.py +++ b/tests/components/onvif/test_config_flow.py @@ -41,6 +41,7 @@ def setup_mock_onvif_camera( with_h264=True, two_profiles=False, with_interfaces=True, + with_interfaces_not_implemented=False, with_serial=True, ): """Prepare mock onvif.ONVIFCamera.""" @@ -54,9 +55,14 @@ def setup_mock_onvif_camera( interface.Enabled = True interface.Info.HwAddress = MAC - devicemgmt.GetNetworkInterfaces = AsyncMock( - return_value=[interface] if with_interfaces else [] - ) + if with_interfaces_not_implemented: + devicemgmt.GetNetworkInterfaces = AsyncMock( + side_effect=Fault("not implemented") + ) + else: + devicemgmt.GetNetworkInterfaces = AsyncMock( + return_value=[interface] if with_interfaces else [] + ) media_service = MagicMock() @@ -153,7 +159,6 @@ async def setup_onvif_integration( domain=config_flow.DOMAIN, source=source, data={**config}, - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, options=options or {}, entry_id=entry_id, unique_id=unique_id, @@ -414,6 +419,47 @@ async def test_flow_manual_entry(hass): } +async def test_flow_import_not_implemented(hass): + """Test that config flow uses Serial Number when no MAC available.""" + with patch( + "homeassistant.components.onvif.config_flow.get_device" + ) as mock_onvif_camera, patch( + "homeassistant.components.onvif.ONVIFDevice" + ) as mock_device, patch( + "homeassistant.components.onvif.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.onvif.async_setup_entry", return_value=True + ) as mock_setup_entry: + setup_mock_onvif_camera(mock_onvif_camera, with_interfaces_not_implemented=True) + setup_mock_device(mock_device) + + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + }, + ) + + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == f"{NAME} - {SERIAL_NUMBER}" + assert result["data"] == { + config_flow.CONF_NAME: NAME, + config_flow.CONF_HOST: HOST, + config_flow.CONF_PORT: PORT, + config_flow.CONF_USERNAME: USERNAME, + config_flow.CONF_PASSWORD: PASSWORD, + } + + async def test_flow_import_no_mac(hass): """Test that config flow uses Serial Number when no MAC available.""" with patch( diff --git a/tests/components/openweathermap/test_config_flow.py b/tests/components/openweathermap/test_config_flow.py index daa38bc1dc7..ba1be4afb4c 100644 --- a/tests/components/openweathermap/test_config_flow.py +++ b/tests/components/openweathermap/test_config_flow.py @@ -10,7 +10,7 @@ from homeassistant.components.openweathermap.const import ( DEFAULT_LANGUAGE, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_USER, ConfigEntryState from homeassistant.const import ( CONF_API_KEY, CONF_LATITUDE, @@ -57,11 +57,11 @@ async def test_form(hass): conf_entries = hass.config_entries.async_entries(DOMAIN) entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state == ConfigEntryState.LOADED await hass.config_entries.async_unload(conf_entries[0].entry_id) await hass.async_block_till_done() - assert entry.state == "not_loaded" + assert entry.state == ConfigEntryState.NOT_LOADED assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == CONFIG[CONF_NAME] @@ -86,7 +86,7 @@ async def test_form_options(hass): assert await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -105,7 +105,7 @@ async def test_form_options(hass): await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == ConfigEntryState.LOADED result = await hass.config_entries.options.async_init(config_entry.entry_id) @@ -124,7 +124,7 @@ async def test_form_options(hass): await hass.async_block_till_done() - assert config_entry.state == "loaded" + assert config_entry.state == ConfigEntryState.LOADED async def test_form_invalid_api_key(hass): diff --git a/tests/components/ozw/common.py b/tests/components/ozw/common.py index 1467d619afe..450066c5aed 100644 --- a/tests/components/ozw/common.py +++ b/tests/components/ozw/common.py @@ -10,14 +10,15 @@ from tests.common import MockConfigEntry async def setup_ozw(hass, entry=None, fixture=None): """Set up OZW and load a dump.""" - mqtt_entry = MockConfigEntry(domain="mqtt", state=config_entries.ENTRY_STATE_LOADED) + mqtt_entry = MockConfigEntry( + domain="mqtt", state=config_entries.ConfigEntryState.LOADED + ) mqtt_entry.add_to_hass(hass) if entry is None: entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, ) entry.add_to_hass(hass) diff --git a/tests/components/ozw/conftest.py b/tests/components/ozw/conftest.py index 1df365054d4..d09259654de 100644 --- a/tests/components/ozw/conftest.py +++ b/tests/components/ozw/conftest.py @@ -4,7 +4,7 @@ from unittest.mock import patch import pytest -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from .common import MQTTMessage @@ -275,6 +275,6 @@ def mock_get_addon_discovery_info(): @pytest.fixture(name="mqtt") async def mock_mqtt_fixture(hass): """Mock the MQTT integration.""" - mqtt_entry = MockConfigEntry(domain="mqtt", state=ENTRY_STATE_LOADED) + mqtt_entry = MockConfigEntry(domain="mqtt", state=ConfigEntryState.LOADED) mqtt_entry.add_to_hass(hass) return mqtt_entry diff --git a/tests/components/ozw/test_init.py b/tests/components/ozw/test_init.py index 339b690f4e4..9719c483800 100644 --- a/tests/components/ozw/test_init.py +++ b/tests/components/ozw/test_init.py @@ -31,7 +31,6 @@ async def test_setup_entry_without_mqtt(hass): entry = MockConfigEntry( domain=DOMAIN, title="OpenZWave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, ) entry.add_to_hass(hass) @@ -64,19 +63,18 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): 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 + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED receive_message = await setup_ozw(hass, entry=entry, fixture=generic_data) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.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 entry.state is config_entries.ConfigEntryState.NOT_LOADED entities = hass.states.async_entity_ids("switch") assert len(entities) == 1 for entity in entities: @@ -100,7 +98,7 @@ async def test_unload_entry(hass, generic_data, switch_msg, caplog): await setup_ozw(hass, entry=entry, fixture=generic_data) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert len(hass.states.async_entity_ids("switch")) == 1 for record in caplog.records: assert record.levelname != "ERROR" @@ -112,23 +110,21 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={"integration_created_addon": False}, ) entry.add_to_hass(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 await hass.config_entries.async_remove(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 # test successful remove with created add-on entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={"integration_created_addon": True}, ) entry.add_to_hass(hass) @@ -138,7 +134,7 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert stop_addon.call_count == 1 assert uninstall_addon.call_count == 1 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() uninstall_addon.reset_mock() @@ -152,7 +148,7 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert stop_addon.call_count == 1 assert uninstall_addon.call_count == 0 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the OpenZWave add-on" in caplog.text stop_addon.side_effect = None @@ -168,7 +164,7 @@ async def test_remove_entry(hass, stop_addon, uninstall_addon, caplog): assert stop_addon.call_count == 1 assert uninstall_addon.call_count == 1 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the OpenZWave add-on" in caplog.text @@ -178,7 +174,6 @@ async def test_setup_entry_with_addon(hass, get_addon_discovery_info): entry = MockConfigEntry( domain=DOMAIN, title="OpenZWave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={"use_addon": True}, ) entry.add_to_hass(hass) @@ -205,7 +200,6 @@ async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): entry = MockConfigEntry( domain=DOMAIN, title="OpenZWave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={"use_addon": True}, ) entry.add_to_hass(hass) @@ -216,7 +210,7 @@ async def test_setup_entry_without_addon_info(hass, get_addon_discovery_info): assert not await hass.config_entries.async_setup(entry.entry_id) assert mock_client.return_value.start_client.call_count == 0 - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY async def test_unload_entry_with_addon( @@ -226,20 +220,19 @@ async def test_unload_entry_with_addon( entry = MockConfigEntry( domain=DOMAIN, title="OpenZWave", - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={"use_addon": True}, ) entry.add_to_hass(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED with patch("homeassistant.components.ozw.MQTTClient", autospec=True) as mock_client: assert await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert mock_client.return_value.start_client.call_count == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED diff --git a/tests/components/ozw/test_websocket_api.py b/tests/components/ozw/test_websocket_api.py index 383d3425ffb..ad3b568b62a 100644 --- a/tests/components/ozw/test_websocket_api.py +++ b/tests/components/ozw/test_websocket_api.py @@ -153,7 +153,6 @@ async def test_websocket_api(hass, generic_data, hass_ws_client): # Test set config parameter config_param = result[0] - print(config_param) current_val = config_param[ATTR_VALUE] new_val = next( option[0] diff --git a/tests/components/panasonic_viera/test_init.py b/tests/components/panasonic_viera/test_init.py index 7351b4e5544..0f30e315683 100644 --- a/tests/components/panasonic_viera/test_init.py +++ b/tests/components/panasonic_viera/test_init.py @@ -7,7 +7,7 @@ from homeassistant.components.panasonic_viera.const import ( DEFAULT_NAME, DOMAIN, ) -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_HOST, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component @@ -209,7 +209,7 @@ async def test_setup_unload_entry(hass, mock_remote): await hass.async_block_till_done() await hass.config_entries.async_unload(mock_entry.entry_id) - assert mock_entry.state == ENTRY_STATE_NOT_LOADED + assert mock_entry.state is ConfigEntryState.NOT_LOADED state_tv = hass.states.get("media_player.panasonic_viera_tv") state_remote = hass.states.get("remote.panasonic_viera_tv") diff --git a/tests/components/philips_js/test_config_flow.py b/tests/components/philips_js/test_config_flow.py index 48230c72dc9..4841cd5a940 100644 --- a/tests/components/philips_js/test_config_flow.py +++ b/tests/components/philips_js/test_config_flow.py @@ -152,6 +152,7 @@ async def test_pairing(hass, mock_tv_pairable, mock_setup_entry): "title": "55PUS7181/12 (ABCDEFGHIJKLF)", "data": MOCK_CONFIG_PAIRED, "version": 1, + "options": {}, } await hass.async_block_till_done() diff --git a/tests/components/picnic/test_sensor.py b/tests/components/picnic/test_sensor.py index 08a2e0282c0..098d86785ab 100644 --- a/tests/components/picnic/test_sensor.py +++ b/tests/components/picnic/test_sensor.py @@ -10,7 +10,6 @@ import requests from homeassistant import config_entries from homeassistant.components.picnic import const from homeassistant.components.picnic.const import CONF_COUNTRY_CODE, SENSOR_TYPES -from homeassistant.config_entries import CONN_CLASS_CLOUD_POLL from homeassistant.const import ( CONF_ACCESS_TOKEN, CURRENCY_EURO, @@ -110,7 +109,6 @@ class TestPicnicSensor(unittest.IsolatedAsyncioTestCase): self.config_entry = MockConfigEntry( domain=const.DOMAIN, data=config_data, - connection_class=CONN_CLASS_CLOUD_POLL, unique_id="295-6y3-1nf4", ) self.config_entry.add_to_hass(self.hass) diff --git a/tests/components/plex/test_config_flow.py b/tests/components/plex/test_config_flow.py index 05bf15b4729..716864c1cb1 100644 --- a/tests/components/plex/test_config_flow.py +++ b/tests/components/plex/test_config_flow.py @@ -23,10 +23,10 @@ from homeassistant.components.plex.const import ( SERVERS, ) from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, SOURCE_INTEGRATION_DISCOVERY, SOURCE_REAUTH, SOURCE_USER, + ConfigEntryState, ) from homeassistant.const import ( CONF_HOST, @@ -354,7 +354,7 @@ async def test_all_available_servers_configured( async def test_option_flow(hass, entry, mock_plex_server): """Test config options flow selection.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None @@ -386,7 +386,7 @@ async def test_option_flow(hass, entry, mock_plex_server): async def test_missing_option_flow(hass, entry, mock_plex_server): """Test config options flow selection when no options stored.""" assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED result = await hass.config_entries.options.async_init( entry.entry_id, context={"source": "test"}, data=None @@ -676,7 +676,7 @@ async def test_setup_with_limited_credentials(hass, entry, setup_plex_server): assert plex_server.owner is None assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED async def test_integration_discovery(hass): @@ -707,7 +707,7 @@ async def test_trigger_reauth( """Test setup and reauthorization of a Plex token.""" await async_setup_component(hass, "persistent_notification", {}) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED with patch( "plexapi.server.PlexServer.clients", side_effect=plexapi.exceptions.Unauthorized @@ -716,7 +716,7 @@ async def test_trigger_reauth( await wait_for_debouncer(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state != ENTRY_STATE_LOADED + assert entry.state is not ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -741,7 +741,7 @@ async def test_trigger_reauth( assert len(hass.config_entries.flow.async_progress()) == 0 assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert entry.data[CONF_SERVER] == mock_plex_server.friendly_name assert entry.data[CONF_SERVER_IDENTIFIER] == mock_plex_server.machine_identifier assert entry.data[PLEX_SERVER_CONFIG][CONF_URL] == PLEX_DIRECT_URL diff --git a/tests/components/plex/test_init.py b/tests/components/plex/test_init.py index dc46c2ca771..530f265f3f0 100644 --- a/tests/components/plex/test_init.py +++ b/tests/components/plex/test_init.py @@ -13,12 +13,7 @@ from homeassistant.components.plex.models import ( TRANSIENT_SECTION, UNKNOWN_SECTION, ) -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( CONF_TOKEN, CONF_URL, @@ -38,7 +33,7 @@ from tests.common import MockConfigEntry, async_fire_time_changed async def test_set_config_entry_unique_id(hass, entry, mock_plex_server): """Test updating missing unique_id from config entry.""" assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert ( hass.config_entries.async_entries(const.DOMAIN)[0].unique_id @@ -57,7 +52,7 @@ async def test_setup_config_entry_with_error(hass, entry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY with patch( "homeassistant.components.plex.PlexServer.connect", @@ -68,7 +63,7 @@ async def test_setup_config_entry_with_error(hass, entry): await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server): @@ -80,7 +75,7 @@ async def test_setup_with_insecure_config_entry(hass, entry, setup_plex_server): await setup_plex_server(config_entry=entry) assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED async def test_unload_config_entry(hass, entry, mock_plex_server): @@ -88,7 +83,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): config_entries = hass.config_entries.async_entries(const.DOMAIN) assert len(config_entries) == 1 assert entry is config_entries[0] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED server_id = mock_plex_server.machine_identifier loaded_server = hass.data[const.DOMAIN][const.SERVERS][server_id] @@ -97,7 +92,7 @@ async def test_unload_config_entry(hass, entry, mock_plex_server): websocket = hass.data[const.DOMAIN][const.WEBSOCKETS][server_id] await hass.config_entries.async_unload(entry.entry_id) assert websocket.close.called - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_setup_with_photo_session(hass, entry, setup_plex_server): @@ -105,7 +100,7 @@ async def test_setup_with_photo_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="photo") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -124,7 +119,7 @@ async def test_setup_with_live_tv_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="live_tv") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -144,7 +139,7 @@ async def test_setup_with_transient_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="transient") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -164,7 +159,7 @@ async def test_setup_with_unknown_session(hass, entry, setup_plex_server): await setup_plex_server(session_type="unknown") assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.async_block_till_done() media_player = hass.states.get( @@ -226,7 +221,7 @@ async def test_setup_when_certificate_changed( assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() - assert old_entry.state == ENTRY_STATE_SETUP_ERROR + assert old_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with no servers found @@ -236,7 +231,7 @@ async def test_setup_when_certificate_changed( assert await hass.config_entries.async_setup(old_entry.entry_id) is False await hass.async_block_till_done() - assert old_entry.state == ENTRY_STATE_SETUP_ERROR + assert old_entry.state is ConfigEntryState.SETUP_ERROR await hass.config_entries.async_unload(old_entry.entry_id) # Test with success @@ -249,7 +244,7 @@ async def test_setup_when_certificate_changed( await hass.async_block_till_done() assert len(hass.config_entries.async_entries(const.DOMAIN)) == 1 - assert old_entry.state == ENTRY_STATE_LOADED + assert old_entry.state is ConfigEntryState.LOADED assert old_entry.data[const.PLEX_SERVER_CONFIG][CONF_URL] == new_url @@ -261,7 +256,7 @@ async def test_tokenless_server(entry, setup_plex_server): entry.data = TOKENLESS_DATA await setup_plex_server(config_entry=entry) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED async def test_bad_token_with_tokenless_server( @@ -272,7 +267,7 @@ async def test_bad_token_with_tokenless_server( await setup_plex_server() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED # Ensure updates that rely on account return nothing trigger_plex_update(mock_websocket) diff --git a/tests/components/plugwise/test_binary_sensor.py b/tests/components/plugwise/test_binary_sensor.py index 6df5b90878a..5d802fb42a0 100644 --- a/tests/components/plugwise/test_binary_sensor.py +++ b/tests/components/plugwise/test_binary_sensor.py @@ -1,6 +1,6 @@ """Tests for the Plugwise binary_sensor integration.""" -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_OFF, STATE_ON from tests.components.plugwise.common import async_init_integration @@ -9,7 +9,7 @@ from tests.components.plugwise.common import async_init_integration async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): """Test creation of climate related binary_sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.auxiliary_slave_boiler_state") assert str(state.state) == STATE_OFF @@ -21,7 +21,7 @@ async def test_anna_climate_binary_sensor_entities(hass, mock_smile_anna): async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): """Test change of climate related binary_sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED hass.states.async_set("binary_sensor.auxiliary_dhw_state", STATE_ON, {}) await hass.async_block_till_done() @@ -40,7 +40,7 @@ async def test_anna_climate_binary_sensor_change(hass, mock_smile_anna): async def test_adam_climate_binary_sensor_change(hass, mock_smile_adam): """Test change of climate related binary_sensor entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("binary_sensor.adam_plugwise_notification") assert str(state.state) == STATE_ON diff --git a/tests/components/plugwise/test_climate.py b/tests/components/plugwise/test_climate.py index e85140660fd..2fed3d18fd2 100644 --- a/tests/components/plugwise/test_climate.py +++ b/tests/components/plugwise/test_climate.py @@ -3,7 +3,7 @@ from plugwise.exceptions import PlugwiseException from homeassistant.components.climate.const import HVAC_MODE_AUTO, HVAC_MODE_HEAT -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.components.plugwise.common import async_init_integration @@ -11,7 +11,7 @@ from tests.components.plugwise.common import async_init_integration async def test_adam_climate_entity_attributes(hass, mock_smile_adam): """Test creation of adam climate device environment.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("climate.zone_lisa_wk") attrs = state.attributes @@ -50,7 +50,7 @@ async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam): mock_smile_adam.set_schedule_state.side_effect = PlugwiseException mock_smile_adam.set_temperature.side_effect = PlugwiseException entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "climate", @@ -85,7 +85,7 @@ async def test_adam_climate_adjust_negative_testing(hass, mock_smile_adam): async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): """Test handling of user requests in adam climate device environment.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "climate", @@ -138,7 +138,7 @@ async def test_adam_climate_entity_climate_changes(hass, mock_smile_adam): async def test_anna_climate_entity_attributes(hass, mock_smile_anna): """Test creation of anna climate device environment.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("climate.anna") attrs = state.attributes @@ -163,7 +163,7 @@ async def test_anna_climate_entity_attributes(hass, mock_smile_anna): async def test_anna_climate_entity_climate_changes(hass, mock_smile_anna): """Test handling of user requests in anna climate device environment.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "climate", diff --git a/tests/components/plugwise/test_init.py b/tests/components/plugwise/test_init.py index eded1e55406..c4f7e1c6b3d 100644 --- a/tests/components/plugwise/test_init.py +++ b/tests/components/plugwise/test_init.py @@ -5,11 +5,7 @@ import asyncio from plugwise.exceptions import XMLDataMissingError from homeassistant.components.plugwise.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from tests.common import AsyncMock, MockConfigEntry from tests.components.plugwise.common import async_init_integration @@ -18,34 +14,34 @@ from tests.components.plugwise.common import async_init_integration async def test_smile_unauthorized(hass, mock_smile_unauth): """Test failing unauthorization by Smile.""" entry = await async_init_integration(hass, mock_smile_unauth) - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR async def test_smile_error(hass, mock_smile_error): """Test server error handling by Smile.""" entry = await async_init_integration(hass, mock_smile_error) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_smile_notconnect(hass, mock_smile_notconnect): """Connection failure error handling by Smile.""" mock_smile_notconnect.connect.return_value = False entry = await async_init_integration(hass, mock_smile_notconnect) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_smile_timeout(hass, mock_smile_notconnect): """Timeout error handling by Smile.""" mock_smile_notconnect.connect.side_effect = asyncio.TimeoutError entry = await async_init_integration(hass, mock_smile_notconnect) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_smile_adam_xmlerror(hass, mock_smile_adam): """Detect malformed XML by Smile in Adam environment.""" mock_smile_adam.full_update_device.side_effect = XMLDataMissingError entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass, mock_smile_adam): @@ -55,7 +51,7 @@ async def test_unload_entry(hass, mock_smile_adam): mock_smile_adam.async_reset = AsyncMock(return_value=True) await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data[DOMAIN] @@ -66,4 +62,4 @@ async def test_async_setup_entry_fail(hass): entry.add_to_hass(hass) await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/plugwise/test_sensor.py b/tests/components/plugwise/test_sensor.py index a2bf4ebc50e..3b5bff781e5 100644 --- a/tests/components/plugwise/test_sensor.py +++ b/tests/components/plugwise/test_sensor.py @@ -1,6 +1,6 @@ """Tests for the Plugwise Sensor integration.""" -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.common import Mock from tests.components.plugwise.common import async_init_integration @@ -9,7 +9,7 @@ from tests.components.plugwise.common import async_init_integration async def test_adam_climate_sensor_entities(hass, mock_smile_adam): """Test creation of climate related sensor entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.adam_outdoor_temperature") assert float(state.state) == 7.81 @@ -34,7 +34,7 @@ async def test_adam_climate_sensor_entities(hass, mock_smile_adam): async def test_anna_as_smt_climate_sensor_entities(hass, mock_smile_anna): """Test creation of climate related sensor entities.""" entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.auxiliary_outdoor_temperature") assert float(state.state) == 18.0 @@ -50,7 +50,7 @@ async def test_anna_climate_sensor_entities(hass, mock_smile_anna): """Test creation of climate related sensor entities as single master thermostat.""" mock_smile_anna.single_master_thermostat.side_effect = Mock(return_value=False) entry = await async_init_integration(hass, mock_smile_anna) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.auxiliary_outdoor_temperature") assert float(state.state) == 18.0 @@ -59,7 +59,7 @@ async def test_anna_climate_sensor_entities(hass, mock_smile_anna): async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): """Test creation of power related sensor entities.""" entry = await async_init_integration(hass, mock_smile_p1) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.p1_net_electricity_point") assert float(state.state) == -2761.0 @@ -80,7 +80,7 @@ async def test_p1_dsmr_sensor_entities(hass, mock_smile_p1): async def test_stretch_sensor_entities(hass, mock_stretch): """Test creation of power related sensor entities.""" entry = await async_init_integration(hass, mock_stretch) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("sensor.koelkast_92c4a_electricity_consumed") assert float(state.state) == 50.5 diff --git a/tests/components/plugwise/test_switch.py b/tests/components/plugwise/test_switch.py index b7237a26150..6355362fbd9 100644 --- a/tests/components/plugwise/test_switch.py +++ b/tests/components/plugwise/test_switch.py @@ -2,7 +2,7 @@ from plugwise.exceptions import PlugwiseException -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from tests.components.plugwise.common import async_init_integration @@ -10,7 +10,7 @@ from tests.components.plugwise.common import async_init_integration async def test_adam_climate_switch_entities(hass, mock_smile_adam): """Test creation of climate related switch entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("switch.cv_pomp") assert str(state.state) == "on" @@ -23,7 +23,7 @@ async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam): """Test exceptions of climate related switch entities.""" mock_smile_adam.set_relay_state.side_effect = PlugwiseException entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "switch", @@ -47,7 +47,7 @@ async def test_adam_climate_switch_negative_testing(hass, mock_smile_adam): async def test_adam_climate_switch_changes(hass, mock_smile_adam): """Test changing of climate related switch entities.""" entry = await async_init_integration(hass, mock_smile_adam) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "switch", @@ -80,7 +80,7 @@ async def test_adam_climate_switch_changes(hass, mock_smile_adam): async def test_stretch_switch_entities(hass, mock_stretch): """Test creation of climate related switch entities.""" entry = await async_init_integration(hass, mock_stretch) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED state = hass.states.get("switch.koelkast_92c4a") assert str(state.state) == "on" @@ -92,7 +92,7 @@ async def test_stretch_switch_entities(hass, mock_stretch): async def test_stretch_switch_changes(hass, mock_stretch): """Test changing of power related switch entities.""" entry = await async_init_integration(hass, mock_stretch) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.services.async_call( "switch", diff --git a/tests/components/prometheus/test_init.py b/tests/components/prometheus/test_init.py index fe91c7c0002..b1abe1de3e5 100644 --- a/tests/components/prometheus/test_init.py +++ b/tests/components/prometheus/test_init.py @@ -47,14 +47,14 @@ async def prometheus_client(hass, hass_client): ) sensor1 = DemoSensor( - None, "Television Energy", 74, None, ENERGY_KILO_WATT_HOUR, None + None, "Television Energy", 74, None, None, ENERGY_KILO_WATT_HOUR, None ) sensor1.hass = hass sensor1.entity_id = "sensor.television_energy" await sensor1.async_update_ha_state() sensor2 = DemoSensor( - None, "Radio Energy", 14, DEVICE_CLASS_POWER, ENERGY_KILO_WATT_HOUR, None + None, "Radio Energy", 14, DEVICE_CLASS_POWER, None, ENERGY_KILO_WATT_HOUR, None ) sensor2.hass = hass sensor2.entity_id = "sensor.radio_energy" @@ -65,13 +65,19 @@ async def prometheus_client(hass, hass_client): await sensor2.async_update_ha_state() sensor3 = DemoSensor( - None, "Electricity price", 0.123, None, f"SEK/{ENERGY_KILO_WATT_HOUR}", None + None, + "Electricity price", + 0.123, + None, + None, + f"SEK/{ENERGY_KILO_WATT_HOUR}", + None, ) sensor3.hass = hass sensor3.entity_id = "sensor.electricity_price" await sensor3.async_update_ha_state() - sensor4 = DemoSensor(None, "Wind Direction", 25, None, DEGREE, None) + sensor4 = DemoSensor(None, "Wind Direction", 25, None, None, DEGREE, None) sensor4.hass = hass sensor4.entity_id = "sensor.wind_direction" await sensor4.async_update_ha_state() @@ -81,6 +87,7 @@ async def prometheus_client(hass, hass_client): "SPS30 PM <1µm Weight concentration", 3.7069, None, + None, CONCENTRATION_MICROGRAMS_PER_CUBIC_METER, None, ) diff --git a/tests/components/ps4/test_config_flow.py b/tests/components/ps4/test_config_flow.py index f8c28d236be..36c38a62b4c 100644 --- a/tests/components/ps4/test_config_flow.py +++ b/tests/components/ps4/test_config_flow.py @@ -64,7 +64,6 @@ MOCK_MANUAL = {"Config Mode": "Manual Entry", CONF_IP_ADDRESS: MOCK_HOST} MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", - "United States", "CA", "California", "San Diego", diff --git a/tests/components/ps4/test_init.py b/tests/components/ps4/test_init.py index cfe2f4b8e87..f57fada8c37 100644 --- a/tests/components/ps4/test_init.py +++ b/tests/components/ps4/test_init.py @@ -46,6 +46,7 @@ MOCK_FLOW_RESULT = { "type": data_entry_flow.RESULT_TYPE_CREATE_ENTRY, "title": "test_ps4", "data": MOCK_DATA, + "options": {}, } MOCK_ENTRY_ID = "SomeID" @@ -55,7 +56,6 @@ MOCK_CONFIG = MockConfigEntry(domain=DOMAIN, data=MOCK_DATA, entry_id=MOCK_ENTRY MOCK_LOCATION = location.LocationInfo( "0.0.0.0", "US", - "United States", "CA", "California", "San Diego", diff --git a/tests/components/pvpc_hourly_pricing/test_config_flow.py b/tests/components/pvpc_hourly_pricing/test_config_flow.py index 31a7005c4cc..2a64d81ef98 100644 --- a/tests/components/pvpc_hourly_pricing/test_config_flow.py +++ b/tests/components/pvpc_hourly_pricing/test_config_flow.py @@ -2,12 +2,11 @@ from datetime import datetime from unittest.mock import patch -from pytz import timezone - from homeassistant import config_entries, data_entry_flow from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.helpers import entity_registry as er +from homeassistant.util import dt as dt_util from .conftest import check_valid_state @@ -26,7 +25,7 @@ async def test_config_flow( - Check abort when trying to config another with same tariff - Check removal and add again to check state restoration """ - hass.config.time_zone = timezone("Europe/Madrid") + hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") mock_data = {"return_time": datetime(2019, 10, 26, 14, 0, tzinfo=date_util.UTC)} def mock_now(): diff --git a/tests/components/pvpc_hourly_pricing/test_sensor.py b/tests/components/pvpc_hourly_pricing/test_sensor.py index 2045ba52671..19f3a7aa31c 100644 --- a/tests/components/pvpc_hourly_pricing/test_sensor.py +++ b/tests/components/pvpc_hourly_pricing/test_sensor.py @@ -3,12 +3,11 @@ from datetime import datetime, timedelta import logging from unittest.mock import patch -from pytz import timezone - from homeassistant.components.pvpc_hourly_pricing import ATTR_TARIFF, DOMAIN from homeassistant.const import CONF_NAME from homeassistant.core import ATTR_NOW, EVENT_TIME_CHANGED from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from .conftest import check_valid_state @@ -32,7 +31,7 @@ async def test_sensor_availability( hass, caplog, legacy_patchable_time, pvpc_aioclient_mock: AiohttpClientMocker ): """Test sensor availability and handling of cloud access.""" - hass.config.time_zone = timezone("Europe/Madrid") + hass.config.time_zone = dt_util.get_time_zone("Europe/Madrid") config = {DOMAIN: [{CONF_NAME: "test_dst", ATTR_TARIFF: "discrimination"}]} mock_data = {"return_time": datetime(2019, 10, 27, 20, 0, 0, tzinfo=date_util.UTC)} diff --git a/tests/components/rainmachine/test_config_flow.py b/tests/components/rainmachine/test_config_flow.py index e79874831fe..1a015a5b181 100644 --- a/tests/components/rainmachine/test_config_flow.py +++ b/tests/components/rainmachine/test_config_flow.py @@ -1,16 +1,25 @@ """Define tests for the OpenUV config flow.""" -from unittest.mock import patch +from unittest.mock import AsyncMock, Mock, patch +import pytest from regenmaschine.errors import RainMachineError -from homeassistant import data_entry_flow -from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN, config_flow -from homeassistant.config_entries import SOURCE_USER +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.rainmachine import CONF_ZONE_RUN_TIME, DOMAIN from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_PORT, CONF_SSL from tests.common import MockConfigEntry +def _get_mock_client(): + mock_controller = Mock() + mock_controller.name = "My Rain Machine" + mock_controller.mac = "aa:bb:cc:dd:ee:ff" + return Mock( + load_local=AsyncMock(), controllers={"aa:bb:cc:dd:ee:ff": mock_controller} + ) + + async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" conf = { @@ -20,13 +29,19 @@ async def test_duplicate_error(hass): CONF_SSL: True, } - MockConfigEntry(domain=DOMAIN, unique_id="192.168.1.100", data=conf).add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf + ).add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=conf - ) + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=conf, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -40,16 +55,18 @@ async def test_invalid_password(hass): CONF_SSL: True, } - flow = config_flow.RainMachineFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} - with patch( "regenmaschine.client.Client.load_local", side_effect=RainMachineError, ): - result = await flow.async_step_user(user_input=conf) - assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=conf, + ) + await hass.async_block_till_done() + + assert result["errors"] == {CONF_PASSWORD: "invalid_auth"} async def test_options_flow(hass): @@ -88,11 +105,11 @@ async def test_options_flow(hass): async def test_show_form(hass): """Test that the form is served with no input.""" - flow = config_flow.RainMachineFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} - - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=None, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -107,22 +124,172 @@ async def test_step_user(hass): CONF_SSL: True, } - flow = config_flow.RainMachineFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_USER} + with patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=conf, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "My Rain Machine" + assert result["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + CONF_ZONE_RUN_TIME: 600, + } + assert mock_setup_entry.called + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] +) +async def test_step_homekit_zeroconf_ip_already_exists(hass, source): + """Test homekit and zeroconf with an ip that already exists.""" + conf = { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + + MockConfigEntry( + domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf + ).add_to_hass(hass) with patch( - "regenmaschine.client.Client.load_local", - return_value=True, + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), ): - result = await flow.async_step_user(user_input=conf) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={"host": "192.168.1.100"}, + ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "192.168.1.100" - assert result["data"] == { - CONF_IP_ADDRESS: "192.168.1.100", - CONF_PASSWORD: "password", - CONF_PORT: 8080, - CONF_SSL: True, - CONF_ZONE_RUN_TIME: 600, - } + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] +) +async def test_step_homekit_zeroconf_ip_change(hass, source): + """Test zeroconf with an ip change.""" + conf = { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + + entry = MockConfigEntry(domain=DOMAIN, unique_id="aa:bb:cc:dd:ee:ff", data=conf) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={"host": "192.168.1.2"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + assert entry.data[CONF_IP_ADDRESS] == "192.168.1.2" + + +@pytest.mark.parametrize( + "source", [config_entries.SOURCE_ZEROCONF, config_entries.SOURCE_HOMEKIT] +) +async def test_step_homekit_zeroconf_new_controller_when_some_exist(hass, source): + """Test homekit and zeroconf for a new controller when one already exists.""" + existing_conf = { + CONF_IP_ADDRESS: "192.168.1.3", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + } + entry = MockConfigEntry( + domain=DOMAIN, unique_id="zz:bb:cc:dd:ee:ff", data=existing_conf + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data={"host": "192.168.1.100"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.rainmachine.async_setup_entry", return_value=True + ) as mock_setup_entry, patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "My Rain Machine" + assert result2["data"] == { + CONF_IP_ADDRESS: "192.168.1.100", + CONF_PASSWORD: "password", + CONF_PORT: 8080, + CONF_SSL: True, + CONF_ZONE_RUN_TIME: 600, + } + assert mock_setup_entry.called + + +async def test_discovery_by_homekit_and_zeroconf_same_time(hass): + """Test the same controller gets discovered by two different methods.""" + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data={"host": "192.168.1.100"}, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch( + "homeassistant.components.rainmachine.config_flow.Client", + return_value=_get_mock_client(), + ): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "192.168.1.100"}, + ) + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" diff --git a/tests/components/recorder/conftest.py b/tests/components/recorder/conftest.py index 6b8c61d4d7d..2a29513a88e 100644 --- a/tests/components/recorder/conftest.py +++ b/tests/components/recorder/conftest.py @@ -3,9 +3,11 @@ from __future__ import annotations from collections.abc import AsyncGenerator from typing import Awaitable, Callable, cast +from unittest.mock import patch import pytest +from homeassistant.components import recorder from homeassistant.components.recorder import Recorder from homeassistant.components.recorder.const import DATA_INSTANCE from homeassistant.core import HomeAssistant @@ -13,47 +15,32 @@ from homeassistant.helpers.typing import ConfigType from .common import async_recorder_block_till_done -from tests.common import ( - async_init_recorder_component, - get_test_home_assistant, - init_recorder_component, -) +from tests.common import async_init_recorder_component SetupRecorderInstanceT = Callable[..., Awaitable[Recorder]] @pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() - - -@pytest.fixture -async def async_setup_recorder_instance() -> AsyncGenerator[ - SetupRecorderInstanceT, None -]: +async def async_setup_recorder_instance( + enable_statistics, +) -> AsyncGenerator[SetupRecorderInstanceT, None]: """Yield callable to setup recorder instance.""" async def async_setup_recorder( hass: HomeAssistant, config: ConfigType | None = None ) -> Recorder: """Setup and return recorder instance.""" # noqa: D401 - await async_init_recorder_component(hass, config) - await hass.async_block_till_done() - instance = cast(Recorder, hass.data[DATA_INSTANCE]) - await async_recorder_block_till_done(hass, instance) - assert isinstance(instance, Recorder) - return instance + stats = recorder.Recorder.async_hourly_statistics if enable_statistics else None + with patch( + "homeassistant.components.recorder.Recorder.async_hourly_statistics", + side_effect=stats, + autospec=True, + ): + await async_init_recorder_component(hass, config) + await hass.async_block_till_done() + instance = cast(Recorder, hass.data[DATA_INSTANCE]) + await async_recorder_block_till_done(hass, instance) + assert isinstance(instance, Recorder) + return instance yield async_setup_recorder diff --git a/tests/components/recorder/models_original.py b/tests/components/recorder/models_original.py index 4c9880d9257..5f64fbda736 100644 --- a/tests/components/recorder/models_original.py +++ b/tests/components/recorder/models_original.py @@ -170,5 +170,5 @@ def _process_timestamp(ts): if ts is None: return None if ts.tzinfo is None: - return dt_util.UTC.localize(ts) + return ts.replace(tzinfo=dt_util.UTC) return dt_util.as_utc(ts) diff --git a/tests/components/recorder/test_history.py b/tests/components/recorder/test_history.py new file mode 100644 index 00000000000..b2940f2bb39 --- /dev/null +++ b/tests/components/recorder/test_history.py @@ -0,0 +1,432 @@ +"""The tests the History component.""" +# pylint: disable=protected-access,invalid-name +from copy import copy +from datetime import timedelta +import json +from unittest.mock import patch, sentinel + +from homeassistant.components.recorder import history +from homeassistant.components.recorder.models import process_timestamp +import homeassistant.core as ha +from homeassistant.helpers.json import JSONEncoder +import homeassistant.util.dt as dt_util + +from tests.common import mock_state_change_event +from tests.components.recorder.common import wait_recording_done + + +def test_get_states(hass_recorder): + """Test getting states at a specific point in time.""" + hass = hass_recorder() + states = [] + + now = dt_util.utcnow() + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=now): + for i in range(5): + state = ha.State( + f"test.point_in_time_{i % 5}", + f"State {i}", + {"attribute_test": i}, + ) + + mock_state_change_event(hass, state) + + states.append(state) + + wait_recording_done(hass) + + future = now + timedelta(seconds=1) + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=future): + for i in range(5): + state = ha.State( + f"test.point_in_time_{i % 5}", + f"State {i}", + {"attribute_test": i}, + ) + + mock_state_change_event(hass, state) + + wait_recording_done(hass) + + # Get states returns everything before POINT + for state1, state2 in zip( + states, + sorted(history.get_states(hass, future), key=lambda state: state.entity_id), + ): + assert state1 == state2 + + # Test get_state here because we have a DB setup + assert states[0] == history.get_state(hass, future, states[0].entity_id) + + time_before_recorder_ran = now - timedelta(days=1000) + assert history.get_states(hass, time_before_recorder_ran) == [] + + assert history.get_state(hass, time_before_recorder_ran, "demo.id") is None + + +def test_state_changes_during_period(hass_recorder): + """Test state change during period.""" + hass = hass_recorder() + entity_id = "media_player.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() + point = start + timedelta(seconds=1) + end = point + timedelta(seconds=1) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("idle") + set_state("YouTube") + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + states = [ + set_state("idle"), + set_state("Netflix"), + set_state("Plex"), + set_state("YouTube"), + ] + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=end): + set_state("Netflix") + set_state("Plex") + + hist = history.state_changes_during_period(hass, start, end, entity_id) + + assert states == hist[entity_id] + + +def test_get_last_state_changes(hass_recorder): + """Test number of state changes.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + point2 = point + timedelta(minutes=1) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("1") + + states = [] + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + states.append(set_state("2")) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point2): + states.append(set_state("3")) + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert states == hist[entity_id] + + +def test_ensure_state_can_be_copied(hass_recorder): + """Ensure a state can pass though copy(). + + The filter integration uses copy() on states + from history. + """ + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state): + """Set the state.""" + hass.states.set(entity_id, state) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=2) + point = start + timedelta(minutes=1) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("1") + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=point): + set_state("2") + + hist = history.get_last_state_changes(hass, 2, entity_id) + + assert copy(hist[entity_id][0]) == hist[entity_id][0] + assert copy(hist[entity_id][1]) == hist[entity_id][1] + + +def test_get_significant_states(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert states == hist + + +def test_get_significant_states_minimal_response(hass_recorder): + """Test that only significant states are returned. + + When minimal responses is set only the first and + last states return a complete state. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four, minimal_response=True) + + # The second media_player.test state is reduced + # down to last_changed and state when minimal_response + # is set. We use JSONEncoder to make sure that are + # pre-encoded last_changed is always the same as what + # will happen with encoding a native state + input_state = states["media_player.test"][1] + orig_last_changed = json.dumps( + process_timestamp(input_state.last_changed), + cls=JSONEncoder, + ).replace('"', "") + orig_state = input_state.state + states["media_player.test"][1] = { + "last_changed": orig_last_changed, + "state": orig_state, + } + + assert states == hist + + +def test_get_significant_states_with_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + if entity_id == "media_player.test": + states[entity_id] = states[entity_id][1:] + for state in states[entity_id]: + if state.last_changed == one: + state.last_changed = one_and_half + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=True, + ) + assert states == hist + + +def test_get_significant_states_without_initial(hass_recorder): + """Test that only significant states are returned. + + We should get back every thermostat change that + includes an attribute change, but only the state updates for + media player (attribute changes are not significant and not returned). + """ + hass = hass_recorder() + zero, four, states = record_states(hass) + one = zero + timedelta(seconds=1) + one_and_half = zero + timedelta(seconds=1.5) + for entity_id in states: + states[entity_id] = list( + filter(lambda s: s.last_changed != one, states[entity_id]) + ) + del states["media_player.test2"] + + hist = history.get_significant_states( + hass, + one_and_half, + four, + include_start_time_state=False, + ) + assert states == hist + + +def test_get_significant_states_entity_id(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states(hass, zero, four, ["media_player.test"]) + assert states == hist + + +def test_get_significant_states_multiple_entity_ids(hass_recorder): + """Test that only significant states are returned for one entity.""" + hass = hass_recorder() + zero, four, states = record_states(hass) + del states["media_player.test2"] + del states["media_player.test3"] + del states["thermostat.test2"] + del states["script.can_cancel_this_one"] + + hist = history.get_significant_states( + hass, + zero, + four, + ["media_player.test", "thermostat.test"], + ) + assert states == hist + + +def test_get_significant_states_are_ordered(hass_recorder): + """Test order of results from get_significant_states. + + When entity ids are given, the results should be returned with the data + in the same order. + """ + hass = hass_recorder() + zero, four, _states = record_states(hass) + entity_ids = ["media_player.test", "media_player.test2"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + entity_ids = ["media_player.test2", "media_player.test"] + hist = history.get_significant_states(hass, zero, four, entity_ids) + assert list(hist.keys()) == entity_ids + + +def test_get_significant_states_only(hass_recorder): + """Test significant states when significant_states_only is set.""" + hass = hass_recorder() + entity_id = "sensor.test" + + def set_state(state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + start = dt_util.utcnow() - timedelta(minutes=4) + points = [] + for i in range(1, 4): + points.append(start + timedelta(minutes=i)) + + states = [] + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=start): + set_state("123", attributes={"attribute": 10.64}) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[0] + ): + # Attributes are different, state not + states.append(set_state("123", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[1] + ): + # state is different, attributes not + states.append(set_state("32", attributes={"attribute": 21.42})) + + with patch( + "homeassistant.components.recorder.dt_util.utcnow", return_value=points[2] + ): + # everything is different + states.append(set_state("412", attributes={"attribute": 54.23})) + + hist = history.get_significant_states(hass, start, significant_changes_only=True) + + assert len(hist[entity_id]) == 2 + assert states[0] not in hist[entity_id] + assert states[1] in hist[entity_id] + assert states[2] in hist[entity_id] + + hist = history.get_significant_states(hass, start, significant_changes_only=False) + + assert len(hist[entity_id]) == 3 + assert states == hist[entity_id] + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates from media player, zone and + thermostat. + """ + mp = "media_player.test" + mp2 = "media_player.test2" + mp3 = "media_player.test3" + therm = "thermostat.test" + therm2 = "thermostat.test2" + zone = "zone.home" + script_c = "script.can_cancel_this_one" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(seconds=1) + two = one + timedelta(seconds=1) + three = two + timedelta(seconds=1) + four = three + timedelta(seconds=1) + + states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp2].append( + set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[mp3].append( + set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[therm].append( + set_state(therm, 20, attributes={"current_temperature": 19.5}) + ) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + # This state will be skipped only different in time + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}) + # This state will be skipped because domain is excluded + set_state(zone, "zoning") + states[script_c].append( + set_state(script_c, "off", attributes={"can_cancel": True}) + ) + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 19.8}) + ) + states[therm2].append( + set_state(therm2, 20, attributes={"current_temperature": 19}) + ) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[mp].append( + set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}) + ) + states[mp3].append( + set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}) + ) + # Attributes changed even though state is the same + states[therm].append( + set_state(therm, 21, attributes={"current_temperature": 20}) + ) + + return zero, four, states diff --git a/tests/components/recorder/test_init.py b/tests/components/recorder/test_init.py index a34df0a4ac2..195e56dc748 100644 --- a/tests/components/recorder/test_init.py +++ b/tests/components/recorder/test_init.py @@ -4,10 +4,12 @@ from datetime import datetime, timedelta import sqlite3 from unittest.mock import patch +import pytest from sqlalchemy.exc import DatabaseError, OperationalError, SQLAlchemyError from homeassistant.components import recorder from homeassistant.components.recorder import ( + CONF_AUTO_PURGE, CONF_DB_URL, CONFIG_SCHEMA, DOMAIN, @@ -15,6 +17,7 @@ from homeassistant.components.recorder import ( SERVICE_DISABLE, SERVICE_ENABLE, SERVICE_PURGE, + SERVICE_PURGE_ENTITIES, SQLITE_URL_PREFIX, Recorder, run_information, @@ -590,7 +593,7 @@ def run_tasks_at_time(hass, test_time): def test_auto_purge(hass_recorder): - """Test periodic purge alarm scheduling.""" + """Test periodic purge scheduling.""" hass = hass_recorder() original_tz = dt_util.DEFAULT_TIME_ZONE @@ -598,41 +601,136 @@ def test_auto_purge(hass_recorder): tz = dt_util.get_time_zone("Europe/Copenhagen") dt_util.set_default_time_zone(tz) - # Purging is schedule to happen at 4:12am every day. Exercise this behavior - # by firing alarms and advancing the clock around this time. Pick an arbitrary - # year in the future to avoid boundary conditions relative to the current date. + # Purging is scheduled to happen at 4:12am every day. Exercise this behavior by + # firing time changed events and advancing the clock around this time. Pick an + # arbitrary year in the future to avoid boundary conditions relative to the current + # date. # # The clock is started at 4:15am then advanced forward below now = dt_util.utcnow() - test_time = tz.localize(datetime(now.year + 2, 1, 1, 4, 15, 0)) + test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) run_tasks_at_time(hass, test_time) with patch( "homeassistant.components.recorder.purge.purge_old_data", return_value=True - ) as purge_old_data: + ) as purge_old_data, patch( + "homeassistant.components.recorder.perodic_db_cleanups" + ) as perodic_db_cleanups: # Advance one day, and the purge task should run test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 + assert len(perodic_db_cleanups.mock_calls) == 1 purge_old_data.reset_mock() + perodic_db_cleanups.reset_mock() # Advance one day, and the purge task should run again test_time = test_time + timedelta(days=1) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 + assert len(perodic_db_cleanups.mock_calls) == 1 purge_old_data.reset_mock() + perodic_db_cleanups.reset_mock() # Advance less than one full day. The alarm should not yet fire. test_time = test_time + timedelta(hours=23) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 0 + assert len(perodic_db_cleanups.mock_calls) == 0 # Advance to the next day and fire the alarm again test_time = test_time + timedelta(hours=1) run_tasks_at_time(hass, test_time) assert len(purge_old_data.mock_calls) == 1 + assert len(perodic_db_cleanups.mock_calls) == 1 + + dt_util.set_default_time_zone(original_tz) + + +def test_auto_purge_disabled(hass_recorder): + """Test periodic db cleanup still run when auto purge is disabled.""" + hass = hass_recorder({CONF_AUTO_PURGE: False}) + + original_tz = dt_util.DEFAULT_TIME_ZONE + + tz = dt_util.get_time_zone("Europe/Copenhagen") + dt_util.set_default_time_zone(tz) + + # Purging is scheduled to happen at 4:12am every day. We want + # to verify that when auto purge is disabled perodic db cleanups + # are still scheduled + # + # The clock is started at 4:15am then advanced forward below + now = dt_util.utcnow() + test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) + run_tasks_at_time(hass, test_time) + + with patch( + "homeassistant.components.recorder.purge.purge_old_data", return_value=True + ) as purge_old_data, patch( + "homeassistant.components.recorder.perodic_db_cleanups" + ) as perodic_db_cleanups: + # Advance one day, and the purge task should run + test_time = test_time + timedelta(days=1) + run_tasks_at_time(hass, test_time) + assert len(purge_old_data.mock_calls) == 0 + assert len(perodic_db_cleanups.mock_calls) == 1 + + purge_old_data.reset_mock() + perodic_db_cleanups.reset_mock() + + dt_util.set_default_time_zone(original_tz) + + +@pytest.mark.parametrize("enable_statistics", [True]) +def test_auto_statistics(hass_recorder): + """Test periodic statistics scheduling.""" + hass = hass_recorder() + + original_tz = dt_util.DEFAULT_TIME_ZONE + + tz = dt_util.get_time_zone("Europe/Copenhagen") + dt_util.set_default_time_zone(tz) + + # Statistics is scheduled to happen at *:12am every hour. Exercise this behavior by + # firing time changed events and advancing the clock around this time. Pick an + # arbitrary year in the future to avoid boundary conditions relative to the current + # date. + # + # The clock is started at 4:15am then advanced forward below + now = dt_util.utcnow() + test_time = datetime(now.year + 2, 1, 1, 4, 15, 0, tzinfo=tz) + run_tasks_at_time(hass, test_time) + + with patch( + "homeassistant.components.recorder.statistics.compile_statistics", + return_value=True, + ) as compile_statistics: + # Advance one hour, and the statistics task should run + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 1 + + compile_statistics.reset_mock() + + # Advance one hour, and the statistics task should run again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 1 + + compile_statistics.reset_mock() + + # Advance less than one full hour. The task should not run. + test_time = test_time + timedelta(minutes=50) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 0 + + # Advance to the next hour, and the statistics task should run again + test_time = test_time + timedelta(hours=1) + run_tasks_at_time(hass, test_time) + assert len(compile_statistics.mock_calls) == 1 dt_util.set_default_time_zone(original_tz) @@ -725,6 +823,7 @@ def test_has_services(hass_recorder): assert hass.services.has_service(DOMAIN, SERVICE_DISABLE) assert hass.services.has_service(DOMAIN, SERVICE_ENABLE) assert hass.services.has_service(DOMAIN, SERVICE_PURGE) + assert hass.services.has_service(DOMAIN, SERVICE_PURGE_ENTITIES) def test_service_disable_events_not_recording(hass, hass_recorder): diff --git a/tests/components/recorder/test_models.py b/tests/components/recorder/test_models.py index 26a1e487ba8..9f32f1c5746 100644 --- a/tests/components/recorder/test_models.py +++ b/tests/components/recorder/test_models.py @@ -2,7 +2,6 @@ from datetime import datetime import pytest -import pytz from sqlalchemy import create_engine from sqlalchemy.orm import scoped_session, sessionmaker @@ -144,11 +143,11 @@ async def test_process_timestamp(): """Test processing time stamp to UTC.""" datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC) datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0) - est = pytz.timezone("US/Eastern") + est = dt_util.get_time_zone("US/Eastern") datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) - nst = pytz.timezone("Canada/Newfoundland") + nst = dt_util.get_time_zone("Canada/Newfoundland") datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst) - hst = pytz.timezone("US/Hawaii") + hst = dt_util.get_time_zone("US/Hawaii") datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst) assert process_timestamp(datetime_with_tzinfo) == datetime( @@ -158,13 +157,13 @@ async def test_process_timestamp(): 2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC ) assert process_timestamp(datetime_est_timezone) == datetime( - 2016, 7, 9, 15, 56, tzinfo=dt.UTC + 2016, 7, 9, 15, 0, tzinfo=dt.UTC ) assert process_timestamp(datetime_nst_timezone) == datetime( - 2016, 7, 9, 14, 31, tzinfo=dt.UTC + 2016, 7, 9, 13, 30, tzinfo=dt.UTC ) assert process_timestamp(datetime_hst_timezone) == datetime( - 2016, 7, 9, 21, 31, tzinfo=dt.UTC + 2016, 7, 9, 21, 0, tzinfo=dt.UTC ) assert process_timestamp(None) is None @@ -173,13 +172,13 @@ async def test_process_timestamp_to_utc_isoformat(): """Test processing time stamp to UTC isoformat.""" datetime_with_tzinfo = datetime(2016, 7, 9, 11, 0, 0, tzinfo=dt.UTC) datetime_without_tzinfo = datetime(2016, 7, 9, 11, 0, 0) - est = pytz.timezone("US/Eastern") + est = dt_util.get_time_zone("US/Eastern") datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) - est = pytz.timezone("US/Eastern") + est = dt_util.get_time_zone("US/Eastern") datetime_est_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=est) - nst = pytz.timezone("Canada/Newfoundland") + nst = dt_util.get_time_zone("Canada/Newfoundland") datetime_nst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=nst) - hst = pytz.timezone("US/Hawaii") + hst = dt_util.get_time_zone("US/Hawaii") datetime_hst_timezone = datetime(2016, 7, 9, 11, 0, 0, tzinfo=hst) assert ( @@ -192,15 +191,15 @@ async def test_process_timestamp_to_utc_isoformat(): ) assert ( process_timestamp_to_utc_isoformat(datetime_est_timezone) - == "2016-07-09T15:56:00+00:00" + == "2016-07-09T15:00:00+00:00" ) assert ( process_timestamp_to_utc_isoformat(datetime_nst_timezone) - == "2016-07-09T14:31:00+00:00" + == "2016-07-09T13:30:00+00:00" ) assert ( process_timestamp_to_utc_isoformat(datetime_hst_timezone) - == "2016-07-09T21:31:00+00:00" + == "2016-07-09T21:00:00+00:00" ) assert process_timestamp_to_utc_isoformat(None) is None diff --git a/tests/components/recorder/test_purge.py b/tests/components/recorder/test_purge.py index 23164bd73f5..6727b4da495 100644 --- a/tests/components/recorder/test_purge.py +++ b/tests/components/recorder/test_purge.py @@ -104,7 +104,7 @@ async def test_purge_old_states_encounters_temporary_mysql_error( mysql_exception.orig = MagicMock(args=(1205, "retryable")) with patch( - "homeassistant.components.recorder.purge.time.sleep" + "homeassistant.components.recorder.util.time.sleep" ) as sleep_mock, patch( "homeassistant.components.recorder.purge._purge_old_recorder_runs", side_effect=[mysql_exception, None], @@ -147,7 +147,7 @@ async def test_purge_old_states_encounters_operational_error( await async_wait_recording_done_without_instance(hass) assert "retrying" not in caplog.text - assert "Error purging history" in caplog.text + assert "Error executing purge" in caplog.text async def test_purge_old_events( @@ -653,6 +653,122 @@ async def test_purge_filtered_events_state_changed( assert session.query(States).get(63).old_state_id == 62 # should have been kept +async def test_purge_entities( + hass: HomeAssistant, async_setup_recorder_instance: SetupRecorderInstanceT +): + """Test purging of specific entities.""" + instance = await async_setup_recorder_instance(hass) + + async def _purge_entities(hass, entity_ids, domains, entity_globs): + service_data = { + "entity_id": entity_ids, + "domains": domains, + "entity_globs": entity_globs, + } + + await hass.services.async_call( + recorder.DOMAIN, recorder.SERVICE_PURGE_ENTITIES, service_data + ) + await hass.async_block_till_done() + + await async_recorder_block_till_done(hass, instance) + await async_wait_purge_done(hass, instance) + + def _add_purge_records(hass: HomeAssistant) -> None: + with recorder.session_scope(hass=hass) as session: + # Add states and state_changed events that should be purged + for days in range(1, 4): + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(1000, 1020): + _add_state_and_state_changed_event( + session, + "sensor.purge_entity", + "purgeme", + timestamp, + event_id * days, + ) + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(10000, 10020): + _add_state_and_state_changed_event( + session, + "purge_domain.entity", + "purgeme", + timestamp, + event_id * days, + ) + timestamp = dt_util.utcnow() - timedelta(days=days) + for event_id in range(100000, 100020): + _add_state_and_state_changed_event( + session, + "binary_sensor.purge_glob", + "purgeme", + timestamp, + event_id * days, + ) + + def _add_keep_records(hass: HomeAssistant) -> None: + with recorder.session_scope(hass=hass) as session: + # Add states and state_changed events that should be kept + timestamp = dt_util.utcnow() - timedelta(days=2) + for event_id in range(200, 210): + _add_state_and_state_changed_event( + session, + "sensor.keep", + "keep", + timestamp, + event_id, + ) + + _add_purge_records(hass) + _add_keep_records(hass) + + # Confirm standard service call + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 190 + + await _purge_entities( + hass, "sensor.purge_entity", "purge_domain", "*purge_glob" + ) + assert states.count() == 10 + + states_sensor_kept = session.query(States).filter( + States.entity_id == "sensor.keep" + ) + assert states_sensor_kept.count() == 10 + + _add_purge_records(hass) + + # Confirm each parameter purges only the associated records + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 190 + + await _purge_entities(hass, "sensor.purge_entity", [], []) + assert states.count() == 130 + + await _purge_entities(hass, [], "purge_domain", []) + assert states.count() == 70 + + await _purge_entities(hass, [], [], "*purge_glob") + assert states.count() == 10 + + states_sensor_kept = session.query(States).filter( + States.entity_id == "sensor.keep" + ) + assert states_sensor_kept.count() == 10 + + _add_purge_records(hass) + + # Confirm calling service without arguments matches all records (default filter behaviour) + with session_scope(hass=hass) as session: + states = session.query(States) + assert states.count() == 190 + + await _purge_entities(hass, [], [], []) + assert states.count() == 0 + + async def _add_test_states(hass: HomeAssistant, instance: recorder.Recorder): """Add multiple states to the db for testing.""" utcnow = dt_util.utcnow() diff --git a/tests/components/recorder/test_statistics.py b/tests/components/recorder/test_statistics.py new file mode 100644 index 00000000000..1ec0f2284b4 --- /dev/null +++ b/tests/components/recorder/test_statistics.py @@ -0,0 +1,93 @@ +"""The tests for sensor recorder platform.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from unittest.mock import patch, sentinel + +from pytest import approx + +from homeassistant.components.recorder import history +from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.components.recorder.common import wait_recording_done + + +def test_compile_hourly_statistics(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(14.915254237288135), + "min": approx(10.0), + "max": approx(20.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates temperature sensors. + """ + mp = "media_player.test" + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns2_attr = {"device_class": "temperature"} + sns3_attr = {} + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=1) + two = one + timedelta(minutes=15) + three = two + timedelta(minutes=30) + four = three + timedelta(minutes=15) + + states = {mp: [], sns1: [], sns2: [], sns3: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "20", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "20", attributes=sns3_attr)) + + return zero, four, states diff --git a/tests/components/recorder/test_util.py b/tests/components/recorder/test_util.py index b5c5b68fe3f..5b4b234fbbb 100644 --- a/tests/components/recorder/test_util.py +++ b/tests/components/recorder/test_util.py @@ -15,28 +15,7 @@ from homeassistant.util import dt as dt_util from .common import corrupt_db_file -from tests.common import ( - async_init_recorder_component, - get_test_home_assistant, - init_recorder_component, -) - - -@pytest.fixture -def hass_recorder(): - """Home Assistant fixture with in-memory recorder.""" - hass = get_test_home_assistant() - - def setup_recorder(config=None): - """Set up with params.""" - init_recorder_component(hass, config) - hass.start() - hass.block_till_done() - hass.data[DATA_INSTANCE].block_till_done() - return hass - - yield setup_recorder - hass.stop() +from tests.common import async_init_recorder_component def test_session_scope_not_setup(hass_recorder): @@ -152,7 +131,7 @@ def test_setup_connection_for_dialect_mysql(): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - assert util.setup_connection_for_dialect("mysql", dbapi_connection) is False + util.setup_connection_for_dialect("mysql", dbapi_connection, True) assert execute_mock.call_args[0][0] == "SET session wait_timeout=28800" @@ -167,9 +146,17 @@ def test_setup_connection_for_dialect_sqlite(): dbapi_connection = MagicMock(cursor=_make_cursor_mock) - assert util.setup_connection_for_dialect("sqlite", dbapi_connection) is True + util.setup_connection_for_dialect("sqlite", dbapi_connection, True) - assert execute_mock.call_args[0][0] == "PRAGMA journal_mode=WAL" + assert len(execute_mock.call_args_list) == 2 + assert execute_mock.call_args_list[0][0][0] == "PRAGMA journal_mode=WAL" + assert execute_mock.call_args_list[1][0][0] == "PRAGMA cache_size = -8192" + + execute_mock.reset_mock() + util.setup_connection_for_dialect("sqlite", dbapi_connection, False) + + assert len(execute_mock.call_args_list) == 1 + assert execute_mock.call_args_list[0][0][0] == "PRAGMA cache_size = -8192" def test_basic_sanity_check(hass_recorder): @@ -261,3 +248,11 @@ def test_end_incomplete_runs(hass_recorder, caplog): assert run_info.end == now_without_tz assert "Ended unfinished session" in caplog.text + + +def test_perodic_db_cleanups(hass_recorder): + """Test perodic db cleanups.""" + hass = hass_recorder() + with patch.object(hass.data[DATA_INSTANCE].engine, "execute") as execute_mock: + util.perodic_db_cleanups(hass.data[DATA_INSTANCE]) + assert execute_mock.call_args[0][0] == "PRAGMA wal_checkpoint(TRUNCATE);" diff --git a/tests/components/remote/test_device_action.py b/tests/components/remote/test_device_action.py index 7cd5a632982..1193764da3a 100644 --- a/tests/components/remote/test_device_action.py +++ b/tests/components/remote/test_device_action.py @@ -70,7 +70,7 @@ async def test_get_actions(hass, device_reg, entity_reg): assert actions == expected_actions -async def test_action(hass, calls): +async def test_action(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/remote/test_device_condition.py b/tests/components/remote/test_device_condition.py index 12cf0e05493..6f3c0e1c0a2 100644 --- a/tests/components/remote/test_device_condition.py +++ b/tests/components/remote/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -165,7 +165,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/remote/test_device_trigger.py b/tests/components/remote/test_device_trigger.py index 616c356936c..3afa731cf59 100644 --- a/tests/components/remote/test_device_trigger.py +++ b/tests/components/remote/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -176,7 +176,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/rfxtrx/test_switch.py b/tests/components/rfxtrx/test_switch.py index ee4fd265fc9..12064911bb6 100644 --- a/tests/components/rfxtrx/test_switch.py +++ b/tests/components/rfxtrx/test_switch.py @@ -3,6 +3,7 @@ from unittest.mock import call import pytest +from homeassistant import config_entries from homeassistant.components.rfxtrx import DOMAIN from homeassistant.core import State @@ -168,4 +169,4 @@ async def test_unknown_event_code(hass, rfxtrx): assert len(conf_entries) == 1 entry = conf_entries[0] - assert entry.state == "loaded" + assert entry.state == config_entries.ConfigEntryState.LOADED diff --git a/tests/components/roku/test_init.py b/tests/components/roku/test_init.py index be9131d5f91..fc624f5cb64 100644 --- a/tests/components/roku/test_init.py +++ b/tests/components/roku/test_init.py @@ -2,11 +2,7 @@ from unittest.mock import patch from homeassistant.components.roku.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.roku import setup_integration @@ -19,7 +15,7 @@ async def test_config_entry_not_ready( """Test the Roku configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( @@ -36,10 +32,10 @@ async def test_unload_config_entry( entry = await setup_integration(hass, aioclient_mock) assert hass.data[DOMAIN][entry.entry_id] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/ruckus_unleashed/test_init.py b/tests/components/ruckus_unleashed/test_init.py index 0340f72891a..e9ac9ec7cd8 100644 --- a/tests/components/ruckus_unleashed/test_init.py +++ b/tests/components/ruckus_unleashed/test_init.py @@ -14,11 +14,7 @@ from homeassistant.components.ruckus_unleashed import ( DOMAIN, MANUFACTURER, ) -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.helpers import device_registry as dr from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC @@ -56,7 +52,7 @@ async def test_setup_entry_connection_error(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_router_device_setup(hass): @@ -84,12 +80,12 @@ async def test_unload_entry(hass): entry = await init_integration(hass) assert len(hass.config_entries.async_entries(DOMAIN)) == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert not hass.data.get(DOMAIN) @@ -113,4 +109,4 @@ async def test_config_not_ready_during_setup(hass): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY diff --git a/tests/components/samsungtv/__init__.py b/tests/components/samsungtv/__init__.py index 4ad1622c6ca..84328736822 100644 --- a/tests/components/samsungtv/__init__.py +++ b/tests/components/samsungtv/__init__.py @@ -1 +1,15 @@ """Tests for the samsungtv component.""" +from homeassistant.components.samsungtv.const import DOMAIN as SAMSUNGTV_DOMAIN +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + + +async def setup_samsungtv(hass: HomeAssistant, config: dict): + """Set up mock Samsung TV.""" + await async_setup_component(hass, "persistent_notification", {}) + entry = MockConfigEntry(domain=SAMSUNGTV_DOMAIN, data=config) + entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() diff --git a/tests/components/samsungtv/conftest.py b/tests/components/samsungtv/conftest.py new file mode 100644 index 00000000000..278c6d7f18a --- /dev/null +++ b/tests/components/samsungtv/conftest.py @@ -0,0 +1,112 @@ +"""Fixtures for Samsung TV.""" +from unittest.mock import Mock, patch + +import pytest + +import homeassistant.util.dt as dt_util + +RESULT_ALREADY_CONFIGURED = "already_configured" +RESULT_ALREADY_IN_PROGRESS = "already_in_progress" + + +@pytest.fixture(name="remote") +def remote_fixture(): + """Patch the samsungctl Remote.""" + with patch( + "homeassistant.components.samsungtv.bridge.Remote" + ) as remote_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remote = Mock() + remote.__enter__ = Mock() + remote.__exit__ = Mock() + remote_class.return_value = remote + yield remote + + +@pytest.fixture(name="remotews") +def remotews_fixture(): + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() + remotews.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + "networkType": "wireless", + }, + } + remotews_class.return_value = remotews + remotews_class().__enter__().token = "FAKE_TOKEN" + yield remotews + + +@pytest.fixture(name="remotews_no_device_info") +def remotews_no_device_info_fixture(): + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() + remotews.rest_device_info.return_value = None + remotews_class.return_value = remotews + remotews_class().__enter__().token = "FAKE_TOKEN" + yield remotews + + +@pytest.fixture(name="remotews_soundbar") +def remotews_soundbar_fixture(): + """Patch the samsungtvws SamsungTVWS.""" + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" + ) as remotews_class, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + remotews = Mock() + remotews.__enter__ = Mock() + remotews.__exit__ = Mock() + remotews.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "mac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SoundBar", + }, + } + remotews_class.return_value = remotews + remotews_class().__enter__().token = "FAKE_TOKEN" + yield remotews + + +@pytest.fixture(name="delay") +def delay_fixture(): + """Patch the delay script function.""" + with patch( + "homeassistant.components.samsungtv.media_player.Script.async_run" + ) as delay: + yield delay + + +@pytest.fixture +def mock_now(): + """Fixture for dtutil.now.""" + return dt_util.utcnow() diff --git a/tests/components/samsungtv/test_config_flow.py b/tests/components/samsungtv/test_config_flow.py index fb1b2a2bc67..5b85ecf7048 100644 --- a/tests/components/samsungtv/test_config_flow.py +++ b/tests/components/samsungtv/test_config_flow.py @@ -1,16 +1,27 @@ """Tests for Samsung TV config flow.""" -from unittest.mock import DEFAULT as DEFAULT_MOCK, Mock, PropertyMock, call, patch +import socket +from unittest.mock import Mock, PropertyMock, call, patch -import pytest from samsungctl.exceptions import AccessDenied, UnhandledResponse from samsungtvws.exceptions import ConnectionFailure -from websocket import WebSocketProtocolException +from websocket import WebSocketException, WebSocketProtocolException from homeassistant import config_entries +from homeassistant.components.dhcp import IP_ADDRESS, MAC_ADDRESS from homeassistant.components.samsungtv.const import ( + ATTR_PROPERTIES, CONF_MANUFACTURER, CONF_MODEL, + DEFAULT_MANUFACTURER, DOMAIN, + METHOD_LEGACY, + METHOD_WEBSOCKET, + RESULT_AUTH_MISSING, + RESULT_CANNOT_CONNECT, + RESULT_NOT_SUPPORTED, + RESULT_UNKNOWN_HOST, + TIMEOUT_REQUEST, + TIMEOUT_WEBSOCKET, ) from homeassistant.components.ssdp import ( ATTR_SSDP_LOCATION, @@ -19,22 +30,84 @@ from homeassistant.components.ssdp import ( ATTR_UPNP_MODEL_NAME, ATTR_UPNP_UDN, ) -from homeassistant.const import CONF_HOST, CONF_ID, CONF_METHOD, CONF_NAME, CONF_TOKEN +from homeassistant.const import ( + CONF_HOST, + CONF_ID, + CONF_IP_ADDRESS, + CONF_MAC, + CONF_METHOD, + CONF_NAME, + CONF_PORT, + CONF_TOKEN, +) +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component +from tests.common import MockConfigEntry +from tests.components.samsungtv.conftest import ( + RESULT_ALREADY_CONFIGURED, + RESULT_ALREADY_IN_PROGRESS, +) + +MOCK_IMPORT_DATA = { + CONF_HOST: "fake_host", + CONF_NAME: "fake", + CONF_PORT: 55000, +} +MOCK_IMPORT_DATA_WITHOUT_NAME = { + CONF_HOST: "fake_host", +} +MOCK_IMPORT_WSDATA = { + CONF_HOST: "fake_host", + CONF_NAME: "fake", + CONF_PORT: 8002, +} MOCK_USER_DATA = {CONF_HOST: "fake_host", CONF_NAME: "fake_name"} MOCK_SSDP_DATA = { ATTR_SSDP_LOCATION: "https://fake_host:12345/test", - ATTR_UPNP_FRIENDLY_NAME: "[TV]fake_name", - ATTR_UPNP_MANUFACTURER: "fake_manufacturer", + ATTR_UPNP_FRIENDLY_NAME: "[TV] fake_name", + ATTR_UPNP_MANUFACTURER: "Samsung fake_manufacturer", ATTR_UPNP_MODEL_NAME: "fake_model", - ATTR_UPNP_UDN: "uuid:fake_uuid", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172de", } MOCK_SSDP_DATA_NOPREFIX = { ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", ATTR_UPNP_FRIENDLY_NAME: "fake2_name", - ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", + ATTR_UPNP_MANUFACTURER: "Samsung fake2_manufacturer", ATTR_UPNP_MODEL_NAME: "fake2_model", - ATTR_UPNP_UDN: "fake2_uuid", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", +} +MOCK_SSDP_DATA_WRONGMODEL = { + ATTR_SSDP_LOCATION: "http://fake2_host:12345/test", + ATTR_UPNP_FRIENDLY_NAME: "fake2_name", + ATTR_UPNP_MANUFACTURER: "fake2_manufacturer", + ATTR_UPNP_MODEL_NAME: "HW-Qfake", + ATTR_UPNP_UDN: "uuid:0d1cef00-00dc-1000-9c80-4844f7b172df", +} +MOCK_DHCP_DATA = {IP_ADDRESS: "fake_host", MAC_ADDRESS: "aa:bb:cc:dd:ee:ff"} +MOCK_ZEROCONF_DATA = { + CONF_HOST: "fake_host", + CONF_PORT: 1234, + ATTR_PROPERTIES: { + "deviceid": "aa:bb:cc:dd:ee:ff", + "manufacturer": "fake_manufacturer", + "model": "fake_model", + "serialNumber": "fake_serial", + }, +} +MOCK_OLD_ENTRY = { + CONF_HOST: "fake_host", + CONF_ID: "0d1cef00-00dc-1000-9c80-4844f7b172de_old", + CONF_IP_ADDRESS: "fake_ip_old", + CONF_METHOD: "legacy", + CONF_PORT: None, +} +MOCK_WS_ENTRY = { + CONF_HOST: "fake_host", + CONF_METHOD: METHOD_WEBSOCKET, + CONF_PORT: 8002, + CONF_MODEL: "any", + CONF_NAME: "any", } AUTODETECT_LEGACY = { @@ -44,62 +117,32 @@ AUTODETECT_LEGACY = { "method": "legacy", "port": None, "host": "fake_host", - "timeout": 31, + "timeout": TIMEOUT_REQUEST, } AUTODETECT_WEBSOCKET_PLAIN = { "host": "fake_host", "name": "HomeAssistant", "port": 8001, - "timeout": 31, + "timeout": TIMEOUT_REQUEST, "token": None, } AUTODETECT_WEBSOCKET_SSL = { "host": "fake_host", "name": "HomeAssistant", "port": 8002, - "timeout": 31, + "timeout": TIMEOUT_REQUEST, "token": None, } +DEVICEINFO_WEBSOCKET_SSL = { + "host": "fake_host", + "name": "HomeAssistant", + "port": 8002, + "timeout": TIMEOUT_WEBSOCKET, + "token": "123456789", +} -@pytest.fixture(name="remote") -def remote_fixture(): - """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket_class: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - socket = Mock() - socket_class.return_value = socket - socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - -@pytest.fixture(name="remotews") -def remotews_fixture(): - """Patch the samsungtvws SamsungTVWS.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remotews_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket_class: - remotews = Mock() - remotews.__enter__ = Mock() - remotews.__exit__ = Mock() - remotews_class.return_value = remotews - remotews_class().__enter__().token = "FAKE_TOKEN" - socket = Mock() - socket_class.return_value = socket - socket_class.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remotews - - -async def test_user_legacy(hass, remote): +async def test_user_legacy(hass: HomeAssistant, remote: Mock): """Test starting a flow by user.""" # show form result = await hass.config_entries.flow.async_init( @@ -118,12 +161,12 @@ async def test_user_legacy(hass, remote): assert result["data"][CONF_HOST] == "fake_host" assert result["data"][CONF_NAME] == "fake_name" assert result["data"][CONF_METHOD] == "legacy" - assert result["data"][CONF_MANUFACTURER] is None + assert result["data"][CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert result["data"][CONF_MODEL] is None - assert result["data"][CONF_ID] is None + assert result["result"].unique_id is None -async def test_user_websocket(hass, remotews): +async def test_user_websocket(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom") @@ -139,46 +182,46 @@ async def test_user_websocket(hass, remotews): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input=MOCK_USER_DATA ) - # legacy tv entry created + # websocket tv entry created assert result["type"] == "create_entry" - assert result["title"] == "fake_name" + assert result["title"] == "Living Room (82GXARRS)" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "fake_name" + assert result["data"][CONF_NAME] == "Living Room" assert result["data"][CONF_METHOD] == "websocket" - assert result["data"][CONF_MANUFACTURER] is None - assert result["data"][CONF_MODEL] is None - assert result["data"][CONF_ID] is None + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" -async def test_user_legacy_missing_auth(hass): +async def test_user_legacy_missing_auth(hass: HomeAssistant, remote: Mock): """Test starting a flow by user with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=AccessDenied("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # legacy device missing authentication result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "auth_missing" + assert result["reason"] == RESULT_AUTH_MISSING -async def test_user_legacy_not_supported(hass): +async def test_user_legacy_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=UnhandledResponse("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # legacy device not supported result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_websocket_not_supported(hass): +async def test_user_websocket_not_supported(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -186,18 +229,16 @@ async def test_user_websocket_not_supported(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # websocket device not supported result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_user_not_successful(hass): +async def test_user_not_successful(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -205,17 +246,15 @@ async def test_user_not_successful(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_user_not_successful_2(hass): +async def test_user_not_successful_2(hass: HomeAssistant, remotews: Mock): """Test starting a flow by user but no connection found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -223,34 +262,15 @@ async def test_user_not_successful_2(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_user_already_configured(hass, remote): - """Test starting a flow by user when already configured.""" - - # entry was added - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA - ) - assert result["type"] == "create_entry" - - # failed as already configured - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_ssdp(hass, remote): +async def test_ssdp(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery.""" # confirm to add the entry @@ -267,13 +287,13 @@ async def test_ssdp(hass, remote): assert result["type"] == "create_entry" assert result["title"] == "fake_model" assert result["data"][CONF_HOST] == "fake_host" - assert result["data"][CONF_NAME] == "Samsung fake_model" - assert result["data"][CONF_MANUFACTURER] == "fake_manufacturer" + assert result["data"][CONF_NAME] == "fake_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake_manufacturer" assert result["data"][CONF_MODEL] == "fake_model" - assert result["data"][CONF_ID] == "fake_uuid" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_ssdp_noprefix(hass, remote): +async def test_ssdp_noprefix(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery without prefixes.""" # confirm to add the entry @@ -292,18 +312,18 @@ async def test_ssdp_noprefix(hass, remote): assert result["type"] == "create_entry" assert result["title"] == "fake2_model" assert result["data"][CONF_HOST] == "fake2_host" - assert result["data"][CONF_NAME] == "Samsung fake2_model" - assert result["data"][CONF_MANUFACTURER] == "fake2_manufacturer" + assert result["data"][CONF_NAME] == "fake2_model" + assert result["data"][CONF_MANUFACTURER] == "Samsung fake2_manufacturer" assert result["data"][CONF_MODEL] == "fake2_model" - assert result["data"][CONF_ID] == "fake2_uuid" + assert result["result"].unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172df" -async def test_ssdp_legacy_missing_auth(hass): +async def test_ssdp_legacy_missing_auth(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery with authentication.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=AccessDenied("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -317,15 +337,15 @@ async def test_ssdp_legacy_missing_auth(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "auth_missing" + assert result["reason"] == RESULT_AUTH_MISSING -async def test_ssdp_legacy_not_supported(hass): +async def test_ssdp_legacy_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=UnhandledResponse("Boom"), - ), patch("homeassistant.components.samsungtv.config_flow.socket"): + ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -339,10 +359,10 @@ async def test_ssdp_legacy_not_supported(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_websocket_not_supported(hass): +async def test_ssdp_websocket_not_supported(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery for not supported device.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -350,8 +370,6 @@ async def test_ssdp_websocket_not_supported(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=WebSocketProtocolException("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # confirm to add the entry result = await hass.config_entries.flow.async_init( @@ -365,10 +383,23 @@ async def test_ssdp_websocket_not_supported(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED -async def test_ssdp_not_successful(hass): +async def test_ssdp_model_not_supported(hass: HomeAssistant, remote: Mock): + """Test starting a flow from discovery.""" + + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_SSDP}, + data=MOCK_SSDP_DATA_WRONGMODEL, + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_NOT_SUPPORTED + + +async def test_ssdp_not_successful(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -376,8 +407,6 @@ async def test_ssdp_not_successful(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # confirm to add the entry @@ -392,10 +421,10 @@ async def test_ssdp_not_successful(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_not_successful_2(hass): +async def test_ssdp_not_successful_2(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery but no device found.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -403,8 +432,6 @@ async def test_ssdp_not_successful_2(hass): ), patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=ConnectionFailure("Boom"), - ), patch( - "homeassistant.components.samsungtv.config_flow.socket" ): # confirm to add the entry @@ -419,10 +446,10 @@ async def test_ssdp_not_successful_2(hass): result["flow_id"], user_input="whatever" ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT -async def test_ssdp_already_in_progress(hass, remote): +async def test_ssdp_already_in_progress(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery twice.""" # confirm to add the entry @@ -437,10 +464,10 @@ async def test_ssdp_already_in_progress(hass, remote): DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result["type"] == "abort" - assert result["reason"] == "already_in_progress" + assert result["reason"] == RESULT_ALREADY_IN_PROGRESS -async def test_ssdp_already_configured(hass, remote): +async def test_ssdp_already_configured(hass: HomeAssistant, remote: Mock): """Test starting a flow from discovery when already configured.""" # entry was added @@ -449,60 +476,224 @@ async def test_ssdp_already_configured(hass, remote): ) assert result["type"] == "create_entry" entry = result["result"] - assert entry.data[CONF_MANUFACTURER] is None + assert entry.data[CONF_MANUFACTURER] == DEFAULT_MANUFACTURER assert entry.data[CONF_MODEL] is None - assert entry.data[CONF_ID] is None + assert entry.unique_id is None # failed as already configured result2 = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA ) assert result2["type"] == "abort" - assert result2["reason"] == "already_configured" + assert result2["reason"] == RESULT_ALREADY_CONFIGURED # check updated device info - assert entry.data[CONF_MANUFACTURER] == "fake_manufacturer" - assert entry.data[CONF_MODEL] == "fake_model" - assert entry.data[CONF_ID] == "fake_uuid" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" -async def test_autodetect_websocket(hass, remote, remotews): - """Test for send key with autodetection of protocol.""" +async def test_import_legacy(hass: HomeAssistant): + """Test importing from yaml with hostname.""" with patch( - "homeassistant.components.samsungtv.bridge.Remote", - side_effect=OSError("Boom"), - ), patch("homeassistant.components.samsungtv.bridge.SamsungTVWS") as remotews: - enter = Mock() - type(enter).token = PropertyMock(return_value="123456789") - remote = Mock() - remote.__enter__ = Mock(return_value=enter) - remote.__exit__ = Mock(return_value=False) - remotews.return_value = remote - + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA, ) - assert result["type"] == "create_entry" - assert result["data"][CONF_METHOD] == "websocket" - assert result["data"][CONF_TOKEN] == "123456789" - assert remotews.call_count == 1 - assert remotews.call_args_list == [call(**AUTODETECT_WEBSOCKET_PLAIN)] + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake" + assert result["data"][CONF_METHOD] == METHOD_LEGACY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None -async def test_autodetect_websocket_ssl(hass, remote, remotews): +async def test_import_legacy_without_name(hass: HomeAssistant): + """Test importing from yaml without a name.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA_WITHOUT_NAME, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake_host" + assert result["data"][CONF_METHOD] == METHOD_LEGACY + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + +async def test_import_websocket(hass: HomeAssistant): + """Test importing from yaml with hostname.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_WSDATA, + ) + await hass.async_block_till_done() + assert result["type"] == "create_entry" + assert result["title"] == "fake" + assert result["data"][CONF_METHOD] == METHOD_WEBSOCKET + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "fake" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["result"].unique_id is None + + +async def test_import_unknown_host(hass: HomeAssistant, remotews: Mock): + """Test importing from yaml with hostname that does not resolve.""" + with patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + side_effect=socket.gaierror, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data=MOCK_IMPORT_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == RESULT_UNKNOWN_HOST + + +async def test_dhcp(hass: HomeAssistant, remotews: Mock): + """Test starting a flow from dhcp.""" + # confirm to add the entry + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_zeroconf(hass: HomeAssistant, remotews: Mock): + """Test starting a flow from zeroconf.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + # entry was added + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input="whatever" + ) + assert result["type"] == "create_entry" + assert result["title"] == "Living Room (82GXARRS)" + assert result["data"][CONF_HOST] == "fake_host" + assert result["data"][CONF_NAME] == "Living Room" + assert result["data"][CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert result["data"][CONF_MANUFACTURER] == "Samsung" + assert result["data"][CONF_MODEL] == "82GXARRS" + assert result["result"].unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_zeroconf_ignores_soundbar(hass: HomeAssistant, remotews_soundbar: Mock): + """Test starting a flow from zeroconf where the device is actually a soundbar.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + +async def test_zeroconf_no_device_info( + hass: HomeAssistant, remotews_no_device_info: Mock +): + """Test starting a flow from zeroconf where device_info returns None.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "not_supported" + + +async def test_zeroconf_and_dhcp_same_time(hass: HomeAssistant, remotews: Mock): + """Test starting a flow from zeroconf and dhcp.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "form" + assert result["step_id"] == "confirm" + + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result2["type"] == "abort" + assert result2["reason"] == "already_in_progress" + + +async def test_autodetect_websocket(hass: HomeAssistant, remote: Mock, remotews: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=OSError("Boom"), ), patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS", - side_effect=[WebSocketProtocolException("Boom"), DEFAULT_MOCK], + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ), patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS" ) as remotews: enter = Mock() type(enter).token = PropertyMock(return_value="123456789") remote = Mock() remote.__enter__ = Mock(return_value=enter) remote.__exit__ = Mock(return_value=False) + remote.rest_device_info.return_value = { + "id": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "device": { + "modelName": "82GXARRS", + "wifiMac": "aa:bb:cc:dd:ee:ff", + "udn": "uuid:be9554b9-c9fb-41f4-8920-22da015376a4", + "mac": "aa:bb:cc:dd:ee:ff", + "name": "[TV] Living Room", + "type": "Samsung SmartTV", + }, + } remotews.return_value = remote result = await hass.config_entries.flow.async_init( @@ -513,42 +704,48 @@ async def test_autodetect_websocket_ssl(hass, remote, remotews): assert result["data"][CONF_TOKEN] == "123456789" assert remotews.call_count == 2 assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_PLAIN), call(**AUTODETECT_WEBSOCKET_SSL), + call(**DEVICEINFO_WEBSOCKET_SSL), ] -async def test_autodetect_auth_missing(hass, remote): +async def test_autodetect_auth_missing(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[AccessDenied("Boom")], - ) as remote: + ) as remote, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "auth_missing" + assert result["reason"] == RESULT_AUTH_MISSING assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_not_supported(hass, remote): +async def test_autodetect_not_supported(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", side_effect=[UnhandledResponse("Boom")], - ) as remote: + ) as remote, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "not_supported" + assert result["reason"] == RESULT_NOT_SUPPORTED assert remote.call_count == 1 assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_legacy(hass, remote): +async def test_autodetect_legacy(hass: HomeAssistant, remote: Mock): """Test for send key with autodetection of protocol.""" with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: result = await hass.config_entries.flow.async_init( @@ -560,7 +757,7 @@ async def test_autodetect_legacy(hass, remote): assert remote.call_args_list == [call(AUTODETECT_LEGACY)] -async def test_autodetect_none(hass, remote, remotews): +async def test_autodetect_none(hass: HomeAssistant, remote: Mock, remotews: Mock): """Test for send key with autodetection of protocol.""" with patch( "homeassistant.components.samsungtv.bridge.Remote", @@ -568,18 +765,228 @@ async def test_autodetect_none(hass, remote, remotews): ) as remote, patch( "homeassistant.components.samsungtv.bridge.SamsungTVWS", side_effect=OSError("Boom"), - ) as remotews: + ) as remotews, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER}, data=MOCK_USER_DATA ) assert result["type"] == "abort" - assert result["reason"] == "cannot_connect" + assert result["reason"] == RESULT_CANNOT_CONNECT assert remote.call_count == 1 assert remote.call_args_list == [ call(AUTODETECT_LEGACY), ] assert remotews.call_count == 2 assert remotews.call_args_list == [ - call(**AUTODETECT_WEBSOCKET_PLAIN), call(**AUTODETECT_WEBSOCKET_SSL), + call(**AUTODETECT_WEBSOCKET_PLAIN), ] + + +async def test_update_old_entry(hass: HomeAssistant, remote: Mock): + """Test update of old entry.""" + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: + remote().rest_device_info.return_value = { + "device": { + "modelName": "fake_model2", + "name": "[TV] Fake Name", + "udn": "uuid:fake_serial", + } + } + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry.add_to_hass(hass) + + config_entries_domain = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries_domain) == 1 + assert entry is config_entries_domain[0] + assert entry.data[CONF_ID] == "0d1cef00-00dc-1000-9c80-4844f7b172de_old" + assert entry.data[CONF_IP_ADDRESS] == "fake_ip_old" + assert not entry.unique_id + + assert await async_setup_component(hass, DOMAIN, {}) is True + await hass.async_block_till_done() + + # failed as already configured + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_SSDP}, data=MOCK_SSDP_DATA + ) + assert result["type"] == "abort" + assert result["reason"] == RESULT_ALREADY_CONFIGURED + + config_entries_domain = hass.config_entries.async_entries(DOMAIN) + assert len(config_entries_domain) == 1 + entry2 = config_entries_domain[0] + + # check updated device info + assert entry2.data.get(CONF_ID) is not None + assert entry2.data.get(CONF_IP_ADDRESS) is not None + assert entry2.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +async def test_update_missing_mac_unique_id_added_from_dhcp(hass, remotews: Mock): + """Test missing mac and unique id added.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data=MOCK_DHCP_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_update_missing_mac_unique_id_added_from_zeroconf(hass, remotews: Mock): + """Test missing mac and unique id added.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY, unique_id=None) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "be9554b9-c9fb-41f4-8920-22da015376a4" + + +async def test_update_missing_mac_added_unique_id_preserved_from_zeroconf( + hass, remotews: Mock +): + """Test missing mac and unique id added.""" + entry = MockConfigEntry( + domain=DOMAIN, + data=MOCK_OLD_ENTRY, + unique_id="0d1cef00-00dc-1000-9c80-4844f7b172de", + ) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=MOCK_ZEROCONF_DATA, + ) + await hass.async_block_till_done() + assert result["type"] == "abort" + assert result["reason"] == "already_configured" + assert entry.data[CONF_MAC] == "aa:bb:cc:dd:ee:ff" + assert entry.unique_id == "0d1cef00-00dc-1000-9c80-4844f7b172de" + + +async def test_form_reauth_legacy(hass, remote: Mock): + """Test reauthenticate legacy.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_OLD_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_form_reauth_websocket(hass, remotews: Mock): + """Test reauthenticate websocket.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_form_reauth_websocket_cannot_connect(hass, remotews: Mock): + """Test reauthenticate websocket when we cannot connect on the first attempt.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS", + side_effect=ConnectionFailure, + ), patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + import pprint + + pprint.pprint(result2) + assert result2["type"] == "form" + assert result2["errors"] == {"base": RESULT_AUTH_MISSING} + + result3 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result3["type"] == "abort" + assert result3["reason"] == "reauth_successful" + + +async def test_form_reauth_websocket_not_supported(hass, remotews: Mock): + """Test reauthenticate websocket when the device is not supported.""" + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_WS_ENTRY) + entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"entry_id": entry.entry_id, "source": config_entries.SOURCE_REAUTH}, + data=entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.samsungtv.bridge.SamsungTVWS", + side_effect=WebSocketException, + ), patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "not_supported" diff --git a/tests/components/samsungtv/test_init.py b/tests/components/samsungtv/test_init.py index bb19f120cf6..f728fd4af10 100644 --- a/tests/components/samsungtv/test_init.py +++ b/tests/components/samsungtv/test_init.py @@ -1,21 +1,22 @@ """Tests for the Samsung TV Integration.""" from unittest.mock import Mock, call, patch -import pytest - from homeassistant.components.media_player.const import DOMAIN, SUPPORT_TURN_ON from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, DOMAIN as SAMSUNGTV_DOMAIN, + METHOD_WEBSOCKET, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, CONF_HOST, + CONF_METHOD, CONF_NAME, SERVICE_VOLUME_UP, ) +from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component ENTITY_ID = f"{DOMAIN}.fake_name" @@ -25,6 +26,7 @@ MOCK_CONFIG = { CONF_HOST: "fake_host", CONF_NAME: "fake_name", CONF_ON_ACTION: [{"delay": "00:00:01"}], + CONF_METHOD: METHOD_WEBSOCKET, } ] } @@ -32,37 +34,22 @@ REMOTE_CALL = { "name": "HomeAssistant", "description": "HomeAssistant", "id": "ha.component.samsung", - "method": "legacy", "host": MOCK_CONFIG[SAMSUNGTV_DOMAIN][0][CONF_HOST], + "method": "legacy", "port": None, "timeout": 1, } -@pytest.fixture(name="remote") -def remote_fixture(): - """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket1, patch( - "homeassistant.components.samsungtv.socket" - ) as socket2: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - -async def test_setup(hass, remote): +async def test_setup(hass: HomeAssistant, remote: Mock): """Test Samsung TV integration is setup.""" - with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: - await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) - await hass.async_block_till_done() + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote, patch( + "homeassistant.components.samsungtv.config_flow.socket.gethostbyname", + return_value="fake_host", + ): + with patch("homeassistant.components.samsungtv.bridge.Remote") as remote: + await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) + await hass.async_block_till_done() state = hass.states.get(ENTITY_ID) # test name and turn_on @@ -80,7 +67,7 @@ async def test_setup(hass, remote): assert remote.call_args == call(REMOTE_CALL) -async def test_setup_duplicate_config(hass, remote, caplog): +async def test_setup_duplicate_config(hass: HomeAssistant, remote: Mock, caplog): """Test duplicate setup of platform.""" DUPLICATE = { SAMSUNGTV_DOMAIN: [ @@ -95,7 +82,7 @@ async def test_setup_duplicate_config(hass, remote, caplog): assert "duplicate host entries found" in caplog.text -async def test_setup_duplicate_entries(hass, remote, caplog): +async def test_setup_duplicate_entries(hass: HomeAssistant, remote: Mock, caplog): """Test duplicate setup of platform.""" await async_setup_component(hass, SAMSUNGTV_DOMAIN, MOCK_CONFIG) await hass.async_block_till_done() diff --git a/tests/components/samsungtv/test_media_player.py b/tests/components/samsungtv/test_media_player.py index 6415f02cdf5..02eceeaacb7 100644 --- a/tests/components/samsungtv/test_media_player.py +++ b/tests/components/samsungtv/test_media_player.py @@ -25,6 +25,7 @@ from homeassistant.components.media_player.const import ( from homeassistant.components.samsungtv.const import ( CONF_ON_ACTION, DOMAIN as SAMSUNGTV_DOMAIN, + TIMEOUT_WEBSOCKET, ) from homeassistant.components.samsungtv.media_player import SUPPORT_SAMSUNGTV from homeassistant.const import ( @@ -34,13 +35,16 @@ from homeassistant.const import ( ATTR_SUPPORTED_FEATURES, CONF_HOST, CONF_IP_ADDRESS, + CONF_MAC, CONF_METHOD, CONF_NAME, CONF_PORT, + CONF_TIMEOUT, CONF_TOKEN, SERVICE_MEDIA_NEXT_TRACK, SERVICE_MEDIA_PAUSE, SERVICE_MEDIA_PLAY, + SERVICE_MEDIA_PLAY_PAUSE, SERVICE_MEDIA_PREVIOUS_TRACK, SERVICE_TURN_OFF, SERVICE_TURN_ON, @@ -49,6 +53,7 @@ from homeassistant.const import ( SERVICE_VOLUME_UP, STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -59,7 +64,7 @@ ENTITY_ID = f"{DOMAIN}.fake" MOCK_CONFIG = { SAMSUNGTV_DOMAIN: [ { - CONF_HOST: "fake", + CONF_HOST: "fake_host", CONF_NAME: "fake", CONF_PORT: 55000, CONF_ON_ACTION: [{"delay": "00:00:01"}], @@ -69,7 +74,7 @@ MOCK_CONFIG = { MOCK_CONFIGWS = { SAMSUNGTV_DOMAIN: [ { - CONF_HOST: "fake", + CONF_HOST: "fake_host", CONF_NAME: "fake", CONF_PORT: 8001, CONF_TOKEN: "123456789", @@ -78,27 +83,31 @@ MOCK_CONFIGWS = { ] } MOCK_CALLS_WS = { - "host": "fake", - "port": 8001, - "token": None, - "timeout": 31, - "name": "HomeAssistant", + CONF_HOST: "fake_host", + CONF_PORT: 8001, + CONF_TOKEN: "123456789", + CONF_TIMEOUT: TIMEOUT_WEBSOCKET, + CONF_NAME: "HomeAssistant", } MOCK_ENTRY_WS = { CONF_IP_ADDRESS: "test", - CONF_HOST: "fake", + CONF_HOST: "fake_host", CONF_METHOD: "websocket", CONF_NAME: "fake", CONF_PORT: 8001, - CONF_TOKEN: "abcde", + CONF_TOKEN: "123456789", } -MOCK_CALLS_ENTRY_WS = { - "host": "fake", - "name": "HomeAssistant", - "port": 8001, - "timeout": 8, - "token": "abcde", + + +MOCK_ENTRY_WS_WITH_MAC = { + CONF_IP_ADDRESS: "test", + CONF_HOST: "fake_host", + CONF_METHOD: "websocket", + CONF_MAC: "aa:bb:cc:dd:ee:ff", + CONF_NAME: "fake", + CONF_PORT: 8002, + CONF_TOKEN: "123456789", } ENTITY_ID_NOTURNON = f"{DOMAIN}.fake_noturnon" @@ -109,45 +118,6 @@ MOCK_CONFIG_NOTURNON = { } -@pytest.fixture(name="remote") -def remote_fixture(): - """Patch the samsungctl Remote.""" - with patch( - "homeassistant.components.samsungtv.bridge.Remote" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket1, patch( - "homeassistant.components.samsungtv.socket" - ) as socket2: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - -@pytest.fixture(name="remotews") -def remotews_fixture(): - """Patch the samsungtvws SamsungTVWS.""" - with patch( - "homeassistant.components.samsungtv.bridge.SamsungTVWS" - ) as remote_class, patch( - "homeassistant.components.samsungtv.config_flow.socket" - ) as socket1, patch( - "homeassistant.components.samsungtv.socket" - ) as socket2: - remote = Mock() - remote.__enter__ = Mock() - remote.__exit__ = Mock() - remote_class.return_value = remote - remote_class().__enter__().token = "FAKE_TOKEN" - socket1.gethostbyname.return_value = "FAKE_IP_ADDRESS" - socket2.gethostbyname.return_value = "FAKE_IP_ADDRESS" - yield remote - - @pytest.fixture(name="delay") def delay_fixture(): """Patch the delay script function.""" @@ -226,7 +196,7 @@ async def test_setup_websocket_2(hass, mock_now): state = hass.states.get(entity_id) assert state assert remote.call_count == 1 - assert remote.call_args_list == [call(**MOCK_CALLS_ENTRY_WS)] + assert remote.call_args_list == [call(**MOCK_CALLS_WS)] async def test_update_on(hass, remote, mock_now): @@ -272,12 +242,18 @@ async def test_update_access_denied(hass, remote, mock_now): with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() assert [ flow for flow in hass.config_entries.flow.async_progress() if flow["context"]["source"] == "reauth" ] + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE async def test_update_connection_failure(hass, remotews, mock_now): @@ -296,12 +272,18 @@ async def test_update_connection_failure(hass, remotews, mock_now): with patch("homeassistant.util.dt.utcnow", return_value=next_update): async_fire_time_changed(hass, next_update) await hass.async_block_till_done() + next_update = mock_now + timedelta(minutes=10) + with patch("homeassistant.util.dt.utcnow", return_value=next_update): + async_fire_time_changed(hass, next_update) + await hass.async_block_till_done() assert [ flow for flow in hass.config_entries.flow.async_progress() if flow["context"]["source"] == "reauth" ] + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_UNAVAILABLE async def test_update_unhandled_response(hass, remote, mock_now): @@ -438,7 +420,8 @@ async def test_state_without_turnon(hass, remote): DOMAIN, SERVICE_TURN_OFF, {ATTR_ENTITY_ID: ENTITY_ID_NOTURNON}, True ) state = hass.states.get(ENTITY_ID_NOTURNON) - assert state.state == STATE_OFF + # Should be STATE_UNAVAILABLE since there is no way to turn it back on + assert state.state == STATE_UNAVAILABLE async def test_supported_features_with_turnon(hass, remote): @@ -555,6 +538,15 @@ async def test_media_play(hass, remote): assert remote.close.call_count == 1 assert remote.close.call_args_list == [call()] + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PLAY"), call("KEY_PAUSE")] + assert remote.close.call_count == 2 + assert remote.close.call_args_list == [call(), call()] + async def test_media_pause(hass, remote): """Test for media_pause.""" @@ -568,6 +560,15 @@ async def test_media_pause(hass, remote): assert remote.close.call_count == 1 assert remote.close.call_args_list == [call()] + assert await hass.services.async_call( + DOMAIN, SERVICE_MEDIA_PLAY_PAUSE, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + # key and update called + assert remote.control.call_count == 2 + assert remote.control.call_args_list == [call("KEY_PAUSE"), call("KEY_PLAY")] + assert remote.close.call_count == 2 + assert remote.close.call_args_list == [call(), call()] + async def test_media_next_track(hass, remote): """Test for media_next_track.""" @@ -604,6 +605,26 @@ async def test_turn_on_with_turnon(hass, remote, delay): assert delay.call_count == 1 +async def test_turn_on_wol(hass, remotews): + """Test turn on.""" + entry = MockConfigEntry( + domain=SAMSUNGTV_DOMAIN, + data=MOCK_ENTRY_WS_WITH_MAC, + unique_id="any", + ) + entry.add_to_hass(hass) + assert await async_setup_component(hass, SAMSUNGTV_DOMAIN, {}) + await hass.async_block_till_done() + with patch( + "homeassistant.components.samsungtv.media_player.send_magic_packet" + ) as mock_send_magic_packet: + assert await hass.services.async_call( + DOMAIN, SERVICE_TURN_ON, {ATTR_ENTITY_ID: ENTITY_ID}, True + ) + await hass.async_block_till_done() + assert mock_send_magic_packet.called + + async def test_turn_on_without_turnon(hass, remote): """Test turn on.""" await setup_samsungtv(hass, MOCK_CONFIG_NOTURNON) diff --git a/tests/components/scene/test_init.py b/tests/components/scene/test_init.py index a3d50d0214f..4c5b832ac14 100644 --- a/tests/components/scene/test_init.py +++ b/tests/components/scene/test_init.py @@ -19,7 +19,7 @@ def entities(hass): yield platform.ENTITIES[0:2] -async def test_config_yaml_alias_anchor(hass, entities): +async def test_config_yaml_alias_anchor(hass, entities, enable_custom_integrations): """Test the usage of YAML aliases and anchors. The following test scene configuration is equivalent to: @@ -64,7 +64,7 @@ async def test_config_yaml_alias_anchor(hass, entities): assert light_2.last_call("turn_on")[1].get("brightness") == 100 -async def test_config_yaml_bool(hass, entities): +async def test_config_yaml_bool(hass, entities, enable_custom_integrations): """Test parsing of booleans in yaml config.""" light_1, light_2 = await setup_lights(hass, entities) @@ -91,7 +91,7 @@ async def test_config_yaml_bool(hass, entities): assert light_2.last_call("turn_on")[1].get("brightness") == 100 -async def test_activate_scene(hass, entities): +async def test_activate_scene(hass, entities, enable_custom_integrations): """Test active scene.""" light_1, light_2 = await setup_lights(hass, entities) @@ -117,6 +117,8 @@ async def test_activate_scene(hass, entities): assert light.is_on(hass, light_2.entity_id) assert light_2.last_call("turn_on")[1].get("brightness") == 100 + await turn_off_lights(hass, [light_2.entity_id]) + calls = async_mock_service(hass, "light", "turn_on") await hass.services.async_call( @@ -156,16 +158,22 @@ async def setup_lights(hass, entities): await hass.async_block_till_done() light_1, light_2 = entities + light_1.supported_color_modes = ["brightness"] + light_2.supported_color_modes = ["brightness"] - await hass.services.async_call( - "light", - "turn_off", - {"entity_id": [light_1.entity_id, light_2.entity_id]}, - blocking=True, - ) - await hass.async_block_till_done() - + await turn_off_lights(hass, [light_1.entity_id, light_2.entity_id]) assert not light.is_on(hass, light_1.entity_id) assert not light.is_on(hass, light_2.entity_id) return light_1, light_2 + + +async def turn_off_lights(hass, entity_ids): + """Turn lights off.""" + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": entity_ids}, + blocking=True, + ) + await hass.async_block_till_done() diff --git a/tests/components/script/test_blueprint.py b/tests/components/script/test_blueprint.py index d5ba914df05..1c02a35792b 100644 --- a/tests/components/script/test_blueprint.py +++ b/tests/components/script/test_blueprint.py @@ -7,7 +7,8 @@ from unittest.mock import patch from homeassistant.components import script from homeassistant.components.blueprint.models import Blueprint, DomainBlueprints -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import Context, HomeAssistant, callback +from homeassistant.helpers import template from homeassistant.setup import async_setup_component from homeassistant.util import yaml @@ -70,44 +71,48 @@ async def test_confirmable_notification(hass: HomeAssistant) -> None: ) turn_on_calls = async_mock_service(hass, "homeassistant", "turn_on") + context = Context() with patch( "homeassistant.components.mobile_app.device_action.async_call_action_from_config" ) as mock_call_action: # Trigger script - await hass.services.async_call(script.DOMAIN, "confirm") + await hass.services.async_call(script.DOMAIN, "confirm", context=context) # Give script the time to attach the trigger. await asyncio.sleep(0.1) - hass.bus.async_fire("mobile_app_notification_action", {"action": "CONFIRM"}) + hass.bus.async_fire("mobile_app_notification_action", {"action": "ANYTHING_ELSE"}) + hass.bus.async_fire( + "mobile_app_notification_action", {"action": "CONFIRM_" + Context().id} + ) + hass.bus.async_fire( + "mobile_app_notification_action", {"action": "CONFIRM_" + context.id} + ) await hass.async_block_till_done() assert len(mock_call_action.mock_calls) == 1 _hass, config, variables, _context = mock_call_action.mock_calls[0][1] - title_tpl = config.pop("title") - message_tpl = config.pop("message") - title_tpl.hass = hass - message_tpl.hass = hass + template.attach(hass, config) + rendered_config = template.render_complex(config, variables) - assert config == { + assert rendered_config == { + "title": "Lord of the things", + "message": "Throw ring in mountain?", "alias": "Send notification", "domain": "mobile_app", "type": "notify", "device_id": "frodo", "data": { "actions": [ - {"action": "CONFIRM", "title": "Confirm"}, - {"action": "DISMISS", "title": "Dismiss"}, + {"action": "CONFIRM_" + _context.id, "title": "Confirm"}, + {"action": "DISMISS_" + _context.id, "title": "Dismiss"}, ] }, } - assert title_tpl.async_render(variables) == "Lord of the things" - assert message_tpl.async_render(variables) == "Throw ring in mountain?" - assert len(turn_on_calls) == 1 assert turn_on_calls[0].data == { "entity_id": ["mount.doom"], diff --git a/tests/components/sense/test_config_flow.py b/tests/components/sense/test_config_flow.py index 41cfdc017dd..55348cca838 100644 --- a/tests/components/sense/test_config_flow.py +++ b/tests/components/sense/test_config_flow.py @@ -17,8 +17,6 @@ async def test_form(hass): assert result["errors"] == {} with patch("sense_energy.ASyncSenseable.authenticate", return_value=True,), patch( - "homeassistant.components.sense.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.sense.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -35,7 +33,6 @@ async def test_form(hass): "email": "test-email", "password": "test-password", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sensor/test_device_condition.py b/tests/components/sensor/test_device_condition.py index 2de95d44eb1..6cad21c5bde 100644 --- a/tests/components/sensor/test_device_condition.py +++ b/tests/components/sensor/test_device_condition.py @@ -41,7 +41,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_conditions(hass, device_reg, entity_reg): +async def test_get_conditions(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected conditions from a sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -80,7 +80,9 @@ async def test_get_conditions(hass, device_reg, entity_reg): assert conditions == expected_conditions -async def test_get_condition_capabilities(hass, device_reg, entity_reg): +async def test_get_condition_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -126,7 +128,9 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): +async def test_get_condition_capabilities_none( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor condition.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -162,7 +166,9 @@ async def test_get_condition_capabilities_none(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state_not_above_below(hass, calls, caplog): +async def test_if_state_not_above_below( + hass, calls, caplog, enable_custom_integrations +): """Test for bad value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -196,7 +202,7 @@ async def test_if_state_not_above_below(hass, calls, caplog): assert "must contain at least one of below, above" in caplog.text -async def test_if_state_above(hass, calls): +async def test_if_state_above(hass, calls, enable_custom_integrations): """Test for value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -254,7 +260,7 @@ async def test_if_state_above(hass, calls): assert calls[0].data["some"] == "event - test_event1" -async def test_if_state_below(hass, calls): +async def test_if_state_below(hass, calls, enable_custom_integrations): """Test for value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -312,7 +318,7 @@ async def test_if_state_below(hass, calls): assert calls[0].data["some"] == "event - test_event1" -async def test_if_state_between(hass, calls): +async def test_if_state_between(hass, calls, enable_custom_integrations): """Test for value conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_device_trigger.py b/tests/components/sensor/test_device_trigger.py index 4c65eff34ab..9da93510523 100644 --- a/tests/components/sensor/test_device_trigger.py +++ b/tests/components/sensor/test_device_trigger.py @@ -45,7 +45,7 @@ def calls(hass): return async_mock_service(hass, "test", "automation") -async def test_get_triggers(hass, device_reg, entity_reg): +async def test_get_triggers(hass, device_reg, entity_reg, enable_custom_integrations): """Test we get the expected triggers from a sensor.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -85,7 +85,9 @@ async def test_get_triggers(hass, device_reg, entity_reg): assert triggers == expected_triggers -async def test_get_trigger_capabilities(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -132,7 +134,9 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): +async def test_get_trigger_capabilities_none( + hass, device_reg, entity_reg, enable_custom_integrations +): """Test we get the expected capabilities from a sensor trigger.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -168,7 +172,9 @@ async def test_get_trigger_capabilities_none(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_not_on_above_below(hass, calls, caplog): +async def test_if_fires_not_on_above_below( + hass, calls, caplog, enable_custom_integrations +): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -198,7 +204,7 @@ async def test_if_fires_not_on_above_below(hass, calls, caplog): assert "must contain at least one of below, above" in caplog.text -async def test_if_fires_on_state_above(hass, calls): +async def test_if_fires_on_state_above(hass, calls, enable_custom_integrations): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -256,7 +262,7 @@ async def test_if_fires_on_state_above(hass, calls): ) -async def test_if_fires_on_state_below(hass, calls): +async def test_if_fires_on_state_below(hass, calls, enable_custom_integrations): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -314,7 +320,7 @@ async def test_if_fires_on_state_below(hass, calls): ) -async def test_if_fires_on_state_between(hass, calls): +async def test_if_fires_on_state_between(hass, calls, enable_custom_integrations): """Test for value triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") platform.init() @@ -384,7 +390,9 @@ async def test_if_fires_on_state_between(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/sensor/test_recorder.py b/tests/components/sensor/test_recorder.py new file mode 100644 index 00000000000..47a950f9eaa --- /dev/null +++ b/tests/components/sensor/test_recorder.py @@ -0,0 +1,491 @@ +"""The tests for sensor recorder platform.""" +# pylint: disable=protected-access,invalid-name +from datetime import timedelta +from unittest.mock import patch, sentinel + +from pytest import approx + +from homeassistant.components.recorder import history +from homeassistant.components.recorder.const import DATA_INSTANCE +from homeassistant.components.recorder.models import process_timestamp_to_utc_isoformat +from homeassistant.components.recorder.statistics import statistics_during_period +from homeassistant.const import STATE_UNAVAILABLE +from homeassistant.setup import setup_component +import homeassistant.util.dt as dt_util + +from tests.components.recorder.common import wait_recording_done + + +def test_compile_hourly_statistics(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(16.440677966101696), + "min": approx(10.0), + "max": approx(30.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + +def test_compile_hourly_energy_statistics(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + sns1_attr = {"device_class": "energy", "state_class": "measurement"} + sns2_attr = {"device_class": "energy"} + sns3_attr = {} + + zero, four, eight, states = record_energy_states( + hass, sns1_attr, sns2_attr, sns3_attr + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": approx(20.0), + "sum": approx(10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(40.0), + "sum": approx(10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(70.0), + "sum": approx(40.0), + }, + ] + } + + +def test_compile_hourly_energy_statistics2(hass_recorder): + """Test compiling hourly statistics.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + sns1_attr = {"device_class": "energy", "state_class": "measurement"} + sns2_attr = {"device_class": "energy", "state_class": "measurement"} + sns3_attr = {"device_class": "energy", "state_class": "measurement"} + + zero, four, eight, states = record_energy_states( + hass, sns1_attr, sns2_attr, sns3_attr + ) + hist = history.get_significant_states( + hass, zero - timedelta.resolution, eight + timedelta.resolution + ) + assert dict(states)["sensor.test1"] == dict(hist)["sensor.test1"] + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=1)) + wait_recording_done(hass) + recorder.do_adhoc_statistics(period="hourly", start=zero + timedelta(hours=2)) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": approx(20.0), + "sum": approx(10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(40.0), + "sum": approx(10.0), + }, + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(70.0), + "sum": approx(40.0), + }, + ], + "sensor.test2": [ + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": approx(130.0), + "sum": approx(20.0), + }, + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(45.0), + "sum": approx(-95.0), + }, + { + "statistic_id": "sensor.test2", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(75.0), + "sum": approx(-65.0), + }, + ], + "sensor.test3": [ + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(zero), + "state": approx(5.0), + "sum": approx(5.0), + }, + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=1)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(50.0), + "sum": approx(30.0), + }, + { + "statistic_id": "sensor.test3", + "start": process_timestamp_to_utc_isoformat(zero + timedelta(hours=2)), + "max": None, + "mean": None, + "min": None, + "last_reset": process_timestamp_to_utc_isoformat(four), + "state": approx(90.0), + "sum": approx(70.0), + }, + ], + } + + +def test_compile_hourly_statistics_unchanged(hass_recorder): + """Test compiling hourly statistics, with no changes during the hour.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=four) + wait_recording_done(hass) + stats = statistics_during_period(hass, four) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(four), + "mean": approx(30.0), + "min": approx(30.0), + "max": approx(30.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + +def test_compile_hourly_statistics_partially_unavailable(hass_recorder): + """Test compiling hourly statistics, with the sensor being partially unavailable.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states_partially_unavailable(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=zero) + wait_recording_done(hass) + stats = statistics_during_period(hass, zero) + assert stats == { + "sensor.test1": [ + { + "statistic_id": "sensor.test1", + "start": process_timestamp_to_utc_isoformat(zero), + "mean": approx(21.1864406779661), + "min": approx(10.0), + "max": approx(25.0), + "last_reset": None, + "state": None, + "sum": None, + } + ] + } + + +def test_compile_hourly_statistics_unavailable(hass_recorder): + """Test compiling hourly statistics, with the sensor being unavailable.""" + hass = hass_recorder() + recorder = hass.data[DATA_INSTANCE] + setup_component(hass, "sensor", {}) + zero, four, states = record_states_partially_unavailable(hass) + hist = history.get_significant_states(hass, zero, four) + assert dict(states) == dict(hist) + + recorder.do_adhoc_statistics(period="hourly", start=four) + wait_recording_done(hass) + stats = statistics_during_period(hass, four) + assert stats == {} + + +def record_states(hass): + """Record some test states. + + We inject a bunch of state updates for temperature sensors. + """ + mp = "media_player.test" + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns2_attr = {"device_class": "temperature"} + sns3_attr = {} + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=1) + two = one + timedelta(minutes=10) + three = two + timedelta(minutes=40) + four = three + timedelta(minutes=10) + + states = {mp: [], sns1: [], sns2: [], sns3: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "15", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "15", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) + + return zero, four, states + + +def record_energy_states(hass, _sns1_attr, _sns2_attr, _sns3_attr): + """Record some test states. + + We inject a bunch of state updates for energy sensors. + """ + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns4 = "sensor.test4" + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=15) + two = one + timedelta(minutes=30) + three = two + timedelta(minutes=15) + four = three + timedelta(minutes=15) + five = four + timedelta(minutes=30) + six = five + timedelta(minutes=15) + seven = six + timedelta(minutes=15) + eight = seven + timedelta(minutes=30) + + sns1_attr = {**_sns1_attr, "last_reset": zero.isoformat()} + sns2_attr = {**_sns2_attr, "last_reset": zero.isoformat()} + sns3_attr = {**_sns3_attr, "last_reset": zero.isoformat()} + sns4_attr = {**_sns3_attr} + + states = {sns1: [], sns2: [], sns3: [], sns4: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=zero): + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 + states[sns2].append(set_state(sns2, "110", attributes=sns2_attr)) # Sum 0 + states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 + states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[sns1].append(set_state(sns1, "15", attributes=sns1_attr)) # Sum 5 + states[sns2].append(set_state(sns2, "120", attributes=sns2_attr)) # Sum 10 + states[sns3].append(set_state(sns3, "0", attributes=sns3_attr)) # Sum 0 + states[sns4].append(set_state(sns4, "0", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "20", attributes=sns1_attr)) # Sum 10 + states[sns2].append(set_state(sns2, "130", attributes=sns2_attr)) # Sum 20 + states[sns3].append(set_state(sns3, "5", attributes=sns3_attr)) # Sum 5 + states[sns4].append(set_state(sns4, "5", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) # Sum 0 + states[sns2].append(set_state(sns2, "0", attributes=sns2_attr)) # Sum -110 + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) # Sum 10 + states[sns4].append(set_state(sns4, "10", attributes=sns4_attr)) # - + + sns1_attr = {**_sns1_attr, "last_reset": four.isoformat()} + sns2_attr = {**_sns2_attr, "last_reset": four.isoformat()} + sns3_attr = {**_sns3_attr, "last_reset": four.isoformat()} + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=four): + states[sns1].append(set_state(sns1, "30", attributes=sns1_attr)) # Sum 0 + states[sns2].append(set_state(sns2, "30", attributes=sns2_attr)) # Sum -110 + states[sns3].append(set_state(sns3, "30", attributes=sns3_attr)) # Sum 10 + states[sns4].append(set_state(sns4, "30", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=five): + states[sns1].append(set_state(sns1, "40", attributes=sns1_attr)) # Sum 10 + states[sns2].append(set_state(sns2, "45", attributes=sns2_attr)) # Sum -95 + states[sns3].append(set_state(sns3, "50", attributes=sns3_attr)) # Sum 30 + states[sns4].append(set_state(sns4, "50", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=six): + states[sns1].append(set_state(sns1, "50", attributes=sns1_attr)) # Sum 20 + states[sns2].append(set_state(sns2, "55", attributes=sns2_attr)) # Sum -85 + states[sns3].append(set_state(sns3, "60", attributes=sns3_attr)) # Sum 40 + states[sns4].append(set_state(sns4, "60", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=seven): + states[sns1].append(set_state(sns1, "60", attributes=sns1_attr)) # Sum 30 + states[sns2].append(set_state(sns2, "65", attributes=sns2_attr)) # Sum -75 + states[sns3].append(set_state(sns3, "80", attributes=sns3_attr)) # Sum 60 + states[sns4].append(set_state(sns4, "80", attributes=sns4_attr)) # - + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=eight): + states[sns1].append(set_state(sns1, "70", attributes=sns1_attr)) # Sum 40 + states[sns2].append(set_state(sns2, "75", attributes=sns2_attr)) # Sum -65 + states[sns3].append(set_state(sns3, "90", attributes=sns3_attr)) # Sum 70 + + return zero, four, eight, states + + +def record_states_partially_unavailable(hass): + """Record some test states. + + We inject a bunch of state updates temperature sensors. + """ + mp = "media_player.test" + sns1 = "sensor.test1" + sns2 = "sensor.test2" + sns3 = "sensor.test3" + sns1_attr = {"device_class": "temperature", "state_class": "measurement"} + sns2_attr = {"device_class": "temperature"} + sns3_attr = {} + + def set_state(entity_id, state, **kwargs): + """Set the state.""" + hass.states.set(entity_id, state, **kwargs) + wait_recording_done(hass) + return hass.states.get(entity_id) + + zero = dt_util.utcnow() + one = zero + timedelta(minutes=1) + two = one + timedelta(minutes=15) + three = two + timedelta(minutes=30) + four = three + timedelta(minutes=15) + + states = {mp: [], sns1: [], sns2: [], sns3: []} + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=one): + states[mp].append( + set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)}) + ) + states[mp].append( + set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}) + ) + states[sns1].append(set_state(sns1, "10", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "10", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "10", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=two): + states[sns1].append(set_state(sns1, "25", attributes=sns1_attr)) + states[sns2].append(set_state(sns2, "25", attributes=sns2_attr)) + states[sns3].append(set_state(sns3, "25", attributes=sns3_attr)) + + with patch("homeassistant.components.recorder.dt_util.utcnow", return_value=three): + states[sns1].append(set_state(sns1, STATE_UNAVAILABLE, attributes=sns1_attr)) + states[sns2].append(set_state(sns2, STATE_UNAVAILABLE, attributes=sns2_attr)) + states[sns3].append(set_state(sns3, STATE_UNAVAILABLE, attributes=sns3_attr)) + + return zero, four, states diff --git a/tests/components/sentry/conftest.py b/tests/components/sentry/conftest.py index 77da4119166..a7347d44bab 100644 --- a/tests/components/sentry/conftest.py +++ b/tests/components/sentry/conftest.py @@ -1,4 +1,8 @@ -"""Configuration for Sonos tests.""" +"""Configuration for Sentry tests.""" +from __future__ import annotations + +from typing import Any + import pytest from homeassistant.components.sentry import DOMAIN @@ -7,12 +11,12 @@ from tests.common import MockConfigEntry @pytest.fixture(name="config_entry") -def config_entry_fixture(): +def config_entry_fixture() -> MockConfigEntry: """Create a mock config entry.""" return MockConfigEntry(domain=DOMAIN, title="Sentry") @pytest.fixture(name="config") -def config_fixture(): +def config_fixture() -> dict[str, Any]: """Create hass config fixture.""" return {DOMAIN: {"dsn": "http://public@sentry.local/1"}} diff --git a/tests/components/sentry/test_config_flow.py b/tests/components/sentry/test_config_flow.py index 259d8c65e16..2246cabe33a 100644 --- a/tests/components/sentry/test_config_flow.py +++ b/tests/components/sentry/test_config_flow.py @@ -16,20 +16,26 @@ from homeassistant.components.sentry.const import ( DOMAIN, ) from homeassistant.config_entries import SOURCE_USER -from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry -async def test_full_user_flow_implementation(hass): +async def test_full_user_flow_implementation(hass: HomeAssistant) -> None: """Test we get the form.""" await async_setup_component(hass, "persistent_notification", {}) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert "flow_id" in result with patch("homeassistant.components.sentry.config_flow.Dsn"), patch( "homeassistant.components.sentry.async_setup_entry", @@ -40,9 +46,9 @@ async def test_full_user_flow_implementation(hass): {"dsn": "http://public@sentry.local/1"}, ) - assert result2["type"] == "create_entry" - assert result2["title"] == "Sentry" - assert result2["data"] == { + assert result2.get("type") == "create_entry" + assert result2.get("title") == "Sentry" + assert result2.get("data") == { "dsn": "http://public@sentry.local/1", } await hass.async_block_till_done() @@ -50,22 +56,23 @@ async def test_full_user_flow_implementation(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_integration_already_exists(hass): +async def test_integration_already_exists(hass: HomeAssistant) -> None: """Test we only allow a single config flow.""" MockConfigEntry(domain=DOMAIN).add_to_hass(hass) result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "single_instance_allowed" + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "single_instance_allowed" -async def test_user_flow_bad_dsn(hass): +async def test_user_flow_bad_dsn(hass: HomeAssistant) -> None: """Test we handle bad dsn error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -76,15 +83,16 @@ async def test_user_flow_bad_dsn(hass): {"dsn": "foo"}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "bad_dsn"} + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("errors") == {"base": "bad_dsn"} -async def test_user_flow_unkown_exception(hass): +async def test_user_flow_unkown_exception(hass: HomeAssistant) -> None: """Test we handle any unknown exception error.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) + assert "flow_id" in result with patch( "homeassistant.components.sentry.config_flow.Dsn", @@ -95,11 +103,11 @@ async def test_user_flow_unkown_exception(hass): {"dsn": "foo"}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("errors") == {"base": "unknown"} -async def test_options_flow(hass): +async def test_options_flow(hass: HomeAssistant) -> None: """Test options config flow.""" entry = MockConfigEntry( domain=DOMAIN, @@ -113,8 +121,9 @@ async def test_options_flow(hass): result = await hass.config_entries.options.async_init(entry.entry_id) - assert result["type"] == RESULT_TYPE_FORM - assert result["step_id"] == "init" + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "init" + assert "flow_id" in result result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -130,8 +139,8 @@ async def test_options_flow(hass): }, ) - assert result["type"] == "create_entry" - assert result["data"] == { + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result.get("data") == { CONF_ENVIRONMENT: "Test", CONF_EVENT_CUSTOM_COMPONENTS: True, CONF_EVENT_HANDLED: True, diff --git a/tests/components/sentry/test_init.py b/tests/components/sentry/test_init.py index e920437b2f7..206018f50a5 100644 --- a/tests/components/sentry/test_init.py +++ b/tests/components/sentry/test_init.py @@ -1,6 +1,6 @@ """Tests for Sentry integration.""" import logging -from unittest.mock import MagicMock, Mock, patch +from unittest.mock import Mock, patch import pytest @@ -112,12 +112,12 @@ async def test_setup_entry_with_tracing(hass: HomeAssistant) -> None: ("0.115.0dev0", "dev"), ], ) -async def test_get_channel(version, channel) -> None: +async def test_get_channel(version: str, channel: str) -> None: """Test if channel detection works from Home Assistant version number.""" assert get_channel(version) == channel -async def test_process_before_send(hass: HomeAssistant): +async def test_process_before_send(hass: HomeAssistant) -> None: """Test regular use of the Sentry process before sending function.""" hass.config.components.add("puppies") hass.config.components.add("a_integration") @@ -308,12 +308,6 @@ async def test_filter_log_events(hass: HomeAssistant, logger, options, event): ) async def test_filter_handled_events(hass: HomeAssistant, handled, options, event): """Tests filtering of handled events based on configuration options.""" - - event_mock = MagicMock() - event_mock.__iter__ = ["tags"] - event_mock.__contains__ = lambda _, val: val == "tags" - event_mock.tags = {"handled": handled} - result = process_before_send( hass, options=options, @@ -321,7 +315,7 @@ async def test_filter_handled_events(hass: HomeAssistant, handled, options, even huuid="12345", system_info={"installation_type": "pytest"}, custom_components=[], - event=event_mock, + event={"tags": {"handled": handled}}, hint={}, ) diff --git a/tests/components/shelly/conftest.py b/tests/components/shelly/conftest.py index 51659cf7736..71157124806 100644 --- a/tests/components/shelly/conftest.py +++ b/tests/components/shelly/conftest.py @@ -66,7 +66,7 @@ MOCK_SHELLY = { @pytest.fixture(autouse=True) def mock_coap(): """Mock out coap.""" - with patch("homeassistant.components.shelly.get_coap_context"): + with patch("homeassistant.components.shelly.utils.get_coap_context"): yield diff --git a/tests/components/sia/__init__.py b/tests/components/sia/__init__.py new file mode 100644 index 00000000000..198b6bc4bb8 --- /dev/null +++ b/tests/components/sia/__init__.py @@ -0,0 +1 @@ +"""Tests for the sia integration.""" diff --git a/tests/components/sia/test_config_flow.py b/tests/components/sia/test_config_flow.py new file mode 100644 index 00000000000..204518c1e5a --- /dev/null +++ b/tests/components/sia/test_config_flow.py @@ -0,0 +1,314 @@ +"""Test the sia config flow.""" +import logging +from unittest.mock import patch + +import pytest + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.sia.config_flow import ACCOUNT_SCHEMA, HUB_SCHEMA +from homeassistant.components.sia.const import ( + CONF_ACCOUNT, + CONF_ACCOUNTS, + CONF_ADDITIONAL_ACCOUNTS, + CONF_ENCRYPTION_KEY, + CONF_IGNORE_TIMESTAMPS, + CONF_PING_INTERVAL, + CONF_ZONES, + DOMAIN, +) +from homeassistant.const import CONF_PORT, CONF_PROTOCOL +from homeassistant.setup import async_setup_component + +from tests.common import MockConfigEntry + +_LOGGER = logging.getLogger(__name__) + + +BASIS_CONFIG_ENTRY_ID = 1 +BASIC_CONFIG = { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + CONF_ZONES: 1, + CONF_ADDITIONAL_ACCOUNTS: False, +} + +BASIC_OPTIONS = {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2} + +BASE_OUT = { + "data": { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNTS: [ + { + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + }, + ], + }, + "options": { + CONF_ACCOUNTS: {"ABCDEF": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 1}} + }, +} + +ADDITIONAL_CONFIG_ENTRY_ID = 2 +BASIC_CONFIG_ADDITIONAL = { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + CONF_ZONES: 1, + CONF_ADDITIONAL_ACCOUNTS: True, +} + +ADDITIONAL_ACCOUNT = { + CONF_ACCOUNT: "ACC2", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 2, + CONF_ZONES: 2, + CONF_ADDITIONAL_ACCOUNTS: False, +} +ADDITIONAL_OUT = { + "data": { + CONF_PORT: 7777, + CONF_PROTOCOL: "TCP", + CONF_ACCOUNTS: [ + { + CONF_ACCOUNT: "ABCDEF", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 10, + }, + { + CONF_ACCOUNT: "ACC2", + CONF_ENCRYPTION_KEY: "AAAAAAAAAAAAAAAA", + CONF_PING_INTERVAL: 2, + }, + ], + }, + "options": { + CONF_ACCOUNTS: { + "ABCDEF": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 1}, + "ACC2": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2}, + } + }, +} + +ADDITIONAL_OPTIONS = { + CONF_ACCOUNTS: { + "ABCDEF": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2}, + "ACC2": {CONF_IGNORE_TIMESTAMPS: False, CONF_ZONES: 2}, + } +} + +BASIC_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + data=BASE_OUT["data"], + options=BASE_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=BASIS_CONFIG_ENTRY_ID, + version=1, +) +ADDITIONAL_CONFIG_ENTRY = MockConfigEntry( + domain=DOMAIN, + data=ADDITIONAL_OUT["data"], + options=ADDITIONAL_OUT["options"], + title="SIA Alarm on port 7777", + entry_id=ADDITIONAL_CONFIG_ENTRY_ID, + version=1, +) + + +@pytest.fixture(params=[False, True], ids=["user", "add_account"]) +def additional(request) -> bool: + """Return True or False for the additional or base test.""" + return request.param + + +@pytest.fixture +async def flow_at_user_step(hass): + """Return a initialized flow.""" + return await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_USER}, + ) + + +@pytest.fixture +async def entry_with_basic_config(hass, flow_at_user_step): + """Return a entry with a basic config.""" + with patch("pysiaalarm.aio.SIAClient.start", return_value=True): + return await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG + ) + + +@pytest.fixture +async def flow_at_add_account_step(hass, flow_at_user_step): + """Return a initialized flow at the additional account step.""" + return await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + + +@pytest.fixture +async def entry_with_additional_account_config(hass, flow_at_add_account_step): + """Return a entry with a two account config.""" + with patch("pysiaalarm.aio.SIAClient.start", return_value=True): + return await hass.config_entries.flow.async_configure( + flow_at_add_account_step["flow_id"], ADDITIONAL_ACCOUNT + ) + + +async def setup_sia(hass, config_entry: MockConfigEntry): + """Add mock config to HASS.""" + assert await async_setup_component(hass, DOMAIN, {}) + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + +async def test_form_start( + hass, flow_at_user_step, flow_at_add_account_step, additional +): + """Start the form and check if you get the right id and schema.""" + if additional: + assert flow_at_add_account_step["step_id"] == "add_account" + assert flow_at_add_account_step["errors"] is None + assert flow_at_add_account_step["data_schema"] == ACCOUNT_SCHEMA + return + assert flow_at_user_step["step_id"] == "user" + assert flow_at_user_step["errors"] is None + assert flow_at_user_step["data_schema"] == HUB_SCHEMA + + +async def test_create(hass, entry_with_basic_config): + """Test we create a entry through the form.""" + assert entry_with_basic_config["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert ( + entry_with_basic_config["title"] + == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" + ) + assert entry_with_basic_config["data"] == BASE_OUT["data"] + assert entry_with_basic_config["options"] == BASE_OUT["options"] + + +async def test_create_additional_account(hass, entry_with_additional_account_config): + """Test we create a config with two accounts.""" + assert ( + entry_with_additional_account_config["type"] + == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + ) + assert ( + entry_with_additional_account_config["title"] + == f"SIA Alarm on port {BASIC_CONFIG[CONF_PORT]}" + ) + + assert entry_with_additional_account_config["data"] == ADDITIONAL_OUT["data"] + assert entry_with_additional_account_config["options"] == ADDITIONAL_OUT["options"] + + +async def test_abort_form(hass, entry_with_basic_config): + """Test aborting a config that already exists.""" + assert entry_with_basic_config["data"][CONF_PORT] == BASIC_CONFIG[CONF_PORT] + start_another_flow = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + get_abort = await hass.config_entries.flow.async_configure( + start_another_flow["flow_id"], BASIC_CONFIG + ) + assert get_abort["type"] == "abort" + assert get_abort["reason"] == "already_configured" + + +@pytest.mark.parametrize( + "field, value, error", + [ + ("encryption_key", "AAAAAAAAAAAAAZZZ", "invalid_key_format"), + ("encryption_key", "AAAAAAAAAAAAA", "invalid_key_length"), + ("account", "ZZZ", "invalid_account_format"), + ("account", "A", "invalid_account_length"), + ("ping_interval", 1500, "invalid_ping"), + ("zones", 0, "invalid_zones"), + ], +) +async def test_validation_errors( + hass, + flow_at_user_step, + additional, + field, + value, + error, +): + """Test we handle the different invalid inputs, both in the user and add_account flow.""" + config = BASIC_CONFIG.copy() + flow_id = flow_at_user_step["flow_id"] + if additional: + flow_at_add_account_step = await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + config = ADDITIONAL_ACCOUNT.copy() + flow_id = flow_at_add_account_step["flow_id"] + + config[field] = value + result_err = await hass.config_entries.flow.async_configure(flow_id, config) + assert result_err["type"] == "form" + assert result_err["errors"] == {"base": error} + + +async def test_unknown(hass, flow_at_user_step, additional): + """Test unknown exceptions.""" + flow_id = flow_at_user_step["flow_id"] + if additional: + flow_at_add_account_step = await hass.config_entries.flow.async_configure( + flow_at_user_step["flow_id"], BASIC_CONFIG_ADDITIONAL + ) + flow_id = flow_at_add_account_step["flow_id"] + with patch( + "pysiaalarm.SIAAccount.validate_account", + side_effect=Exception, + ): + config = ADDITIONAL_ACCOUNT if additional else BASIC_CONFIG + result_err = await hass.config_entries.flow.async_configure(flow_id, config) + assert result_err + assert result_err["step_id"] == "add_account" if additional else "user" + assert result_err["errors"] == {"base": "unknown"} + assert result_err["data_schema"] == ACCOUNT_SCHEMA if additional else HUB_SCHEMA + + +async def test_options_basic(hass): + """Test options flow for single account.""" + await setup_sia(hass, BASIC_CONFIG_ENTRY) + result = await hass.config_entries.options.async_init(BASIC_CONFIG_ENTRY.entry_id) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + assert result["last_step"] + + updated = await hass.config_entries.options.async_configure( + result["flow_id"], BASIC_OPTIONS + ) + assert updated["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert updated["data"] == { + CONF_ACCOUNTS: {BASIC_CONFIG[CONF_ACCOUNT]: BASIC_OPTIONS} + } + + +async def test_options_additional(hass): + """Test options flow for single account.""" + await setup_sia(hass, ADDITIONAL_CONFIG_ENTRY) + result = await hass.config_entries.options.async_init( + ADDITIONAL_CONFIG_ENTRY.entry_id + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "options" + assert not result["last_step"] + + updated = await hass.config_entries.options.async_configure( + result["flow_id"], BASIC_OPTIONS + ) + assert updated["type"] == data_entry_flow.RESULT_TYPE_FORM + assert updated["step_id"] == "options" + assert updated["last_step"] diff --git a/tests/components/smart_meter_texas/test_init.py b/tests/components/smart_meter_texas/test_init.py index 7db4113e3cf..0c49e6285ae 100644 --- a/tests/components/smart_meter_texas/test_init.py +++ b/tests/components/smart_meter_texas/test_init.py @@ -6,12 +6,7 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.smart_meter_texas.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component @@ -31,14 +26,14 @@ async def test_auth_failure(hass, config_entry, aioclient_mock): """Test if user's username or password is not accepted.""" await setup_integration(hass, config_entry, aioclient_mock, auth_fail=True) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR async def test_api_timeout(hass, config_entry, aioclient_mock): """Test that a timeout results in ConfigEntryNotReady.""" await setup_integration(hass, config_entry, aioclient_mock, auth_timeout=True) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_update_failure(hass, config_entry, aioclient_mock): @@ -64,9 +59,9 @@ async def test_unload_config_entry(hass, config_entry, aioclient_mock): config_entries = hass.config_entries.async_entries(DOMAIN) assert len(config_entries) == 1 assert config_entries[0] is config_entry - assert config_entry.state == ENTRY_STATE_LOADED + assert config_entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(config_entry.entry_id) await hass.async_block_till_done() - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/smartthings/conftest.py b/tests/components/smartthings/conftest.py index d145e7644a9..2a7b5ed7084 100644 --- a/tests/components/smartthings/conftest.py +++ b/tests/components/smartthings/conftest.py @@ -37,7 +37,7 @@ from homeassistant.components.smartthings.const import ( STORAGE_VERSION, ) from homeassistant.config import async_process_ha_core_config -from homeassistant.config_entries import CONN_CLASS_CLOUD_PUSH, SOURCE_USER, ConfigEntry +from homeassistant.config_entries import SOURCE_USER, ConfigEntry from homeassistant.const import ( CONF_ACCESS_TOKEN, CONF_CLIENT_ID, @@ -61,8 +61,6 @@ async def setup_platform(hass, platform: str, *, devices=None, scenes=None): "Test", {CONF_INSTALLED_APP_ID: str(uuid4())}, SOURCE_USER, - CONN_CLASS_CLOUD_PUSH, - system_options={}, ) broker = DeviceBroker( hass, config_entry, Mock(), Mock(), devices or [], scenes or [] @@ -231,7 +229,6 @@ def config_entry_fixture(hass, installed_app, location): title=location.name, version=2, source=SOURCE_USER, - connection_class=CONN_CLASS_CLOUD_PUSH, ) diff --git a/tests/components/smartthings/test_binary_sensor.py b/tests/components/smartthings/test_binary_sensor.py index 7e8ab7d2c9b..f3d548c1e39 100644 --- a/tests/components/smartthings/test_binary_sensor.py +++ b/tests/components/smartthings/test_binary_sensor.py @@ -12,6 +12,7 @@ from homeassistant.components.binary_sensor import ( ) from homeassistant.components.smartthings import binary_sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_FRIENDLY_NAME, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -91,6 +92,7 @@ async def test_unload_config_entry(hass, device_factory): "Motion Sensor 1", [Capability.motion_sensor], {Attribute.motion: "inactive"} ) config_entry = await setup_platform(hass, BINARY_SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "binary_sensor") # Assert diff --git a/tests/components/smartthings/test_cover.py b/tests/components/smartthings/test_cover.py index 44c2b2f9285..aad7a4b037e 100644 --- a/tests/components/smartthings/test_cover.py +++ b/tests/components/smartthings/test_cover.py @@ -19,6 +19,7 @@ from homeassistant.components.cover import ( STATE_OPENING, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_BATTERY_LEVEL, ATTR_ENTITY_ID, STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -191,6 +192,7 @@ async def test_unload_config_entry(hass, device_factory): "Garage", [Capability.garage_door_control], {Attribute.door: "open"} ) config_entry = await setup_platform(hass, COVER_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, COVER_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_fan.py b/tests/components/smartthings/test_fan.py index 6cdfa5b8917..2a66fc646c7 100644 --- a/tests/components/smartthings/test_fan.py +++ b/tests/components/smartthings/test_fan.py @@ -17,6 +17,7 @@ from homeassistant.components.fan import ( SUPPORT_SET_SPEED, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -186,6 +187,7 @@ async def test_unload_config_entry(hass, device_factory): status={Attribute.switch: "off", Attribute.fan_speed: 0}, ) config_entry = await setup_platform(hass, FAN_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "fan") # Assert diff --git a/tests/components/smartthings/test_light.py b/tests/components/smartthings/test_light.py index c9dbb094161..81062adf934 100644 --- a/tests/components/smartthings/test_light.py +++ b/tests/components/smartthings/test_light.py @@ -19,6 +19,7 @@ from homeassistant.components.light import ( SUPPORT_TRANSITION, ) from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_ENTITY_ID, ATTR_SUPPORTED_FEATURES, @@ -306,6 +307,7 @@ async def test_unload_config_entry(hass, device_factory): }, ) config_entry = await setup_platform(hass, LIGHT_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "light") # Assert diff --git a/tests/components/smartthings/test_lock.py b/tests/components/smartthings/test_lock.py index 1168108656e..86c8d534a71 100644 --- a/tests/components/smartthings/test_lock.py +++ b/tests/components/smartthings/test_lock.py @@ -9,6 +9,7 @@ from pysmartthings.device import Status from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -103,6 +104,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Lock_1", [Capability.lock], {Attribute.lock: "locked"}) config_entry = await setup_platform(hass, LOCK_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "lock") # Assert diff --git a/tests/components/smartthings/test_scene.py b/tests/components/smartthings/test_scene.py index 647389eeb42..288fae046f5 100644 --- a/tests/components/smartthings/test_scene.py +++ b/tests/components/smartthings/test_scene.py @@ -5,6 +5,7 @@ The only mocking required is of the underlying SmartThings API object so real HTTP calls are not initiated during testing. """ from homeassistant.components.scene import DOMAIN as SCENE_DOMAIN +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID, SERVICE_TURN_ON, STATE_UNAVAILABLE from homeassistant.helpers import entity_registry as er @@ -44,6 +45,7 @@ async def test_unload_config_entry(hass, scene): """Test the scene is removed when the config entry is unloaded.""" # Arrange config_entry = await setup_platform(hass, SCENE_DOMAIN, scenes=[scene]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, SCENE_DOMAIN) # Assert diff --git a/tests/components/smartthings/test_sensor.py b/tests/components/smartthings/test_sensor.py index 0f148b8931f..4af88e27fe4 100644 --- a/tests/components/smartthings/test_sensor.py +++ b/tests/components/smartthings/test_sensor.py @@ -9,6 +9,7 @@ from pysmartthings import ATTRIBUTES, CAPABILITIES, Attribute, Capability from homeassistant.components.sensor import DEVICE_CLASSES, DOMAIN as SENSOR_DOMAIN from homeassistant.components.smartthings import sensor from homeassistant.components.smartthings.const import DOMAIN, SIGNAL_SMARTTHINGS_UPDATE +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ( ATTR_FRIENDLY_NAME, ATTR_UNIT_OF_MEASUREMENT, @@ -116,6 +117,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Sensor 1", [Capability.battery], {Attribute.battery: 100}) config_entry = await setup_platform(hass, SENSOR_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "sensor") # Assert diff --git a/tests/components/smartthings/test_switch.py b/tests/components/smartthings/test_switch.py index 21d508bcbc2..7c202fad12e 100644 --- a/tests/components/smartthings/test_switch.py +++ b/tests/components/smartthings/test_switch.py @@ -12,6 +12,7 @@ from homeassistant.components.switch import ( ATTR_TODAY_ENERGY_KWH, DOMAIN as SWITCH_DOMAIN, ) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -95,6 +96,7 @@ async def test_unload_config_entry(hass, device_factory): # Arrange device = device_factory("Switch 1", [Capability.switch], {Attribute.switch: "on"}) config_entry = await setup_platform(hass, SWITCH_DOMAIN, devices=[device]) + config_entry.state = ConfigEntryState.LOADED # Act await hass.config_entries.async_forward_entry_unload(config_entry, "switch") # Assert diff --git a/tests/components/smarttub/conftest.py b/tests/components/smarttub/conftest.py index 84566fcccc5..c05762a903d 100644 --- a/tests/components/smarttub/conftest.py +++ b/tests/components/smarttub/conftest.py @@ -35,13 +35,67 @@ async def setup_component(hass): @pytest.fixture(name="spa") -def mock_spa(): +def mock_spa(spa_state): """Mock a smarttub.Spa.""" mock_spa = create_autospec(smarttub.Spa, instance=True) mock_spa.id = "mockspa1" mock_spa.brand = "mockbrand1" mock_spa.model = "mockmodel1" + + mock_spa.get_status_full.return_value = spa_state + + mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) + mock_circulation_pump.id = "CP" + mock_circulation_pump.spa = mock_spa + mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF + mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION + + mock_jet_off = create_autospec(smarttub.SpaPump, instance=True) + mock_jet_off.id = "P1" + mock_jet_off.spa = mock_spa + mock_jet_off.state = smarttub.SpaPump.PumpState.OFF + mock_jet_off.type = smarttub.SpaPump.PumpType.JET + + mock_jet_on = create_autospec(smarttub.SpaPump, instance=True) + mock_jet_on.id = "P2" + mock_jet_on.spa = mock_spa + mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH + mock_jet_on.type = smarttub.SpaPump.PumpType.JET + + spa_state.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on] + + mock_light_off = create_autospec(smarttub.SpaLight, instance=True) + mock_light_off.spa = mock_spa + mock_light_off.zone = 1 + mock_light_off.intensity = 0 + mock_light_off.mode = smarttub.SpaLight.LightMode.OFF + + mock_light_on = create_autospec(smarttub.SpaLight, instance=True) + mock_light_on.spa = mock_spa + mock_light_on.zone = 2 + mock_light_on.intensity = 50 + mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE + + spa_state.lights = [mock_light_off, mock_light_on] + + mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True) + mock_filter_reminder.id = "FILTER01" + mock_filter_reminder.name = "MyFilter" + mock_filter_reminder.remaining_days = 2 + mock_filter_reminder.snoozed = False + + mock_spa.get_reminders.return_value = [mock_filter_reminder] + + mock_spa.get_errors.return_value = [] + + return mock_spa + + +@pytest.fixture(name="spa_state") +def mock_spa_state(): + """Create a smarttub.SpaStateFull with mocks.""" + full_status = smarttub.SpaStateFull( mock_spa, { @@ -73,51 +127,15 @@ def mock_spa(): "pumps": [], }, ) - mock_spa.get_status_full.return_value = full_status - mock_circulation_pump = create_autospec(smarttub.SpaPump, instance=True) - mock_circulation_pump.id = "CP" - mock_circulation_pump.spa = mock_spa - mock_circulation_pump.state = smarttub.SpaPump.PumpState.OFF - mock_circulation_pump.type = smarttub.SpaPump.PumpType.CIRCULATION + full_status.primary_filtration.set = create_autospec( + smarttub.SpaPrimaryFiltrationCycle, instance=True + ).set + full_status.secondary_filtration.set_mode = create_autospec( + smarttub.SpaSecondaryFiltrationCycle, instance=True + ).set_mode - mock_jet_off = create_autospec(smarttub.SpaPump, instance=True) - mock_jet_off.id = "P1" - mock_jet_off.spa = mock_spa - mock_jet_off.state = smarttub.SpaPump.PumpState.OFF - mock_jet_off.type = smarttub.SpaPump.PumpType.JET - - mock_jet_on = create_autospec(smarttub.SpaPump, instance=True) - mock_jet_on.id = "P2" - mock_jet_on.spa = mock_spa - mock_jet_on.state = smarttub.SpaPump.PumpState.HIGH - mock_jet_on.type = smarttub.SpaPump.PumpType.JET - - full_status.pumps = [mock_circulation_pump, mock_jet_off, mock_jet_on] - - mock_light_off = create_autospec(smarttub.SpaLight, instance=True) - mock_light_off.spa = mock_spa - mock_light_off.zone = 1 - mock_light_off.intensity = 0 - mock_light_off.mode = smarttub.SpaLight.LightMode.OFF - - mock_light_on = create_autospec(smarttub.SpaLight, instance=True) - mock_light_on.spa = mock_spa - mock_light_on.zone = 2 - mock_light_on.intensity = 50 - mock_light_on.mode = smarttub.SpaLight.LightMode.PURPLE - - full_status.lights = [mock_light_off, mock_light_on] - - mock_filter_reminder = create_autospec(smarttub.SpaReminder, instance=True) - mock_filter_reminder.id = "FILTER01" - mock_filter_reminder.name = "MyFilter" - mock_filter_reminder.remaining_days = 2 - mock_filter_reminder.snoozed = False - - mock_spa.get_reminders.return_value = [mock_filter_reminder] - - return mock_spa + return full_status @pytest.fixture(name="account") diff --git a/tests/components/smarttub/test_binary_sensor.py b/tests/components/smarttub/test_binary_sensor.py index b5a7c516a0e..16b4f60d3e4 100644 --- a/tests/components/smarttub/test_binary_sensor.py +++ b/tests/components/smarttub/test_binary_sensor.py @@ -1,5 +1,11 @@ """Test the SmartTub binary sensor platform.""" -from homeassistant.components.binary_sensor import STATE_OFF +from datetime import datetime +from unittest.mock import create_autospec + +import pytest +import smarttub + +from homeassistant.components.binary_sensor import STATE_OFF, STATE_ON async def test_binary_sensors(spa, setup_entry, hass): @@ -10,6 +16,11 @@ async def test_binary_sensors(spa, setup_entry, hass): # disabled by default assert state is None + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_error" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == STATE_OFF + async def test_reminders(spa, setup_entry, hass): """Test the reminder sensor.""" @@ -19,3 +30,54 @@ async def test_reminders(spa, setup_entry, hass): assert state is not None assert state.state == STATE_OFF assert state.attributes["snoozed"] is False + + +@pytest.fixture +def mock_error(spa): + """Mock error.""" + error = create_autospec(smarttub.SpaError, instance=True) + error.code = 11 + error.title = "Flow Switch Stuck Open" + error.description = None + error.active = True + error.created_at = datetime.now() + error.updated_at = datetime.now() + error.error_type = "TUB_ERROR" + return error + + +async def test_error(spa, hass, config_entry, mock_error): + """Test the error sensor.""" + + spa.get_errors.return_value = [mock_error] + + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_error" + state = hass.states.get(entity_id) + assert state is not None + + assert state.state == STATE_ON + assert state.attributes["error_code"] == 11 + + +async def test_snooze(spa, setup_entry, hass): + """Test snoozing a reminder.""" + + entity_id = f"binary_sensor.{spa.brand}_{spa.model}_myfilter_reminder" + reminder = spa.get_reminders.return_value[0] + days = 30 + + await hass.services.async_call( + "smarttub", + "snooze_reminder", + { + "entity_id": entity_id, + "days": 30, + }, + blocking=True, + ) + + reminder.snooze.assert_called_with(days) diff --git a/tests/components/smarttub/test_climate.py b/tests/components/smarttub/test_climate.py index a9c2de4e6e2..83a223cee98 100644 --- a/tests/components/smarttub/test_climate.py +++ b/tests/components/smarttub/test_climate.py @@ -33,7 +33,7 @@ from homeassistant.const import ( from . import trigger_update -async def test_thermostat_update(spa, setup_entry, hass): +async def test_thermostat_update(spa, spa_state, setup_entry, hass): """Test the thermostat entity.""" entity_id = f"climate.{spa.brand}_{spa.model}_thermostat" @@ -42,7 +42,7 @@ async def test_thermostat_update(spa, setup_entry, hass): assert state.attributes[ATTR_HVAC_ACTION] == CURRENT_HVAC_HEAT - spa.get_status_full.return_value.heater = "OFF" + spa_state.heater = "OFF" await trigger_update(hass) state = hass.states.get(entity_id) @@ -85,7 +85,7 @@ async def test_thermostat_update(spa, setup_entry, hass): ) spa.set_heat_mode.assert_called_with(smarttub.Spa.HeatMode.ECONOMY) - spa.get_status_full.return_value.heat_mode = smarttub.Spa.HeatMode.ECONOMY + spa_state.heat_mode = smarttub.Spa.HeatMode.ECONOMY await trigger_update(hass) state = hass.states.get(entity_id) assert state.attributes.get(ATTR_PRESET_MODE) == PRESET_ECO diff --git a/tests/components/smarttub/test_init.py b/tests/components/smarttub/test_init.py index df44edb3da3..f316a66c5d1 100644 --- a/tests/components/smarttub/test_init.py +++ b/tests/components/smarttub/test_init.py @@ -7,11 +7,7 @@ from smarttub import LoginFailed from homeassistant.components import smarttub from homeassistant.components.smarttub.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, - SOURCE_REAUTH, -) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.setup import async_setup_component @@ -30,7 +26,7 @@ async def test_setup_entry_not_ready(setup_component, hass, config_entry, smartt config_entry.add_to_hass(hass) await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_RETRY + assert config_entry.state is ConfigEntryState.SETUP_RETRY async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_api): @@ -40,7 +36,7 @@ async def test_setup_auth_failed(setup_component, hass, config_entry, smarttub_a config_entry.add_to_hass(hass) with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: await hass.config_entries.async_setup(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_SETUP_ERROR + assert config_entry.state is ConfigEntryState.SETUP_ERROR mock_flow_init.assert_called_with( DOMAIN, context={ diff --git a/tests/components/smarttub/test_sensor.py b/tests/components/smarttub/test_sensor.py index 4f179b24910..025e0b6e13e 100644 --- a/tests/components/smarttub/test_sensor.py +++ b/tests/components/smarttub/test_sensor.py @@ -1,6 +1,7 @@ """Test the SmartTub sensor platform.""" import pytest +import smarttub @pytest.mark.parametrize( @@ -23,7 +24,7 @@ async def test_sensor(spa, setup_entry, hass, entity_suffix, expected_state): assert state.state == expected_state -async def test_primary_filtration(spa, setup_entry, hass): +async def test_primary_filtration(spa, spa_state, setup_entry, hass): """Test the primary filtration cycle sensor.""" entity_id = f"sensor.{spa.brand}_{spa.model}_primary_filtration_cycle" @@ -35,8 +36,16 @@ async def test_primary_filtration(spa, setup_entry, hass): assert state.attributes["mode"] == "normal" assert state.attributes["start_hour"] == 2 + await hass.services.async_call( + "smarttub", + "set_primary_filtration", + {"entity_id": entity_id, "duration": 8, "start_hour": 1}, + blocking=True, + ) + spa_state.primary_filtration.set.assert_called_with(duration=8, start_hour=1) -async def test_secondary_filtration(spa, setup_entry, hass): + +async def test_secondary_filtration(spa, spa_state, setup_entry, hass): """Test the secondary filtration cycle sensor.""" entity_id = f"sensor.{spa.brand}_{spa.model}_secondary_filtration_cycle" @@ -45,3 +54,16 @@ async def test_secondary_filtration(spa, setup_entry, hass): assert state.state == "inactive" assert state.attributes["cycle_last_updated"] is not None assert state.attributes["mode"] == "away" + + await hass.services.async_call( + "smarttub", + "set_secondary_filtration", + { + "entity_id": entity_id, + "mode": "frequent", + }, + blocking=True, + ) + spa_state.secondary_filtration.set_mode.assert_called_with( + mode=smarttub.SpaSecondaryFiltrationCycle.SecondaryFiltrationMode.FREQUENT + ) diff --git a/tests/components/smhi/__init__.py b/tests/components/smhi/__init__.py index 100b1f1bbb1..d815aafc8f5 100644 --- a/tests/components/smhi/__init__.py +++ b/tests/components/smhi/__init__.py @@ -1 +1,3 @@ """Tests for the SMHI component.""" +ENTITY_ID = "weather.smhi_test" +TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} diff --git a/tests/components/smhi/conftest.py b/tests/components/smhi/conftest.py new file mode 100644 index 00000000000..6ededa6d975 --- /dev/null +++ b/tests/components/smhi/conftest.py @@ -0,0 +1,10 @@ +"""Provide common smhi fixtures.""" +import pytest + +from tests.common import load_fixture + + +@pytest.fixture(scope="session") +def api_response(): + """Return an API response.""" + return load_fixture("smhi.json") diff --git a/tests/components/smhi/test_init.py b/tests/components/smhi/test_init.py index ac4177dca7d..ab937d266a4 100644 --- a/tests/components/smhi/test_init.py +++ b/tests/components/smhi/test_init.py @@ -1,30 +1,44 @@ """Test SMHI component setup process.""" -from unittest.mock import Mock +from smhi.smhi_lib import APIURL_TEMPLATE -from homeassistant.components import smhi +from homeassistant.components.smhi.const import DOMAIN +from homeassistant.core import HomeAssistant -from .common import AsyncMock +from . import ENTITY_ID, TEST_CONFIG -TEST_CONFIG = { - "config": { - "name": "0123456789ABCDEF", - "longitude": "62.0022", - "latitude": "17.0022", - } -} +from tests.common import MockConfigEntry +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_forward_async_setup_entry() -> None: - """Test that it will forward setup entry.""" - hass = Mock() +async def test_setup_entry( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test setup entry.""" + uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) + aioclient_mock.get(uri, text=api_response) + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry.add_to_hass(hass) - assert await smhi.async_setup_entry(hass, {}) is True - assert len(hass.config_entries.async_setup_platforms.mock_calls) == 1 + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state -async def test_forward_async_unload_entry() -> None: - """Test that it will forward unload entry.""" - hass = AsyncMock() - hass.config_entries.async_unload_platforms = AsyncMock(return_value=True) - assert await smhi.async_unload_entry(hass, {}) is True - assert len(hass.config_entries.async_unload_platforms.mock_calls) == 1 +async def test_remove_entry(hass: HomeAssistant) -> None: + """Test remove entry.""" + entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert state + + await hass.config_entries.async_remove(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + assert not state diff --git a/tests/components/smhi/test_weather.py b/tests/components/smhi/test_weather.py index 9170f3a9ed0..33214be0ae3 100644 --- a/tests/components/smhi/test_weather.py +++ b/tests/components/smhi/test_weather.py @@ -1,14 +1,19 @@ """Test for the smhi weather entity.""" import asyncio -from datetime import datetime -import logging -from unittest.mock import AsyncMock, Mock, patch +from datetime import datetime, timedelta +from unittest.mock import patch -from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecastException +import pytest +from smhi.smhi_lib import APIURL_TEMPLATE, SmhiForecast, SmhiForecastException -from homeassistant.components.smhi import weather as weather_smhi -from homeassistant.components.smhi.const import ATTR_SMHI_CLOUDINESS +from homeassistant.components.smhi.const import ( + ATTR_SMHI_CLOUDINESS, + ATTR_SMHI_THUNDER_PROBABILITY, + ATTR_SMHI_WIND_GUST_SPEED, +) +from homeassistant.components.smhi.weather import CONDITION_CLASSES, RETRY_TIMEOUT from homeassistant.components.weather import ( + ATTR_FORECAST, ATTR_FORECAST_CONDITION, ATTR_FORECAST_PRECIPITATION, ATTR_FORECAST_TEMP, @@ -21,42 +26,40 @@ from homeassistant.components.weather import ( ATTR_WEATHER_VISIBILITY, ATTR_WEATHER_WIND_BEARING, ATTR_WEATHER_WIND_SPEED, - DOMAIN as WEATHER_DOMAIN, ) -from homeassistant.const import TEMP_CELSIUS +from homeassistant.const import STATE_UNKNOWN from homeassistant.core import HomeAssistant +from homeassistant.util.dt import utcnow -from tests.common import MockConfigEntry, load_fixture +from . import ENTITY_ID, TEST_CONFIG -_LOGGER = logging.getLogger(__name__) - -TEST_CONFIG = {"name": "test", "longitude": "17.84197", "latitude": "59.32624"} +from tests.common import MockConfigEntry, async_fire_time_changed +from tests.test_util.aiohttp import AiohttpClientMocker -async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: - """Test for successfully setting up the smhi platform. - - This test are deeper integrated with the core. Since only - config_flow is used the component are setup with - "async_forward_entry_setup". The actual result are tested - with the entity state rather than "per function" unity tests - """ +async def test_setup_hass( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker, api_response: str +) -> None: + """Test for successfully setting up the smhi integration.""" uri = APIURL_TEMPLATE.format(TEST_CONFIG["longitude"], TEST_CONFIG["latitude"]) - api_response = load_fixture("smhi.json") aioclient_mock.get(uri, text=api_response) entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) - await hass.config_entries.async_forward_entry_setup(entry, WEATHER_DOMAIN) + await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() assert aioclient_mock.call_count == 1 # Testing the actual entity state for # deeper testing than normal unity test - state = hass.states.get("weather.smhi_test") + state = hass.states.get(ENTITY_ID) + assert state assert state.state == "sunny" assert state.attributes[ATTR_SMHI_CLOUDINESS] == 50 + assert state.attributes[ATTR_SMHI_THUNDER_PROBABILITY] == 33 + assert state.attributes[ATTR_SMHI_WIND_GUST_SPEED] == 17 assert state.attributes[ATTR_WEATHER_ATTRIBUTION].find("SMHI") >= 0 assert state.attributes[ATTR_WEATHER_HUMIDITY] == 55 assert state.attributes[ATTR_WEATHER_PRESSURE] == 1024 @@ -64,7 +67,6 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert state.attributes[ATTR_WEATHER_VISIBILITY] == 50 assert state.attributes[ATTR_WEATHER_WIND_SPEED] == 7 assert state.attributes[ATTR_WEATHER_WIND_BEARING] == 134 - _LOGGER.error(state.attributes) assert len(state.attributes["forecast"]) == 4 forecast = state.attributes["forecast"][1] @@ -75,149 +77,171 @@ async def test_setup_hass(hass: HomeAssistant, aioclient_mock) -> None: assert forecast[ATTR_FORECAST_CONDITION] == "partlycloudy" -def test_properties_no_data(hass: HomeAssistant) -> None: +async def test_properties_no_data(hass: HomeAssistant) -> None: """Test properties when no API data available.""" - weather = weather_smhi.SmhiWeather("name", "10", "10") - weather.hass = hass + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) - assert weather.name == "name" - assert weather.should_poll is True - assert weather.temperature is None - assert weather.humidity is None - assert weather.wind_speed is None - assert weather.wind_bearing is None - assert weather.visibility is None - assert weather.pressure is None - assert weather.cloudiness is None - assert weather.condition is None - assert weather.forecast is None - assert weather.temperature_unit == TEMP_CELSIUS + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + side_effect=SmhiForecastException("boom"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert ( + state.attributes[ATTR_WEATHER_ATTRIBUTION] == "Swedish weather institute (SMHI)" + ) + assert ATTR_WEATHER_HUMIDITY not in state.attributes + assert ATTR_WEATHER_PRESSURE not in state.attributes + assert ATTR_WEATHER_TEMPERATURE not in state.attributes + assert ATTR_WEATHER_VISIBILITY not in state.attributes + assert ATTR_WEATHER_WIND_SPEED not in state.attributes + assert ATTR_WEATHER_WIND_BEARING not in state.attributes + assert ATTR_FORECAST not in state.attributes + assert ATTR_SMHI_CLOUDINESS not in state.attributes + assert ATTR_SMHI_THUNDER_PROBABILITY not in state.attributes + assert ATTR_SMHI_WIND_GUST_SPEED not in state.attributes -# pylint: disable=protected-access -def test_properties_unknown_symbol() -> None: +async def test_properties_unknown_symbol(hass: HomeAssistant) -> None: """Test behaviour when unknown symbol from API.""" - hass = Mock() - data = Mock() - data.temperature = 5 - data.mean_precipitation = 0.5 - data.total_precipitation = 1 - data.humidity = 5 - data.wind_speed = 10 - data.wind_direction = 180 - data.horizontal_visibility = 6 - data.pressure = 1008 - data.cloudiness = 52 - data.symbol = 100 # Faulty symbol - data.valid_time = datetime(2018, 1, 1, 0, 1, 2) + data = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 1, 0, 1, 2), + ) - data2 = Mock() - data2.temperature = 5 - data2.mean_precipitation = 0.5 - data2.total_precipitation = 1 - data2.humidity = 5 - data2.wind_speed = 10 - data2.wind_direction = 180 - data2.horizontal_visibility = 6 - data2.pressure = 1008 - data2.cloudiness = 52 - data2.symbol = 100 # Faulty symbol - data2.valid_time = datetime(2018, 1, 1, 12, 1, 2) + data2 = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 1, 12, 1, 2), + ) - data3 = Mock() - data3.temperature = 5 - data3.mean_precipitation = 0.5 - data3.total_precipitation = 1 - data3.humidity = 5 - data3.wind_speed = 10 - data3.wind_direction = 180 - data3.horizontal_visibility = 6 - data3.pressure = 1008 - data3.cloudiness = 52 - data3.symbol = 100 # Faulty symbol - data3.valid_time = datetime(2018, 1, 2, 12, 1, 2) + data3 = SmhiForecast( + temperature=5, + temperature_max=10, + temperature_min=0, + humidity=5, + pressure=1008, + thunder=0, + cloudiness=52, + precipitation=1, + wind_direction=180, + wind_speed=10, + horizontal_visibility=6, + wind_gust=1.5, + mean_precipitation=0.5, + total_precipitation=1, + symbol=100, # Faulty symbol + valid_time=datetime(2018, 1, 2, 12, 1, 2), + ) testdata = [data, data2, data3] - weather = weather_smhi.SmhiWeather("name", "10", "10") - weather.hass = hass - weather._forecasts = testdata - assert weather.condition is None - forecast = weather.forecast[0] - assert forecast[ATTR_FORECAST_CONDITION] is None + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + return_value=testdata, + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + state = hass.states.get(ENTITY_ID) + + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert ATTR_FORECAST in state.attributes + assert all( + forecast[ATTR_FORECAST_CONDITION] is None + for forecast in state.attributes[ATTR_FORECAST] + ) -# pylint: disable=protected-access -async def test_refresh_weather_forecast_exceeds_retries(hass) -> None: +@pytest.mark.parametrize("error", [SmhiForecastException(), asyncio.TimeoutError()]) +async def test_refresh_weather_forecast_retry( + hass: HomeAssistant, error: Exception +) -> None: """Test the refresh weather forecast function.""" + entry = MockConfigEntry(domain="smhi", data=TEST_CONFIG) + entry.add_to_hass(hass) + now = utcnow() - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=SmhiForecastException(), - ): + with patch( + "homeassistant.components.smhi.weather.Smhi.async_get_forecast", + side_effect=error, + ) as mock_get_forecast: + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - weather._fail_count = 2 + state = hass.states.get(ENTITY_ID) - await weather.async_update() - assert weather._forecasts is None - assert not call_later.mock_calls + assert state + assert state.name == "test" + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 1 + future = now + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() -async def test_refresh_weather_forecast_timeout(hass) -> None: - """Test timeout exception.""" - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 2 - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather_smhi.SmhiWeather, "retry_update" - ), patch.object( - weather_smhi.SmhiWeather, - "get_weather_forecast", - side_effect=asyncio.TimeoutError, - ): + future = future + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() - await weather.async_update() - assert len(call_later.mock_calls) == 1 - # Assert we are going to wait RETRY_TIMEOUT seconds - assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + assert mock_get_forecast.call_count == 3 + future = future + timedelta(seconds=RETRY_TIMEOUT + 1) + async_fire_time_changed(hass, future) + await hass.async_block_till_done() -async def test_refresh_weather_forecast_exception() -> None: - """Test any exception.""" - - hass = Mock() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - - with patch.object( - hass.helpers.event, "async_call_later" - ) as call_later, patch.object( - weather, - "get_weather_forecast", - side_effect=SmhiForecastException(), - ): - await weather.async_update() - assert len(call_later.mock_calls) == 1 - # Assert we are going to wait RETRY_TIMEOUT seconds - assert call_later.mock_calls[0][1][0] == weather_smhi.RETRY_TIMEOUT - - -async def test_retry_update(): - """Test retry function of refresh forecast.""" - hass = Mock() - weather = weather_smhi.SmhiWeather("name", "17.0022", "62.0022") - weather.hass = hass - - with patch.object(weather, "async_update", AsyncMock()) as update: - await weather.retry_update(None) - assert len(update.mock_calls) == 1 + state = hass.states.get(ENTITY_ID) + assert state + assert state.state == STATE_UNKNOWN + # after three failed retries we stop retrying and go back to normal interval + assert mock_get_forecast.call_count == 3 def test_condition_class(): @@ -225,7 +249,7 @@ def test_condition_class(): def get_condition(index: int) -> str: """Return condition given index.""" - return [k for k, v in weather_smhi.CONDITION_CLASSES.items() if index in v][0] + return [k for k, v in CONDITION_CLASSES.items() if index in v][0] # SMHI definitions as follows, see # http://opendata.smhi.se/apidocs/metfcst/parameters.html diff --git a/tests/components/solaredge/test_config_flow.py b/tests/components/solaredge/test_config_flow.py index bb21607fead..059e10c7662 100644 --- a/tests/components/solaredge/test_config_flow.py +++ b/tests/components/solaredge/test_config_flow.py @@ -5,8 +5,8 @@ import pytest from requests.exceptions import ConnectTimeout, HTTPError from homeassistant import data_entry_flow -from homeassistant.components.solaredge import config_flow -from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME +from homeassistant.components.solaredge.const import CONF_SITE_ID, DEFAULT_NAME, DOMAIN +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_API_KEY, CONF_NAME from homeassistant.core import HomeAssistant @@ -26,109 +26,86 @@ def mock_controller(): yield api -def init_config_flow(hass: HomeAssistant) -> config_flow.SolarEdgeConfigFlow: - """Init a configuration flow.""" - flow = config_flow.SolarEdgeConfigFlow() - flow.hass = hass - return flow - - async def test_user(hass: HomeAssistant, test_api: Mock) -> None: """Test user config.""" - flow = init_config_flow(hass) - - result = await flow.async_step_user() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "user" - - # tets with all provided - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "solaredge_site_1_2_3" - assert result["data"][CONF_SITE_ID] == SITE_ID - assert result["data"][CONF_API_KEY] == API_KEY + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" - -async def test_import(hass: HomeAssistant, test_api: Mock) -> None: - """Test import step.""" - flow = init_config_flow(hass) - - # import with site_id and api_key - result = await flow.async_step_import( - {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + # test with all provided + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "solaredge" - assert result["data"][CONF_SITE_ID] == SITE_ID - assert result["data"][CONF_API_KEY] == API_KEY + assert result.get("type") == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("title") == "solaredge_site_1_2_3" - # import with all - result = await flow.async_step_import( - {CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID, CONF_NAME: NAME} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "solaredge_site_1_2_3" - assert result["data"][CONF_SITE_ID] == SITE_ID - assert result["data"][CONF_API_KEY] == API_KEY + data = result.get("data") + assert data + assert data[CONF_SITE_ID] == SITE_ID + assert data[CONF_API_KEY] == API_KEY async def test_abort_if_already_setup(hass: HomeAssistant, test_api: str) -> None: """Test we abort if the site_id is already setup.""" - flow = init_config_flow(hass) MockConfigEntry( domain="solaredge", data={CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY}, ).add_to_hass(hass) - # import: Should fail, same SITE_ID - result = await flow.async_step_import( - {CONF_NAME: DEFAULT_NAME, CONF_SITE_ID: SITE_ID, CONF_API_KEY: API_KEY} - ) - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - # user: Should fail, same SITE_ID - result = await flow.async_step_user( - {CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: "test", CONF_SITE_ID: SITE_ID, CONF_API_KEY: "test"}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "already_configured"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "already_configured"} async def test_asserts(hass: HomeAssistant, test_api: Mock) -> None: """Test the _site_in_configuration_exists method.""" - flow = init_config_flow(hass) # test with inactive site test_api.get_details.return_value = {"details": {"status": "NOK"}} - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "site_not_active"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "site_not_active"} # test with api_failure test_api.get_details.return_value = {} - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "invalid_api_key"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "invalid_api_key"} # test with ConnectionTimeout test_api.get_details.side_effect = ConnectTimeout() - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "could_not_connect"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} # test with HTTPError test_api.get_details.side_effect = HTTPError() - result = await flow.async_step_user( - {CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_NAME: NAME, CONF_API_KEY: API_KEY, CONF_SITE_ID: SITE_ID}, ) - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["errors"] == {CONF_SITE_ID: "could_not_connect"} + assert result.get("type") == data_entry_flow.RESULT_TYPE_FORM + assert result.get("errors") == {CONF_SITE_ID: "could_not_connect"} diff --git a/tests/components/somfy/test_config_flow.py b/tests/components/somfy/test_config_flow.py index 60200824c00..b7d78883706 100644 --- a/tests/components/somfy/test_config_flow.py +++ b/tests/components/somfy/test_config_flow.py @@ -2,10 +2,8 @@ import asyncio from unittest.mock import patch -import pytest - from homeassistant import config_entries, data_entry_flow, setup -from homeassistant.components.somfy import DOMAIN, config_flow +from homeassistant.components.somfy import DOMAIN from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.helpers import config_entry_oauth2_flow @@ -15,39 +13,22 @@ CLIENT_ID_VALUE = "1234" CLIENT_SECRET_VALUE = "5678" -@pytest.fixture() -async def mock_impl(hass): - """Mock implementation.""" - await setup.async_setup_component(hass, "http", {}) - - impl = config_entry_oauth2_flow.LocalOAuth2Implementation( - hass, - DOMAIN, - CLIENT_ID_VALUE, - CLIENT_SECRET_VALUE, - "https://accounts.somfy.com/oauth/oauth/v2/auth", - "https://accounts.somfy.com/oauth/oauth/v2/token", - ) - config_flow.SomfyFlowHandler.async_register_implementation(hass, impl) - return impl - - async def test_abort_if_no_configuration(hass): """Check flow abort when no configuration.""" - flow = config_flow.SomfyFlowHandler() - flow.hass = hass - result = await flow.async_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_ABORT assert result["reason"] == "missing_configuration" async def test_abort_if_existing_entry(hass): """Check flow abort when an entry already exist.""" - flow = config_flow.SomfyFlowHandler() - flow.hass = hass MockConfigEntry(domain=DOMAIN).add_to_hass(hass) - result = await flow.async_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_ABORT assert result["reason"] == "single_instance_allowed" @@ -63,8 +44,7 @@ async def test_full_flow( DOMAIN: { CONF_CLIENT_ID: CLIENT_ID_VALUE, CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, - }, - "http": {"base_url": "https://example.com"}, + } }, ) @@ -117,23 +97,33 @@ async def test_full_flow( assert DOMAIN in hass.config.components entry = hass.config_entries.async_entries(DOMAIN)[0] - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert await hass.config_entries.async_unload(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED -async def test_abort_if_authorization_timeout( - hass, mock_impl, current_request_with_host -): +async def test_abort_if_authorization_timeout(hass, current_request_with_host): """Check Somfy authorization timeout.""" - flow = config_flow.SomfyFlowHandler() - flow.hass = hass + assert await setup.async_setup_component( + hass, + DOMAIN, + { + DOMAIN: { + CONF_CLIENT_ID: CLIENT_ID_VALUE, + CONF_CLIENT_SECRET: CLIENT_SECRET_VALUE, + } + }, + ) - with patch.object( - mock_impl, "async_generate_authorize_url", side_effect=asyncio.TimeoutError + with patch( + "homeassistant.components.somfy.config_entry_oauth2_flow." + "LocalOAuth2Implementation.async_generate_authorize_url", + side_effect=asyncio.TimeoutError, ): - result = await flow.async_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_ABORT assert result["reason"] == "authorize_url_timeout" diff --git a/tests/components/somfy_mylink/test_config_flow.py b/tests/components/somfy_mylink/test_config_flow.py index 59f6bd37407..d74b4392f31 100644 --- a/tests/components/somfy_mylink/test_config_flow.py +++ b/tests/components/somfy_mylink/test_config_flow.py @@ -7,9 +7,6 @@ import pytest from homeassistant import config_entries, data_entry_flow, setup from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.somfy_mylink.const import ( - CONF_DEFAULT_REVERSE, - CONF_ENTITY_CONFIG, - CONF_REVERSE, CONF_REVERSED_TARGET_IDS, CONF_SYSTEM_ID, DOMAIN, @@ -32,8 +29,6 @@ async def test_form_user(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -54,7 +49,6 @@ async def test_form_user(hass): CONF_PORT: 1234, CONF_SYSTEM_ID: "456", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -77,8 +71,6 @@ async def test_form_user_already_configured(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -93,116 +85,6 @@ async def test_form_user_already_configured(hass): await hass.async_block_till_done() assert result2["type"] == "abort" - assert len(mock_setup.mock_calls) == 0 - assert len(mock_setup_entry.mock_calls) == 0 - - -async def test_form_import(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "MyLink 1.1.1.1" - assert result["data"] == { - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_with_entity_config(hass): - """Test we can import entity config.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - CONF_DEFAULT_REVERSE: True, - CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "create_entry" - assert result["title"] == "MyLink 1.1.1.1" - assert result["data"] == { - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: 456, - CONF_DEFAULT_REVERSE: True, - CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, - } - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_form_import_already_exists(hass): - """Test we get the form with import source.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - config_entry = MockConfigEntry( - domain=DOMAIN, - data={CONF_HOST: "1.1.1.1", CONF_PORT: 12, CONF_SYSTEM_ID: 46}, - ) - config_entry.add_to_hass(hass) - - with patch( - "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", - return_value={"any": "data"}, - ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.somfy_mylink.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: "456", - }, - ) - await hass.async_block_till_done() - - assert result["type"] == "abort" - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -354,77 +236,6 @@ async def test_options_with_targets(hass, reversed): await hass.async_block_till_done() -@pytest.mark.parametrize("reversed", [True, False]) -async def test_form_import_with_entity_config_modify_options(hass, reversed): - """Test we can import entity config and modify options.""" - await setup.async_setup_component(hass, "persistent_notification", {}) - - mock_imported_config_entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "1.1.1.1", - CONF_PORT: 1234, - CONF_SYSTEM_ID: "456", - CONF_DEFAULT_REVERSE: True, - CONF_ENTITY_CONFIG: {"cover.xyz": {CONF_REVERSE: False}}, - }, - ) - mock_imported_config_entry.add_to_hass(hass) - - mock_status_info = { - "result": [ - {"targetID": "1.1", "name": "xyz"}, - {"targetID": "1.2", "name": "zulu"}, - ] - } - - with patch( - "homeassistant.components.somfy_mylink.SomfyMyLinkSynergy.status_info", - return_value=mock_status_info, - ): - assert await hass.config_entries.async_setup( - mock_imported_config_entry.entry_id - ) - await hass.async_block_till_done() - - assert mock_imported_config_entry.options == { - "reversed_target_ids": {"1.2": True} - } - - result = await hass.config_entries.options.async_init( - mock_imported_config_entry.entry_id - ) - await hass.async_block_till_done() - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result["step_id"] == "init" - - result2 = await hass.config_entries.options.async_configure( - result["flow_id"], - user_input={"target_id": "1.2"}, - ) - - assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - result3 = await hass.config_entries.options.async_configure( - result2["flow_id"], - user_input={"reverse": reversed}, - ) - - assert result3["type"] == data_entry_flow.RESULT_TYPE_FORM - - result4 = await hass.config_entries.options.async_configure( - result3["flow_id"], - user_input={"target_id": None}, - ) - assert result4["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - - # Will not be altered if nothing changes - assert mock_imported_config_entry.options == { - CONF_REVERSED_TARGET_IDS: {"1.2": reversed}, - } - - await hass.async_block_till_done() - - async def test_form_user_already_configured_from_dhcp(hass): """Test we abort if already configured from dhcp.""" await setup.async_setup_component(hass, "persistent_notification", {}) @@ -439,8 +250,6 @@ async def test_form_user_already_configured_from_dhcp(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -457,7 +266,6 @@ async def test_form_user_already_configured_from_dhcp(hass): await hass.async_block_till_done() assert result["type"] == "abort" - assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 @@ -501,8 +309,6 @@ async def test_dhcp_discovery(hass): "homeassistant.components.somfy_mylink.config_flow.SomfyMyLinkSynergy.status_info", return_value={"any": "data"}, ), patch( - "homeassistant.components.somfy_mylink.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.somfy_mylink.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -523,5 +329,4 @@ async def test_dhcp_discovery(hass): CONF_PORT: 1234, CONF_SYSTEM_ID: "456", } - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 0e9c253f1b8..6dfce4ee2fe 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -2,13 +2,7 @@ from unittest.mock import patch from homeassistant.components.sonarr.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, - SOURCE_REAUTH, -) +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_SOURCE from homeassistant.core import HomeAssistant @@ -21,7 +15,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the configuration entry not ready.""" entry = await setup_integration(hass, aioclient_mock, connection_error=True) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_config_entry_reauth( @@ -31,7 +25,7 @@ async def test_config_entry_reauth( with patch.object(hass.config_entries.flow, "async_init") as mock_flow_init: entry = await setup_integration(hass, aioclient_mock, invalid_auth=True) - assert entry.state == ENTRY_STATE_SETUP_ERROR + assert entry.state is ConfigEntryState.SETUP_ERROR mock_flow_init.assert_called_once_with( DOMAIN, @@ -56,10 +50,10 @@ async def test_unload_config_entry( assert hass.data[DOMAIN] assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/sonarr/test_sensor.py b/tests/components/sonarr/test_sensor.py index 9824319f3ff..96d350ac6df 100644 --- a/tests/components/sonarr/test_sensor.py +++ b/tests/components/sonarr/test_sensor.py @@ -109,7 +109,6 @@ async def test_disabled_by_default_sensors( """Test the disabled by default sensors.""" await setup_integration(hass, aioclient_mock) registry = er.async_get(hass) - print(registry.entities) state = hass.states.get(entity_id) assert state is None diff --git a/tests/components/sonos/conftest.py b/tests/components/sonos/conftest.py index f22c462f881..62fd3254d60 100644 --- a/tests/components/sonos/conftest.py +++ b/tests/components/sonos/conftest.py @@ -10,6 +10,36 @@ from homeassistant.const import CONF_HOSTS from tests.common import MockConfigEntry +class SonosMockService: + """Mock a Sonos Service used in callbacks.""" + + def __init__(self, service_type): + """Initialize the instance.""" + self.service_type = service_type + self.subscribe = AsyncMock() + + +class SonosMockEvent: + """Mock a sonos Event used in callbacks.""" + + def __init__(self, soco, service, variables): + """Initialize the instance.""" + self.sid = f"{soco.uid}_sub0000000001" + self.seq = "0" + self.timestamp = 1621000000.0 + self.service = service + self.variables = variables + + def increment_variable(self, var_name): + """Increment the value of the var_name key in variables dict attribute. + + Assumes value has a format of :. + """ + base, count = self.variables[var_name].split(":") + newcount = int(count) + 1 + self.variables[var_name] = ":".join([base, str(newcount)]) + + @pytest.fixture(name="config_entry") def config_entry_fixture(): """Create a mock Sonos config entry.""" @@ -17,7 +47,7 @@ def config_entry_fixture(): @pytest.fixture(name="soco") -def soco_fixture(music_library, speaker_info, battery_info, dummy_soco_service): +def soco_fixture(music_library, speaker_info, battery_info, alarm_clock): """Create a mock pysonos SoCo fixture.""" with patch("pysonos.SoCo", autospec=True) as mock, patch( "socket.gethostbyname", return_value="192.168.42.2" @@ -27,17 +57,18 @@ def soco_fixture(music_library, speaker_info, battery_info, dummy_soco_service): mock_soco.play_mode = "NORMAL" mock_soco.music_library = music_library mock_soco.get_speaker_info.return_value = speaker_info - mock_soco.avTransport = dummy_soco_service - mock_soco.renderingControl = dummy_soco_service - mock_soco.zoneGroupTopology = dummy_soco_service - mock_soco.contentDirectory = dummy_soco_service - mock_soco.deviceProperties = dummy_soco_service + mock_soco.avTransport = SonosMockService("AVTransport") + mock_soco.renderingControl = SonosMockService("RenderingControl") + mock_soco.zoneGroupTopology = SonosMockService("ZoneGroupTopology") + mock_soco.contentDirectory = SonosMockService("ContentDirectory") + mock_soco.deviceProperties = SonosMockService("DeviceProperties") + mock_soco.alarmClock = alarm_clock mock_soco.mute = False mock_soco.night_mode = True mock_soco.dialog_mode = True mock_soco.volume = 19 mock_soco.get_battery_info.return_value = battery_info - + mock_soco.all_zones = [mock_soco] yield mock_soco @@ -59,14 +90,6 @@ def config_fixture(): return {DOMAIN: {MP_DOMAIN: {CONF_HOSTS: ["192.168.42.1"]}}} -@pytest.fixture(name="dummy_soco_service") -def dummy_soco_service_fixture(): - """Create dummy_soco_service fixture.""" - service = Mock() - service.subscribe = AsyncMock() - return service - - @pytest.fixture(name="music_library") def music_library_fixture(): """Create music_library fixture.""" @@ -75,6 +98,42 @@ def music_library_fixture(): return music_library +@pytest.fixture(name="alarm_clock") +def alarm_clock_fixture(): + """Create alarmClock fixture.""" + alarm_clock = SonosMockService("AlarmClock") + alarm_clock.ListAlarms = Mock() + alarm_clock.ListAlarms.return_value = { + "CurrentAlarmList": "" + '' + " " + } + return alarm_clock + + +@pytest.fixture(name="alarm_clock_extended") +def alarm_clock_fixture_extended(): + """Create alarmClock fixture.""" + alarm_clock = SonosMockService("AlarmClock") + alarm_clock.ListAlarms = Mock() + alarm_clock.ListAlarms.return_value = { + "CurrentAlarmList": "" + '' + '' + " " + } + return alarm_clock + + @pytest.fixture(name="speaker_info") def speaker_info_fixture(): """Create speaker_info fixture.""" @@ -83,6 +142,7 @@ def speaker_info_fixture(): "model_name": "Model Name", "software_version": "49.2-64250", "mac_address": "00-11-22-33-44-55", + "display_version": "13.1", } @@ -95,3 +155,29 @@ def battery_info_fixture(): "Temperature": "NORMAL", "PowerSource": "SONOS_CHARGING_RING", } + + +@pytest.fixture(name="battery_event") +def battery_event_fixture(soco): + """Create battery_event fixture.""" + variables = { + "zone_name": "Zone A", + "more_info": "BattChg:NOT_CHARGING,RawBattPct:100,BattPct:100,BattTmp:25", + } + return SonosMockEvent(soco, soco.deviceProperties, variables) + + +@pytest.fixture(name="alarm_event") +def alarm_event_fixture(soco): + """Create alarm_event fixture.""" + variables = { + "time_zone": "ffc40a000503000003000502ffc4", + "time_server": "0.sonostime.pool.ntp.org,1.sonostime.pool.ntp.org,2.sonostime.pool.ntp.org,3.sonostime.pool.ntp.org", + "time_generation": "20000001", + "alarm_list_version": "RINCON_test:1", + "time_format": "INV", + "date_format": "INV", + "daily_index_refresh_time": None, + } + + return SonosMockEvent(soco, soco.alarmClock, variables) diff --git a/tests/components/sonos/test_media_player.py b/tests/components/sonos/test_media_player.py index af8437cd417..cba6967463a 100644 --- a/tests/components/sonos/test_media_player.py +++ b/tests/components/sonos/test_media_player.py @@ -1,7 +1,7 @@ """Tests for the Sonos Media Player platform.""" import pytest -from homeassistant.components.sonos import DOMAIN, media_player +from homeassistant.components.sonos import DATA_SONOS, DOMAIN, media_player from homeassistant.const import STATE_IDLE from homeassistant.core import Context from homeassistant.exceptions import Unauthorized @@ -20,18 +20,24 @@ async def test_async_setup_entry_hosts(hass, config_entry, config, soco): """Test static setup.""" await setup_platform(hass, config_entry, config) - entities = list(hass.data[media_player.DATA_SONOS].media_player_entities.values()) - entity = entities[0] - assert entity.soco == soco + speakers = list(hass.data[DATA_SONOS].discovered.values()) + speaker = speakers[0] + assert speaker.soco == soco + + media_player = hass.states.get("media_player.zone_a") + assert media_player.state == STATE_IDLE async def test_async_setup_entry_discover(hass, config_entry, discover): """Test discovery setup.""" await setup_platform(hass, config_entry, {}) - entities = list(hass.data[media_player.DATA_SONOS].media_player_entities.values()) - entity = entities[0] - assert entity.unique_id == "RINCON_test" + speakers = list(hass.data[DATA_SONOS].discovered.values()) + speaker = speakers[0] + assert speaker.soco.uid == "RINCON_test" + + media_player = hass.states.get("media_player.zone_a") + assert media_player.state == STATE_IDLE async def test_services(hass, config_entry, config, hass_read_only_user): @@ -57,8 +63,8 @@ async def test_device_registry(hass, config_entry, config, soco): identifiers={("sonos", "RINCON_test")} ) assert reg_device.model == "Model Name" - assert reg_device.sw_version == "49.2-64250" - assert reg_device.connections == {("mac", "00:11:22:33:44:55")} + assert reg_device.sw_version == "13.1" + assert reg_device.connections == {(dr.CONNECTION_NETWORK_MAC, "00:11:22:33:44:55")} assert reg_device.manufacturer == "Sonos" assert reg_device.suggested_area == "Zone A" assert reg_device.name == "Zone A" diff --git a/tests/components/sonos/test_sensor.py b/tests/components/sonos/test_sensor.py index 42bf6eedb9c..c8910b481f3 100644 --- a/tests/components/sonos/test_sensor.py +++ b/tests/components/sonos/test_sensor.py @@ -3,7 +3,7 @@ from pysonos.exceptions import NotSupportedException from homeassistant.components.sonos import DOMAIN from homeassistant.components.sonos.binary_sensor import ATTR_BATTERY_POWER_SOURCE -from homeassistant.const import STATE_ON +from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE from homeassistant.setup import async_setup_component @@ -55,3 +55,34 @@ async def test_battery_attributes(hass, config_entry, config, soco): assert ( power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "SONOS_CHARGING_RING" ) + + +async def test_battery_on_S1(hass, config_entry, config, soco, battery_event): + """Test battery state updates on a Sonos S1 device.""" + soco.get_battery_info.return_value = {} + + await setup_platform(hass, config_entry, config) + + subscription = soco.deviceProperties.subscribe.return_value + sub_callback = subscription.callback + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + battery = entity_registry.entities["sensor.zone_a_battery"] + battery_state = hass.states.get(battery.entity_id) + assert battery_state.state == STATE_UNAVAILABLE + + power = entity_registry.entities["binary_sensor.zone_a_power"] + power_state = hass.states.get(power.entity_id) + assert power_state.state == STATE_UNAVAILABLE + + # Update the speaker with a callback event + sub_callback(battery_event) + await hass.async_block_till_done() + + battery_state = hass.states.get(battery.entity_id) + assert battery_state.state == "100" + + power_state = hass.states.get(power.entity_id) + assert power_state.state == STATE_OFF + assert power_state.attributes.get(ATTR_BATTERY_POWER_SOURCE) == "BATTERY" diff --git a/tests/components/sonos/test_switch.py b/tests/components/sonos/test_switch.py new file mode 100644 index 00000000000..41cb241d377 --- /dev/null +++ b/tests/components/sonos/test_switch.py @@ -0,0 +1,77 @@ +"""Tests for the Sonos Alarm switch platform.""" +from homeassistant.components.sonos import DOMAIN +from homeassistant.components.sonos.switch import ( + ATTR_DURATION, + ATTR_ID, + ATTR_INCLUDE_LINKED_ZONES, + ATTR_PLAY_MODE, + ATTR_RECURRENCE, + ATTR_VOLUME, +) +from homeassistant.const import ATTR_TIME, STATE_ON +from homeassistant.helpers.entity_registry import async_get as async_get_entity_registry +from homeassistant.setup import async_setup_component + + +async def setup_platform(hass, config_entry, config): + """Set up the switch platform for testing.""" + config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + +async def test_entity_registry(hass, config_entry, config): + """Test sonos device with alarm registered in the device registry.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + assert "media_player.zone_a" in entity_registry.entities + assert "switch.sonos_alarm_14" in entity_registry.entities + + +async def test_alarm_attributes(hass, config_entry, config): + """Test for correct sonos alarm state.""" + await setup_platform(hass, config_entry, config) + + entity_registry = await hass.helpers.entity_registry.async_get_registry() + + alarm = entity_registry.entities["switch.sonos_alarm_14"] + alarm_state = hass.states.get(alarm.entity_id) + assert alarm_state.state == STATE_ON + assert alarm_state.attributes.get(ATTR_TIME) == "07:00:00" + assert alarm_state.attributes.get(ATTR_ID) == "14" + assert alarm_state.attributes.get(ATTR_DURATION) == "02:00:00" + assert alarm_state.attributes.get(ATTR_RECURRENCE) == "DAILY" + assert alarm_state.attributes.get(ATTR_VOLUME) == 0.25 + assert alarm_state.attributes.get(ATTR_PLAY_MODE) == "SHUFFLE_NOREPEAT" + assert not alarm_state.attributes.get(ATTR_INCLUDE_LINKED_ZONES) + + +async def test_alarm_create_delete( + hass, config_entry, config, soco, alarm_clock, alarm_clock_extended, alarm_event +): + """Test for correct creation and deletion of alarms during runtime.""" + soco.alarmClock = alarm_clock_extended + + await setup_platform(hass, config_entry, config) + + subscription = alarm_clock_extended.subscribe.return_value + sub_callback = subscription.callback + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + entity_registry = async_get_entity_registry(hass) + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" in entity_registry.entities + + alarm_clock_extended.ListAlarms.return_value = alarm_clock.ListAlarms.return_value + alarm_event.increment_variable("alarm_list_version") + + sub_callback(event=alarm_event) + await hass.async_block_till_done() + + assert "switch.sonos_alarm_14" in entity_registry.entities + assert "switch.sonos_alarm_15" not in entity_registry.entities diff --git a/tests/components/speedtestdotnet/test_init.py b/tests/components/speedtestdotnet/test_init.py index 72bcb743a8d..30d3d2a1d63 100644 --- a/tests/components/speedtestdotnet/test_init.py +++ b/tests/components/speedtestdotnet/test_init.py @@ -38,7 +38,7 @@ async def test_successful_config_entry(hass): ) as forward_entry_setup: await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert forward_entry_setup.mock_calls[0][1] == ( entry, "sensor", @@ -58,7 +58,7 @@ async def test_setup_failed(hass): await hass.config_entries.async_setup(entry.entry_id) - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY async def test_unload_entry(hass): @@ -75,5 +75,5 @@ async def test_unload_entry(hass): assert await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert speedtestdotnet.DOMAIN not in hass.data diff --git a/tests/components/squeezebox/test_config_flow.py b/tests/components/squeezebox/test_config_flow.py index cd734e1627c..e740ea671cd 100644 --- a/tests/components/squeezebox/test_config_flow.py +++ b/tests/components/squeezebox/test_config_flow.py @@ -4,6 +4,7 @@ from unittest.mock import patch from pysqueezebox import Server from homeassistant import config_entries +from homeassistant.components.dhcp import HOSTNAME, IP_ADDRESS, MAC_ADDRESS from homeassistant.components.squeezebox.const import DOMAIN from homeassistant.const import ( CONF_HOST, @@ -196,6 +197,41 @@ async def test_discovery_no_uuid(hass): assert result["step_id"] == "edit" +async def test_dhcp_discovery(hass): + """Test we can process discovery from dhcp.""" + with patch( + "pysqueezebox.Server.async_query", + return_value={"uuid": UUID}, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "edit" + + +async def test_dhcp_discovery_no_connection(hass): + """Test we can process discovery from dhcp without connecting to squeezebox server.""" + with patch("pysqueezebox.Server.async_query", new=patch_async_query_unauthorized): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={ + IP_ADDRESS: "1.1.1.1", + MAC_ADDRESS: "AA:BB:CC:DD:EE:FF", + HOSTNAME: "any", + }, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "edit" + + async def test_import(hass): """Test handling of configuration imported.""" with patch("pysqueezebox.Server.async_query", return_value={"uuid": UUID},), patch( diff --git a/tests/components/srp_energy/__init__.py b/tests/components/srp_energy/__init__.py index e14f9186f44..c3966e940b0 100644 --- a/tests/components/srp_energy/__init__.py +++ b/tests/components/srp_energy/__init__.py @@ -38,7 +38,6 @@ async def init_integration( domain=srp_energy.SRP_ENERGY_DOMAIN, source=source, data=config, - connection_class=config_entries.CONN_CLASS_CLOUD_POLL, options=options, entry_id=entry_id, ) diff --git a/tests/components/srp_energy/test_init.py b/tests/components/srp_energy/test_init.py index 8e758d05114..8c8d87674fe 100644 --- a/tests/components/srp_energy/test_init.py +++ b/tests/components/srp_energy/test_init.py @@ -1,4 +1,5 @@ """Tests for Srp Energy component Init.""" +from homeassistant import config_entries from homeassistant.components import srp_energy from tests.components.srp_energy import init_integration @@ -7,7 +8,7 @@ from tests.components.srp_energy import init_integration async def test_setup_entry(hass): """Test setup entry fails if deCONZ is not available.""" config_entry = await init_integration(hass) - assert config_entry.state == "loaded" + assert config_entry.state == config_entries.ConfigEntryState.LOADED assert hass.data[srp_energy.SRP_ENERGY_DOMAIN] diff --git a/tests/components/stream/test_hls.py b/tests/components/stream/test_hls.py index ab0c21efdfb..f9b96a662d9 100644 --- a/tests/components/stream/test_hls.py +++ b/tests/components/stream/test_hls.py @@ -1,6 +1,5 @@ """The tests for hls streams.""" from datetime import timedelta -import io from unittest.mock import patch from urllib.parse import urlparse @@ -18,7 +17,8 @@ from tests.common import async_fire_time_changed from tests.components.stream.common import generate_h264_video STREAM_SOURCE = "some-stream-source" -SEQUENCE_BYTES = io.BytesIO(b"some-bytes") +INIT_BYTES = b"init" +MOOF_BYTES = b"some-bytes" DURATION = 10 TEST_TIMEOUT = 5.0 # Lower than 9s home assistant timeout MAX_ABORT_SEGMENTS = 20 # Abort test to avoid looping forever @@ -248,7 +248,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): stream_worker_sync.pause() hls = stream.add_provider("hls") - hls.put(Segment(1, SEQUENCE_BYTES, DURATION)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -257,7 +257,7 @@ async def test_hls_playlist_view(hass, hls_stream, stream_worker_sync): assert resp.status == 200 assert await resp.text() == make_playlist(sequence=1, segments=[make_segment(1)]) - hls.put(Segment(2, SEQUENCE_BYTES, DURATION)) + hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") assert resp.status == 200 @@ -281,7 +281,7 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): # Produce enough segments to overfill the output buffer by one for sequence in range(1, MAX_SEGMENTS + 2): - hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION)) + hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") @@ -297,18 +297,14 @@ async def test_hls_max_segments(hass, hls_stream, stream_worker_sync): segments=segments, ) - # Fetch the actual segments with a fake byte payload - with patch( - "homeassistant.components.stream.hls.get_m4s", return_value=b"fake-payload" - ): - # The segment that fell off the buffer is not accessible - segment_response = await hls_client.get("/segment/1.m4s") - assert segment_response.status == 404 + # The segment that fell off the buffer is not accessible + segment_response = await hls_client.get("/segment/1.m4s") + assert segment_response.status == 404 - # However all segments in the buffer are accessible, even those that were not in the playlist. - for sequence in range(2, MAX_SEGMENTS + 2): - segment_response = await hls_client.get(f"/segment/{sequence}.m4s") - assert segment_response.status == 200 + # However all segments in the buffer are accessible, even those that were not in the playlist. + for sequence in range(2, MAX_SEGMENTS + 2): + segment_response = await hls_client.get(f"/segment/{sequence}.m4s") + assert segment_response.status == 200 stream_worker_sync.resume() stream.stop() @@ -322,9 +318,9 @@ async def test_hls_playlist_view_discontinuity(hass, hls_stream, stream_worker_s stream_worker_sync.pause() hls = stream.add_provider("hls") - hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) - hls.put(Segment(2, SEQUENCE_BYTES, DURATION, stream_id=0)) - hls.put(Segment(3, SEQUENCE_BYTES, DURATION, stream_id=1)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) + hls.put(Segment(2, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) + hls.put(Segment(3, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1)) await hass.async_block_till_done() hls_client = await hls_stream(stream) @@ -354,11 +350,11 @@ async def test_hls_max_segments_discontinuity(hass, hls_stream, stream_worker_sy hls_client = await hls_stream(stream) - hls.put(Segment(1, SEQUENCE_BYTES, DURATION, stream_id=0)) + hls.put(Segment(1, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=0)) # Produce enough segments to overfill the output buffer by one for sequence in range(1, MAX_SEGMENTS + 2): - hls.put(Segment(sequence, SEQUENCE_BYTES, DURATION, stream_id=1)) + hls.put(Segment(sequence, INIT_BYTES, MOOF_BYTES, DURATION, stream_id=1)) await hass.async_block_till_done() resp = await hls_client.get("/playlist.m3u8") diff --git a/tests/components/stream/test_recorder.py b/tests/components/stream/test_recorder.py index 5ee055754b9..9097d03a7a9 100644 --- a/tests/components/stream/test_recorder.py +++ b/tests/components/stream/test_recorder.py @@ -1,10 +1,13 @@ """The tests for hls streams.""" +from __future__ import annotations + import asyncio +from collections import deque from datetime import timedelta +from io import BytesIO import logging import os import threading -from typing import Deque from unittest.mock import patch import async_timeout @@ -13,6 +16,7 @@ import pytest from homeassistant.components.stream import create_stream from homeassistant.components.stream.core import Segment +from homeassistant.components.stream.fmp4utils import get_init_and_moof_data from homeassistant.components.stream.recorder import recorder_save_worker from homeassistant.exceptions import HomeAssistantError from homeassistant.setup import async_setup_component @@ -37,8 +41,9 @@ class SaveRecordWorkerSync: """Initialize SaveRecordWorkerSync.""" self.reset() self._segments = None + self._save_thread = None - def recorder_save_worker(self, file_out: str, segments: Deque[Segment]): + def recorder_save_worker(self, file_out: str, segments: deque[Segment]): """Mock method for patch.""" logging.debug("recorder_save_worker thread started") assert self._save_thread is None @@ -180,7 +185,9 @@ async def test_recorder_save(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker(filename, [Segment(1, source, 4)]) + recorder_save_worker( + filename, [Segment(1, *get_init_and_moof_data(source.getbuffer()), 4)] + ) # Assert assert os.path.exists(filename) @@ -193,13 +200,20 @@ async def test_recorder_discontinuity(tmpdir): filename = f"{tmpdir}/test.mp4" # Run - recorder_save_worker(filename, [Segment(1, source, 4, 0), Segment(2, source, 4, 1)]) + init, moof_data = get_init_and_moof_data(source.getbuffer()) + recorder_save_worker( + filename, + [ + Segment(1, init, moof_data, 4, 0), + Segment(2, init, moof_data, 4, 1), + ], + ) # Assert assert os.path.exists(filename) -async def test_recorder_no_segements(tmpdir): +async def test_recorder_no_segments(tmpdir): """Test recorder behavior with a stream failure which causes no segments.""" # Setup filename = f"{tmpdir}/test.mp4" @@ -247,7 +261,9 @@ async def test_record_stream_audio( last_segment = segment stream_worker_sync.resume() - result = av.open(last_segment.segment, "r", format="mp4") + result = av.open( + BytesIO(last_segment.init + last_segment.moof_data), "r", format="mp4" + ) assert len(result.streams.audio) == expected_audio_streams result.close() diff --git a/tests/components/subaru/conftest.py b/tests/components/subaru/conftest.py index 1b8d1439e68..1ca7926cea2 100644 --- a/tests/components/subaru/conftest.py +++ b/tests/components/subaru/conftest.py @@ -18,7 +18,7 @@ from homeassistant.components.subaru.const import ( VEHICLE_HAS_SAFETY_SERVICE, VEHICLE_NAME, ) -from homeassistant.config_entries import ENTRY_STATE_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import CONF_DEVICE_ID, CONF_PASSWORD, CONF_PIN, CONF_USERNAME from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util @@ -144,5 +144,5 @@ async def ev_entry(hass): assert DOMAIN in hass.config_entries.async_domains() assert len(hass.config_entries.async_entries(DOMAIN)) == 1 assert hass.config_entries.async_get_entry(entry.entry_id) - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED return entry diff --git a/tests/components/subaru/test_config_flow.py b/tests/components/subaru/test_config_flow.py index 031b9c29d09..aed15150619 100644 --- a/tests/components/subaru/test_config_flow.py +++ b/tests/components/subaru/test_config_flow.py @@ -115,6 +115,7 @@ async def test_user_form_pin_not_required(hass, user_form): "type": "create_entry", "version": 1, "data": deepcopy(TEST_CONFIG), + "options": {}, } expected["data"][CONF_PIN] = None result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID @@ -176,6 +177,7 @@ async def test_pin_form_success(hass, pin_form): "type": "create_entry", "version": 1, "data": TEST_CONFIG, + "options": {}, } result["data"][CONF_DEVICE_ID] = TEST_DEVICE_ID assert result == expected diff --git a/tests/components/subaru/test_init.py b/tests/components/subaru/test_init.py index 13b510e8c40..cd87ed40315 100644 --- a/tests/components/subaru/test_init.py +++ b/tests/components/subaru/test_init.py @@ -8,12 +8,7 @@ from homeassistant.components.homeassistant import ( SERVICE_UPDATE_ENTITY, ) from homeassistant.components.subaru.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_ERROR, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.const import ATTR_ENTITY_ID from homeassistant.setup import async_setup_component @@ -44,7 +39,7 @@ async def test_setup_ev(hass, ev_entry): """Test setup with an EV vehicle.""" check_entry = hass.config_entries.async_get_entry(ev_entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_LOADED + assert check_entry.state is ConfigEntryState.LOADED async def test_setup_g2(hass): @@ -57,7 +52,7 @@ async def test_setup_g2(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_LOADED + assert check_entry.state is ConfigEntryState.LOADED async def test_setup_g1(hass): @@ -67,7 +62,7 @@ async def test_setup_g1(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_LOADED + assert check_entry.state is ConfigEntryState.LOADED async def test_unsuccessful_connect(hass): @@ -81,7 +76,7 @@ async def test_unsuccessful_connect(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_SETUP_RETRY + assert check_entry.state is ConfigEntryState.SETUP_RETRY async def test_invalid_credentials(hass): @@ -95,7 +90,7 @@ async def test_invalid_credentials(hass): ) check_entry = hass.config_entries.async_get_entry(entry.entry_id) assert check_entry - assert check_entry.state == ENTRY_STATE_SETUP_ERROR + assert check_entry.state is ConfigEntryState.SETUP_ERROR async def test_update_skip_unsubscribed(hass): @@ -147,7 +142,7 @@ async def test_fetch_failed(hass): async def test_unload_entry(hass, ev_entry): """Test that entry is unloaded.""" - assert ev_entry.state == ENTRY_STATE_LOADED + assert ev_entry.state is ConfigEntryState.LOADED assert await hass.config_entries.async_unload(ev_entry.entry_id) await hass.async_block_till_done() - assert ev_entry.state == ENTRY_STATE_NOT_LOADED + assert ev_entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/sun/test_trigger.py b/tests/components/sun/test_trigger.py index 6e6a1f11ef9..f100fb53dc8 100644 --- a/tests/components/sun/test_trigger.py +++ b/tests/components/sun/test_trigger.py @@ -33,7 +33,7 @@ def calls(hass): def setup_comp(hass): """Initialize components.""" mock_component(hass, "group") - dt_util.set_default_time_zone(hass.config.time_zone) + hass.config.set_time_zone(hass.config.time_zone) hass.loop.run_until_complete( async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) ) diff --git a/tests/components/switch/test_device_action.py b/tests/components/switch/test_device_action.py index 2a98cd2fad4..9f8d821e74b 100644 --- a/tests/components/switch/test_device_action.py +++ b/tests/components/switch/test_device_action.py @@ -70,7 +70,7 @@ async def test_get_actions(hass, device_reg, entity_reg): assert actions == expected_actions -async def test_action(hass, calls): +async def test_action(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off actions.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/switch/test_device_condition.py b/tests/components/switch/test_device_condition.py index 9273610dee9..e2102976f8d 100644 --- a/tests/components/switch/test_device_condition.py +++ b/tests/components/switch/test_device_condition.py @@ -91,7 +91,7 @@ async def test_get_condition_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_state(hass, calls): +async def test_if_state(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off conditions.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -165,7 +165,7 @@ async def test_if_state(hass, calls): assert calls[1].data["some"] == "is_off event - test_event2" -async def test_if_fires_on_for_condition(hass, calls): +async def test_if_fires_on_for_condition(hass, calls, enable_custom_integrations): """Test for firing if condition is on with delay.""" point1 = dt_util.utcnow() point2 = point1 + timedelta(seconds=10) diff --git a/tests/components/switch/test_device_trigger.py b/tests/components/switch/test_device_trigger.py index d958dd21911..e871bf6f645 100644 --- a/tests/components/switch/test_device_trigger.py +++ b/tests/components/switch/test_device_trigger.py @@ -91,7 +91,7 @@ async def test_get_trigger_capabilities(hass, device_reg, entity_reg): assert capabilities == expected_capabilities -async def test_if_fires_on_state_change(hass, calls): +async def test_if_fires_on_state_change(hass, calls, enable_custom_integrations): """Test for turn_on and turn_off triggers firing.""" platform = getattr(hass.components, f"test.{DOMAIN}") @@ -176,7 +176,9 @@ async def test_if_fires_on_state_change(hass, calls): ) -async def test_if_fires_on_state_change_with_for(hass, calls): +async def test_if_fires_on_state_change_with_for( + hass, calls, enable_custom_integrations +): """Test for triggers firing with delay.""" platform = getattr(hass.components, f"test.{DOMAIN}") diff --git a/tests/components/switch/test_init.py b/tests/components/switch/test_init.py index cf2933282ea..44302ec311b 100644 --- a/tests/components/switch/test_init.py +++ b/tests/components/switch/test_init.py @@ -17,7 +17,7 @@ def entities(hass): yield platform.ENTITIES -async def test_methods(hass, entities): +async def test_methods(hass, entities, enable_custom_integrations): """Test is_on, turn_on, turn_off methods.""" switch_1, switch_2, switch_3 = entities assert await async_setup_component( @@ -49,7 +49,9 @@ async def test_methods(hass, entities): assert switch.is_on(hass, switch_3.entity_id) -async def test_switch_context(hass, entities, hass_admin_user): +async def test_switch_context( + hass, entities, hass_admin_user, enable_custom_integrations +): """Test that switch context works.""" assert await async_setup_component(hass, "switch", {"switch": {"platform": "test"}}) diff --git a/tests/components/syncthing/__init__.py b/tests/components/syncthing/__init__.py new file mode 100644 index 00000000000..8a4f28832ea --- /dev/null +++ b/tests/components/syncthing/__init__.py @@ -0,0 +1 @@ +"""Tests for the syncthing integration.""" diff --git a/tests/components/syncthing/test_config_flow.py b/tests/components/syncthing/test_config_flow.py new file mode 100644 index 00000000000..30f8bc0386b --- /dev/null +++ b/tests/components/syncthing/test_config_flow.py @@ -0,0 +1,106 @@ +"""Tests for syncthing config flow.""" + +from unittest.mock import patch + +from aiosyncthing.exceptions import UnauthorizedError + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.syncthing.const import DOMAIN +from homeassistant.const import CONF_NAME, CONF_TOKEN, CONF_URL, CONF_VERIFY_SSL + +from tests.common import MockConfigEntry + +NAME = "Syncthing" +URL = "http://127.0.0.1:8384" +TOKEN = "token" +VERIFY_SSL = True + +MOCK_ENTRY = { + CONF_NAME: NAME, + CONF_URL: URL, + CONF_TOKEN: TOKEN, + CONF_VERIFY_SSL: VERIFY_SSL, +} + + +async def test_show_setup_form(hass): + """Test that the setup form is served.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] == {} + assert result["step_id"] == "user" + + +async def test_flow_successfull(hass): + """Test with required fields only.""" + with patch( + "aiosyncthing.system.System.status", return_value={"myID": "server-id"} + ), patch( + "homeassistant.components.syncthing.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data={ + CONF_NAME: NAME, + CONF_URL: URL, + CONF_TOKEN: TOKEN, + CONF_VERIFY_SSL: VERIFY_SSL, + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result["title"] == "http://127.0.0.1:8384" + assert result["data"][CONF_NAME] == NAME + assert result["data"][CONF_URL] == URL + assert result["data"][CONF_TOKEN] == TOKEN + assert result["data"][CONF_VERIFY_SSL] == VERIFY_SSL + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_flow_already_configured(hass): + """Test name is already configured.""" + + entry = MockConfigEntry(domain=DOMAIN, data=MOCK_ENTRY, unique_id="server-id") + entry.add_to_hass(hass) + + with patch("aiosyncthing.system.System.status", return_value={"myID": "server-id"}): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data=MOCK_ENTRY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_flow_invalid_auth(hass): + """Test invalid auth.""" + + with patch("aiosyncthing.system.System.status", side_effect=UnauthorizedError): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data=MOCK_ENTRY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"]["token"] == "invalid_auth" + + +async def test_flow_cannot_connect(hass): + """Test cannot connect.""" + + with patch("aiosyncthing.system.System.status", side_effect=Exception): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "user"}, + data=MOCK_ENTRY, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"]["base"] == "cannot_connect" diff --git a/tests/components/system_bridge/__init__.py b/tests/components/system_bridge/__init__.py new file mode 100644 index 00000000000..f049f887584 --- /dev/null +++ b/tests/components/system_bridge/__init__.py @@ -0,0 +1 @@ +"""Tests for the System Bridge integration.""" diff --git a/tests/components/system_bridge/test_config_flow.py b/tests/components/system_bridge/test_config_flow.py new file mode 100644 index 00000000000..5cd7a77d911 --- /dev/null +++ b/tests/components/system_bridge/test_config_flow.py @@ -0,0 +1,409 @@ +"""Test the System Bridge config flow.""" +from unittest.mock import patch + +from aiohttp.client_exceptions import ClientConnectionError +from systembridge.exceptions import BridgeAuthenticationException + +from homeassistant import config_entries, data_entry_flow, setup +from homeassistant.components.system_bridge.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT + +from tests.common import MockConfigEntry + +FIXTURE_MAC_ADDRESS = "aa:bb:cc:dd:ee:ff" +FIXTURE_UUID = "e91bf575-56f3-4c83-8f42-70ac17adcd33" + +FIXTURE_AUTH_INPUT = {CONF_API_KEY: "abc-123-def-456-ghi"} + +FIXTURE_USER_INPUT = { + CONF_API_KEY: "abc-123-def-456-ghi", + CONF_HOST: "test-bridge", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF_INPUT = { + CONF_API_KEY: "abc-123-def-456-ghi", + CONF_HOST: "1.1.1.1", + CONF_PORT: "9170", +} + +FIXTURE_ZEROCONF = { + CONF_HOST: "1.1.1.1", + CONF_PORT: 9170, + "hostname": "test-bridge.local.", + "type": "_system-bridge._udp.local.", + "name": "System Bridge - test-bridge._system-bridge._udp.local.", + "properties": { + "address": "http://test-bridge:9170", + "fqdn": "test-bridge", + "host": "test-bridge", + "ip": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + "port": "9170", + "uuid": FIXTURE_UUID, + }, +} + +FIXTURE_ZEROCONF_BAD = { + CONF_HOST: "1.1.1.1", + CONF_PORT: 9170, + "hostname": "test-bridge.local.", + "type": "_system-bridge._udp.local.", + "name": "System Bridge - test-bridge._system-bridge._udp.local.", + "properties": { + "something": "bad", + }, +} + +FIXTURE_OS = { + "platform": "linux", + "distro": "Ubuntu", + "release": "20.10", + "codename": "Groovy Gorilla", + "kernel": "5.8.0-44-generic", + "arch": "x64", + "hostname": "test-bridge", + "fqdn": "test-bridge.local", + "codepage": "UTF-8", + "logofile": "ubuntu", + "serial": "abcdefghijklmnopqrstuvwxyz", + "build": "", + "servicepack": "", + "uefi": True, + "users": [], +} + + +FIXTURE_NETWORK = { + "connections": [], + "gatewayDefault": "192.168.1.1", + "interfaceDefault": "wlp2s0", + "interfaces": { + "wlp2s0": { + "iface": "wlp2s0", + "ifaceName": "wlp2s0", + "ip4": "1.1.1.1", + "mac": FIXTURE_MAC_ADDRESS, + }, + }, + "stats": {}, +} + +FIXTURE_SYSTEM = { + "baseboard": { + "manufacturer": "System manufacturer", + "model": "Model", + "version": "Rev X.0x", + "serial": "1234567", + "assetTag": "", + "memMax": 134217728, + "memSlots": 4, + }, + "bios": { + "vendor": "System vendor", + "version": "12345", + "releaseDate": "2019-11-13", + "revision": "", + }, + "chassis": { + "manufacturer": "Default string", + "model": "", + "type": "Desktop", + "version": "Default string", + "serial": "Default string", + "assetTag": "", + "sku": "", + }, + "system": { + "manufacturer": "System manufacturer", + "model": "System Product Name", + "version": "System Version", + "serial": "System Serial Number", + "uuid": "abc123-def456", + "sku": "SKU", + "virtual": False, + }, + "uuid": { + "os": FIXTURE_UUID, + "hardware": "abc123-def456", + "macs": [FIXTURE_MAC_ADDRESS], + }, +} + + +FIXTURE_BASE_URL = ( + f"http://{FIXTURE_USER_INPUT[CONF_HOST]}:{FIXTURE_USER_INPUT[CONF_PORT]}" +) + +FIXTURE_ZEROCONF_BASE_URL = ( + f"http://{FIXTURE_ZEROCONF[CONF_HOST]}:{FIXTURE_ZEROCONF[CONF_PORT]}" +) + + +async def test_user_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test full user flow.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", json=FIXTURE_OS) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", json=FIXTURE_NETWORK) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", json=FIXTURE_SYSTEM) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test-bridge" + assert result2["data"] == FIXTURE_USER_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_auth( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=BridgeAuthenticationException) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=ClientConnectionError) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_unknow_error( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we handle unknown error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["errors"] is None + + with patch( + "homeassistant.components.system_bridge.config_flow.Bridge.async_get_os", + side_effect=Exception("Boom"), + ): + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_USER_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "user" + assert result2["errors"] == {"base": "unknown"} + + +async def test_reauth_authorization_error( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we show user form on authorization error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=BridgeAuthenticationException) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=BridgeAuthenticationException) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_connection_error( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test we show user form on connection error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", exc=ClientConnectionError) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", exc=ClientConnectionError) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test reauth flow.""" + mock_config = MockConfigEntry( + domain=DOMAIN, unique_id=FIXTURE_UUID, data=FIXTURE_USER_INPUT + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": "reauth"}, data=FIXTURE_USER_INPUT + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "authenticate" + + aioclient_mock.get(f"{FIXTURE_BASE_URL}/os", json=FIXTURE_OS) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/network", json=FIXTURE_NETWORK) + aioclient_mock.get(f"{FIXTURE_BASE_URL}/system", json=FIXTURE_SYSTEM) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_flow( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test zeroconf flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=FIXTURE_ZEROCONF, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert not result["errors"] + + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/os", json=FIXTURE_OS) + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/network", json=FIXTURE_NETWORK) + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/system", json=FIXTURE_SYSTEM) + + with patch( + "homeassistant.components.system_bridge.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result2["title"] == "test-bridge" + assert result2["data"] == FIXTURE_ZEROCONF_INPUT + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_zeroconf_cannot_connect( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test zeroconf cannot connect flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=FIXTURE_ZEROCONF, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert not result["errors"] + + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/os", exc=ClientConnectionError) + aioclient_mock.get( + f"{FIXTURE_ZEROCONF_BASE_URL}/network", exc=ClientConnectionError + ) + aioclient_mock.get(f"{FIXTURE_ZEROCONF_BASE_URL}/system", exc=ClientConnectionError) + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], FIXTURE_AUTH_INPUT + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result2["step_id"] == "authenticate" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_zeroconf_bad_zeroconf_info( + hass, aiohttp_client, aioclient_mock, current_request_with_host +) -> None: + """Test zeroconf cannot connect flow.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=FIXTURE_ZEROCONF_BAD, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "unknown" diff --git a/tests/components/tasmota/conftest.py b/tests/components/tasmota/conftest.py index 5c0b7d315f9..aa73e4d9994 100644 --- a/tests/components/tasmota/conftest.py +++ b/tests/components/tasmota/conftest.py @@ -4,7 +4,6 @@ from unittest.mock import patch from hatasmota.discovery import get_status_sensor_entities import pytest -from homeassistant import config_entries from homeassistant.components.tasmota.const import ( CONF_DISCOVERY_PREFIX, DEFAULT_PREFIX, @@ -64,7 +63,6 @@ async def setup_tasmota_helper(hass): hass.config.components.add("tasmota") entry = MockConfigEntry( - connection_class=config_entries.CONN_CLASS_LOCAL_PUSH, data={CONF_DISCOVERY_PREFIX: DEFAULT_PREFIX}, domain=DOMAIN, title="Tasmota", diff --git a/tests/components/tasmota/test_common.py b/tests/components/tasmota/test_common.py index 44f57581694..9174060ef93 100644 --- a/tests/components/tasmota/test_common.py +++ b/tests/components/tasmota/test_common.py @@ -382,7 +382,9 @@ async def help_test_discovery_removal( await hass.async_block_till_done() # Verify device and entity registry entries are created - device_entry = device_reg.async_get_device(set(), {("mac", config1[CONF_MAC])}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config1[CONF_MAC])} + ) assert device_entry is not None entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") assert entity_entry is not None @@ -403,7 +405,9 @@ async def help_test_discovery_removal( await hass.async_block_till_done() # Verify entity registry entries are cleared - device_entry = device_reg.async_get_device(set(), {("mac", config2[CONF_MAC])}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config2[CONF_MAC])} + ) assert device_entry is not None entity_entry = entity_reg.async_get(f"{domain}.{entity_id}") assert entity_entry is None @@ -487,14 +491,18 @@ async def help_test_discovery_device_remove( ) await hass.async_block_till_done() - device = device_reg.async_get_device(set(), {("mac", config[CONF_MAC])}) + device = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config[CONF_MAC])} + ) assert device is not None assert entity_reg.async_get_entity_id(domain, "tasmota", unique_id) async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{config[CONF_MAC]}/config", "") await hass.async_block_till_done() - device = device_reg.async_get_device(set(), {("mac", config[CONF_MAC])}) + device = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, config[CONF_MAC])} + ) assert device is None assert not entity_reg.async_get_entity_id(domain, "tasmota", unique_id) diff --git a/tests/components/tasmota/test_device_trigger.py b/tests/components/tasmota/test_device_trigger.py index 1fa1c629a33..8ef4f7df919 100644 --- a/tests/components/tasmota/test_device_trigger.py +++ b/tests/components/tasmota/test_device_trigger.py @@ -9,6 +9,7 @@ import pytest import homeassistant.components.automation as automation from homeassistant.components.tasmota.const import DEFAULT_PREFIX, DOMAIN from homeassistant.components.tasmota.device_trigger import async_attach_trigger +from homeassistant.helpers import device_registry as dr from homeassistant.setup import async_setup_component from .test_common import DEFAULT_CONFIG @@ -33,7 +34,9 @@ async def test_get_triggers_btn(hass, device_reg, entity_reg, mqtt_mock, setup_t async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) expected_triggers = [ { "platform": "device", @@ -65,7 +68,9 @@ async def test_get_triggers_swc(hass, device_reg, entity_reg, mqtt_mock, setup_t async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) expected_triggers = [ { "platform": "device", @@ -92,7 +97,9 @@ async def test_get_unknown_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -133,7 +140,9 @@ async def test_get_non_existing_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, []) @@ -157,7 +166,9 @@ async def test_discover_bad_triggers( ) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, []) @@ -189,7 +200,9 @@ async def test_discover_bad_triggers( ) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) triggers = await async_get_device_automations(hass, "trigger", device_entry.id) assert_lists_same(triggers, []) @@ -231,7 +244,9 @@ async def test_update_remove_triggers( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) expected_triggers1 = [ { @@ -287,7 +302,9 @@ async def test_if_fires_on_mqtt_message_btn( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -357,7 +374,9 @@ async def test_if_fires_on_mqtt_message_swc( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -453,7 +472,9 @@ async def test_if_fires_on_mqtt_message_late_discover( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -527,7 +548,9 @@ async def test_if_fires_on_mqtt_message_after_update( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -604,7 +627,9 @@ async def test_no_resubscribe_same_topic(hass, device_reg, mqtt_mock, setup_tasm async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -650,7 +675,9 @@ async def test_not_fires_on_mqtt_message_after_remove_by_mqtt( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -719,7 +746,9 @@ async def test_not_fires_on_mqtt_message_after_remove_from_registry( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert await async_setup_component( hass, @@ -774,7 +803,9 @@ async def test_attach_remove(hass, device_reg, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) calls = [] @@ -829,7 +860,9 @@ async def test_attach_remove_late(hass, device_reg, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) calls = [] @@ -894,7 +927,9 @@ async def test_attach_remove_late2(hass, device_reg, mqtt_mock, setup_tasmota): async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) calls = [] @@ -940,7 +975,9 @@ async def test_attach_remove_unknown1(hass, device_reg, mqtt_mock, setup_tasmota async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) remove = await async_attach_trigger( hass, @@ -982,7 +1019,9 @@ async def test_attach_unknown_remove_device_from_registry( async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config1)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) await async_attach_trigger( hass, @@ -1015,7 +1054,9 @@ async def test_attach_remove_config_entry(hass, device_reg, mqtt_mock, setup_tas async_fire_mqtt_message(hass, f"{DEFAULT_PREFIX}/{mac}/config", json.dumps(config)) await hass.async_block_till_done() - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) calls = [] diff --git a/tests/components/tasmota/test_discovery.py b/tests/components/tasmota/test_discovery.py index 35be7e50e62..3e1175faa0f 100644 --- a/tests/components/tasmota/test_discovery.py +++ b/tests/components/tasmota/test_discovery.py @@ -5,6 +5,7 @@ from unittest.mock import patch from homeassistant.components.tasmota.const import DEFAULT_PREFIX from homeassistant.components.tasmota.discovery import ALREADY_DISCOVERED +from homeassistant.helpers import device_registry as dr from .conftest import setup_tasmota_helper from .test_common import DEFAULT_CONFIG, DEFAULT_CONFIG_9_0_0_3 @@ -116,7 +117,9 @@ async def test_correct_config_discovery( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None entity_entry = entity_reg.async_get("switch.test") assert entity_entry is not None @@ -143,7 +146,9 @@ async def test_device_discover( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None assert device_entry.manufacturer == "Tasmota" assert device_entry.model == config["md"] @@ -166,7 +171,9 @@ async def test_device_discover_deprecated( await hass.async_block_till_done() # Verify device and registry entries are created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None assert device_entry.manufacturer == "Tasmota" assert device_entry.model == config["md"] @@ -192,7 +199,9 @@ async def test_device_update( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None # Update device parameters @@ -208,7 +217,9 @@ async def test_device_update( await hass.async_block_till_done() # Verify device entry is updated - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None assert device_entry.model == "Another model" assert device_entry.name == "Another name" @@ -230,7 +241,9 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None async_fire_mqtt_message( @@ -241,7 +254,9 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None @@ -254,18 +269,22 @@ async def test_device_remove_stale(hass, mqtt_mock, caplog, device_reg, setup_ta # Create a device device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", mac)}, + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) # Verify device entry was created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None # Remove the device device_reg.async_remove_device(device_entry.id) # Verify device entry is removed - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None @@ -284,7 +303,9 @@ async def test_device_rediscover( await hass.async_block_till_done() # Verify device entry is created - device_entry1 = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry1 = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry1 is not None async_fire_mqtt_message( @@ -295,7 +316,9 @@ async def test_device_rediscover( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None async_fire_mqtt_message( @@ -306,7 +329,9 @@ async def test_device_rediscover( await hass.async_block_till_done() # Verify device entry is created, and id is reused - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None assert device_entry1.id == device_entry.id diff --git a/tests/components/tasmota/test_init.py b/tests/components/tasmota/test_init.py index 5b553164583..1b9c88ee4b1 100644 --- a/tests/components/tasmota/test_init.py +++ b/tests/components/tasmota/test_init.py @@ -5,6 +5,7 @@ from unittest.mock import call from homeassistant.components import websocket_api from homeassistant.components.tasmota.const import DEFAULT_PREFIX +from homeassistant.helpers import device_registry as dr from .test_common import DEFAULT_CONFIG @@ -22,14 +23,18 @@ async def test_device_remove( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None device_reg.async_remove_device(device_entry.id) await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None # Verify retained discovery topic has been cleared @@ -52,7 +57,7 @@ async def test_device_remove_non_tasmota_device( mac = "12:34:56:AB:CD:EF" device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", mac)}, + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) assert device_entry is not None @@ -60,7 +65,9 @@ async def test_device_remove_non_tasmota_device( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None # Verify no Tasmota discovery message was sent @@ -76,7 +83,7 @@ async def test_device_remove_stale_tasmota_device( mac = "12:34:56:AB:CD:EF" device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", mac)}, + connections={(dr.CONNECTION_NETWORK_MAC, mac)}, ) assert device_entry is not None @@ -84,7 +91,9 @@ async def test_device_remove_stale_tasmota_device( await hass.async_block_till_done() # Verify device entry is removed - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None # Verify retained discovery topic has been cleared @@ -109,7 +118,9 @@ async def test_tasmota_ws_remove_discovered_device( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None client = await hass_ws_client(hass) @@ -120,7 +131,9 @@ async def test_tasmota_ws_remove_discovered_device( assert response["success"] # Verify device entry is cleared - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is None @@ -135,7 +148,9 @@ async def test_tasmota_ws_remove_discovered_device_twice( await hass.async_block_till_done() # Verify device entry is created - device_entry = device_reg.async_get_device(set(), {("mac", mac)}) + device_entry = device_reg.async_get_device( + set(), {(dr.CONNECTION_NETWORK_MAC, mac)} + ) assert device_entry is not None client = await hass_ws_client(hass) @@ -163,7 +178,7 @@ async def test_tasmota_ws_remove_non_tasmota_device( device_entry = device_reg.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) assert device_entry is not None diff --git a/tests/components/tasmota/test_sensor.py b/tests/components/tasmota/test_sensor.py index 425fa3f26f6..e9aa291fe6d 100644 --- a/tests/components/tasmota/test_sensor.py +++ b/tests/components/tasmota/test_sensor.py @@ -233,6 +233,55 @@ async def test_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): assert state.state == "7.8" +async def test_indexed_sensor_state_via_mqtt2(hass, mqtt_mock, setup_tasmota): + """Test state update via MQTT for sensor with last_reset property.""" + config = copy.deepcopy(DEFAULT_CONFIG) + sensor_config = copy.deepcopy(INDEXED_SENSOR_CONFIG) + mac = config["mac"] + + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/config", + json.dumps(config), + ) + await hass.async_block_till_done() + async_fire_mqtt_message( + hass, + f"{DEFAULT_PREFIX}/{mac}/sensors", + json.dumps(sensor_config), + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == "unavailable" + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + async_fire_mqtt_message(hass, "tasmota_49A3BC/tele/LWT", "Online") + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == STATE_UNKNOWN + assert not state.attributes.get(ATTR_ASSUMED_STATE) + + # Test periodic state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/tele/SENSOR", + '{"ENERGY":{"Total":1.2,"TotalStartTime":"2018-11-23T15:33:47"}}', + ) + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == "1.2" + assert state.attributes["last_reset"] == "2018-11-23T15:33:47+00:00" + + # Test polled state update + async_fire_mqtt_message( + hass, + "tasmota_49A3BC/stat/STATUS10", + '{"StatusSNS":{"ENERGY":{"Total":5.6,"TotalStartTime":"2018-11-23T16:33:47"}}}', + ) + state = hass.states.get("sensor.tasmota_energy_total") + assert state.state == "5.6" + assert state.attributes["last_reset"] == "2018-11-23T16:33:47+00:00" + + async def test_bad_indexed_sensor_state_via_mqtt(hass, mqtt_mock, setup_tasmota): """Test state update via MQTT where sensor is not matching configuration.""" config = copy.deepcopy(DEFAULT_CONFIG) diff --git a/tests/components/tcp/test_binary_sensor.py b/tests/components/tcp/test_binary_sensor.py index 21dd84b1892..f8c13b41c30 100644 --- a/tests/components/tcp/test_binary_sensor.py +++ b/tests/components/tcp/test_binary_sensor.py @@ -20,9 +20,9 @@ TEST_ENTITY = "binary_sensor.test_name" def mock_socket_fixture(): """Mock the socket.""" with patch( - "homeassistant.components.tcp.sensor.socket.socket" + "homeassistant.components.tcp.common.socket.socket" ) as mock_socket, patch( - "homeassistant.components.tcp.sensor.select.select", + "homeassistant.components.tcp.common.select.select", return_value=(True, False, False), ): # yield the return value of the socket context manager diff --git a/tests/components/tcp/test_sensor.py b/tests/components/tcp/test_sensor.py index b1efef305bf..46db3367677 100644 --- a/tests/components/tcp/test_sensor.py +++ b/tests/components/tcp/test_sensor.py @@ -4,7 +4,7 @@ from unittest.mock import call, patch import pytest -import homeassistant.components.tcp.sensor as tcp +import homeassistant.components.tcp.common as tcp from homeassistant.setup import async_setup_component from tests.common import assert_setup_component @@ -41,7 +41,7 @@ socket_test_value = "value" @pytest.fixture(name="mock_socket") def mock_socket_fixture(mock_select): """Mock socket.""" - with patch("homeassistant.components.tcp.sensor.socket.socket") as mock_socket: + with patch("homeassistant.components.tcp.common.socket.socket") as mock_socket: socket_instance = mock_socket.return_value.__enter__.return_value socket_instance.recv.return_value = socket_test_value.encode() yield socket_instance @@ -51,12 +51,24 @@ def mock_socket_fixture(mock_select): def mock_select_fixture(): """Mock select.""" with patch( - "homeassistant.components.tcp.sensor.select.select", + "homeassistant.components.tcp.common.select.select", return_value=(True, False, False), ) as mock_select: yield mock_select +@pytest.fixture(name="mock_ssl_context") +def mock_ssl_context_fixture(): + """Mock select.""" + with patch( + "homeassistant.components.tcp.common.ssl.create_default_context", + ) as mock_ssl_context: + mock_ssl_context.return_value.wrap_socket.return_value.recv.return_value = ( + socket_test_value + "_ssl" + ).encode() + yield mock_ssl_context + + async def test_setup_platform_valid_config(hass, mock_socket): """Check a valid configuration and call add_entities with sensor.""" with assert_setup_component(1, "sensor"): @@ -159,3 +171,66 @@ async def test_update_returns_if_template_render_fails(hass, mock_socket): assert state assert state.state == "unknown" + + +async def test_ssl_state(hass, mock_socket, mock_select, mock_ssl_context): + """Return the contents of _state, updated over SSL.""" + config = copy(SENSOR_TEST_CONFIG) + config[tcp.CONF_SSL] = "on" + + assert await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == "test_value_ssl" + assert mock_socket.connect.called + assert mock_socket.connect.call_args == call( + (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) + ) + assert not mock_socket.send.called + assert mock_ssl_context.called + assert mock_ssl_context.return_value.check_hostname + mock_ssl_socket = mock_ssl_context.return_value.wrap_socket.return_value + assert mock_ssl_socket.send.called + assert mock_ssl_socket.send.call_args == call( + SENSOR_TEST_CONFIG["payload"].encode() + ) + assert mock_select.call_args == call( + [mock_ssl_socket], [], [], SENSOR_TEST_CONFIG[tcp.CONF_TIMEOUT] + ) + assert mock_ssl_socket.recv.called + assert mock_ssl_socket.recv.call_args == call(SENSOR_TEST_CONFIG["buffer_size"]) + + +async def test_ssl_state_verify_off(hass, mock_socket, mock_select, mock_ssl_context): + """Return the contents of _state, updated over SSL (verify_ssl disabled).""" + config = copy(SENSOR_TEST_CONFIG) + config[tcp.CONF_SSL] = "on" + config[tcp.CONF_VERIFY_SSL] = "off" + + assert await async_setup_component(hass, "sensor", {"sensor": config}) + await hass.async_block_till_done() + + state = hass.states.get(TEST_ENTITY) + + assert state + assert state.state == "test_value_ssl" + assert mock_socket.connect.called + assert mock_socket.connect.call_args == call( + (SENSOR_TEST_CONFIG["host"], SENSOR_TEST_CONFIG["port"]) + ) + assert not mock_socket.send.called + assert mock_ssl_context.called + assert not mock_ssl_context.return_value.check_hostname + mock_ssl_socket = mock_ssl_context.return_value.wrap_socket.return_value + assert mock_ssl_socket.send.called + assert mock_ssl_socket.send.call_args == call( + SENSOR_TEST_CONFIG["payload"].encode() + ) + assert mock_select.call_args == call( + [mock_ssl_socket], [], [], SENSOR_TEST_CONFIG[tcp.CONF_TIMEOUT] + ) + assert mock_ssl_socket.recv.called + assert mock_ssl_socket.recv.call_args == call(SENSOR_TEST_CONFIG["buffer_size"]) diff --git a/tests/components/template/test_light.py b/tests/components/template/test_light.py index b3a4b2a1aa4..b2eb5f06417 100644 --- a/tests/components/template/test_light.py +++ b/tests/components/template/test_light.py @@ -8,8 +8,11 @@ import homeassistant.components.light as light from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, + ATTR_EFFECT, ATTR_HS_COLOR, + ATTR_TRANSITION, ATTR_WHITE_VALUE, + SUPPORT_TRANSITION, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -378,6 +381,63 @@ async def test_on_action(hass, calls): assert len(calls) == 1 +async def test_on_action_with_transition(hass, calls): + """Test on action with transition.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{states.light.test_state.state}}", + "turn_on": { + "service": "test.automation", + "data_template": { + "transition": "{{transition}}", + }, + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "supports_transition_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + "transition": "{{transition}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("light.test_state", STATE_OFF) + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_OFF + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_TRANSITION: 5}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["transition"] == 5 + + async def test_on_action_optimistic(hass, calls): """Test on action with optimistic state.""" assert await setup.async_setup_component( @@ -443,7 +503,9 @@ async def test_off_action(hass, calls): "service": "light.turn_on", "entity_id": "light.test_state", }, - "turn_off": {"service": "test.automation"}, + "turn_off": { + "service": "test.automation", + }, "set_level": { "service": "light.turn_on", "data_template": { @@ -477,6 +539,63 @@ async def test_off_action(hass, calls): assert len(calls) == 1 +async def test_off_action_with_transition(hass, calls): + """Test off action with transition.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{states.light.test_state.state}}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "test.automation", + "data_template": { + "transition": "{{transition}}", + }, + }, + "supports_transition_template": "{{true}}", + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + "transition": "{{transition}}", + }, + }, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + hass.states.async_set("light.test_state", STATE_ON) + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state.state == STATE_ON + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_OFF, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_TRANSITION: 2}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["transition"] == 2 + + async def test_off_action_optimistic(hass, calls): """Test off action with optimistic state.""" assert await setup.async_setup_component( @@ -1119,6 +1238,417 @@ async def test_color_template(hass, expected_hs, template): assert state.attributes.get("hs_color") == expected_hs +async def test_effect_action_valid_effect(hass, calls): + """Test setting valid effect with template.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{true}}", + "turn_on": {"service": "test.automation"}, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Disco', 'Police'] }}", + "effect_template": "{{ 'Disco' }}", + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_EFFECT: "Disco"}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["effect"] == "Disco" + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect") == "Disco" + + +async def test_effect_action_invalid_effect(hass, calls): + """Test setting invalid effect with template.""" + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{true}}", + "turn_on": {"service": "test.automation"}, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_level": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "brightness": "{{brightness}}", + }, + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Disco', 'Police'] }}", + "effect_template": "{{ None }}", + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + + await hass.services.async_call( + light.DOMAIN, + SERVICE_TURN_ON, + {ATTR_ENTITY_ID: "light.test_template_light", ATTR_EFFECT: "RGB"}, + blocking=True, + ) + + assert len(calls) == 1 + assert calls[0].data["effect"] == "RGB" + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect") is None + + +@pytest.mark.parametrize( + "expected_effect_list,template", + [ + ( + ["Strobe color", "Police", "Christmas", "RGB", "Random Loop"], + "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", + ), + ( + ["Police", "RGB", "Random Loop"], + "{{ ['Police', 'RGB', 'Random Loop'] }}", + ), + (None, "{{ [] }}"), + (None, "{{ '[]' }}"), + (None, "{{ 124 }}"), + (None, "{{ '124' }}"), + (None, "{{ none }}"), + (None, ""), + ], +) +async def test_effect_list_template(hass, expected_effect_list, template): + """Test the template for the effect list.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": template, + "effect_template": "{{ None }}", + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect_list") == expected_effect_list + + +@pytest.mark.parametrize( + "expected_effect,template", + [ + (None, "Disco"), + (None, "None"), + (None, "{{ None }}"), + ("Police", "Police"), + ("Strobe color", "{{ 'Strobe color' }}"), + ], +) +async def test_effect_template(hass, expected_effect, template): + """Test the template for the effect.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + light.DOMAIN, + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_effect": { + "service": "test.automation", + "data_template": { + "entity_id": "test.test_state", + "effect": "{{effect}}", + }, + }, + "effect_list_template": "{{ ['Strobe color', 'Police', 'Christmas', 'RGB', 'Random Loop'] }}", + "effect_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("effect") == expected_effect + + +@pytest.mark.parametrize( + "expected_min_mireds,template", + [ + (118, "{{118}}"), + (153, "{{x - 12}}"), + (153, "None"), + (153, "{{ none }}"), + (153, ""), + (153, "{{ 'a' }}"), + ], +) +async def test_min_mireds_template(hass, expected_min_mireds, template): + """Test the template for the min mireds.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": "{{200}}", + "min_mireds_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("min_mireds") == expected_min_mireds + + +@pytest.mark.parametrize( + "expected_max_mireds,template", + [ + (488, "{{488}}"), + (500, "{{x - 12}}"), + (500, "None"), + (500, "{{ none }}"), + (500, ""), + (500, "{{ 'a' }}"), + ], +) +async def test_max_mireds_template(hass, expected_max_mireds, template): + """Test the template for the max mireds.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "temperature_template": "{{200}}", + "max_mireds_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + assert state is not None + assert state.attributes.get("max_mireds") == expected_max_mireds + + +@pytest.mark.parametrize( + "expected_supports_transition,template", + [ + (True, "{{true}}"), + (True, "{{1 == 1}}"), + (False, "{{false}}"), + (False, "{{ none }}"), + (False, ""), + (False, "None"), + ], +) +async def test_supports_transition_template( + hass, expected_supports_transition, template +): + """Test the template for the supports transition.""" + with assert_setup_component(1, light.DOMAIN): + assert await setup.async_setup_component( + hass, + "light", + { + "light": { + "platform": "template", + "lights": { + "test_template_light": { + "value_template": "{{ 1 == 1 }}", + "turn_on": { + "service": "light.turn_on", + "entity_id": "light.test_state", + }, + "turn_off": { + "service": "light.turn_off", + "entity_id": "light.test_state", + }, + "set_temperature": { + "service": "light.turn_on", + "data_template": { + "entity_id": "light.test_state", + "color_temp": "{{color_temp}}", + }, + }, + "supports_transition_template": template, + } + }, + } + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.test_template_light") + + expected_value = 1 + + if expected_supports_transition is True: + expected_value = 0 + + assert state is not None + assert ( + int(state.attributes.get("supported_features")) & SUPPORT_TRANSITION + ) != expected_value + + async def test_available_template_with_entities(hass): """Test availability templates with values from other entities.""" await setup.async_setup_component( diff --git a/tests/components/tod/test_binary_sensor.py b/tests/components/tod/test_binary_sensor.py index 2eb506f80f3..8b63082c36c 100644 --- a/tests/components/tod/test_binary_sensor.py +++ b/tests/components/tod/test_binary_sensor.py @@ -3,7 +3,6 @@ from datetime import datetime, timedelta from unittest.mock import patch import pytest -import pytz from homeassistant.const import STATE_OFF, STATE_ON import homeassistant.core as ha @@ -61,7 +60,7 @@ async def test_setup_no_sensors(hass): async def test_in_period_on_start(hass): """Test simple setting.""" - test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=hass.config.time_zone) + test_time = datetime(2019, 1, 10, 18, 43, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ { @@ -85,7 +84,7 @@ async def test_in_period_on_start(hass): async def test_midnight_turnover_before_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = datetime(2019, 1, 10, 22, 30, 0, tzinfo=hass.config.time_zone) + test_time = datetime(2019, 1, 10, 22, 30, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -104,9 +103,7 @@ async def test_midnight_turnover_before_midnight_inside_period(hass): async def test_midnight_turnover_after_midnight_inside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = hass.config.time_zone.localize( - datetime(2019, 1, 10, 21, 0, 0) - ).astimezone(pytz.UTC) + test_time = datetime(2019, 1, 10, 21, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -140,9 +137,7 @@ async def test_midnight_turnover_after_midnight_inside_period(hass): async def test_midnight_turnover_before_midnight_outside_period(hass): """Test midnight turnover setting before midnight outside period.""" - test_time = hass.config.time_zone.localize( - datetime(2019, 1, 10, 20, 30, 0) - ).astimezone(pytz.UTC) + test_time = datetime(2019, 1, 10, 20, 30, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Night", "after": "22:00", "before": "5:00"} @@ -161,9 +156,7 @@ async def test_midnight_turnover_before_midnight_outside_period(hass): async def test_midnight_turnover_after_midnight_outside_period(hass): """Test midnight turnover setting before midnight inside period .""" - test_time = hass.config.time_zone.localize( - datetime(2019, 1, 10, 20, 0, 0) - ).astimezone(pytz.UTC) + test_time = datetime(2019, 1, 10, 20, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ @@ -180,9 +173,7 @@ async def test_midnight_turnover_after_midnight_outside_period(hass): state = hass.states.get("binary_sensor.night") assert state.state == STATE_OFF - switchover_time = hass.config.time_zone.localize( - datetime(2019, 1, 11, 4, 59, 0) - ).astimezone(pytz.UTC) + switchover_time = datetime(2019, 1, 11, 4, 59, 0, tzinfo=dt_util.UTC) with patch( "homeassistant.components.tod.binary_sensor.dt_util.utcnow", return_value=switchover_time, @@ -210,9 +201,7 @@ async def test_midnight_turnover_after_midnight_outside_period(hass): async def test_from_sunrise_to_sunset(hass): """Test period from sunrise to sunset.""" - test_time = hass.config.time_zone.localize(datetime(2019, 1, 12)).astimezone( - pytz.UTC - ) + test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) ) @@ -311,9 +300,7 @@ async def test_from_sunrise_to_sunset(hass): async def test_from_sunset_to_sunrise(hass): """Test period from sunset to sunrise.""" - test_time = hass.config.time_zone.localize(datetime(2019, 1, 12)).astimezone( - pytz.UTC - ) + test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) sunset = dt_util.as_local(get_astral_event_date(hass, "sunset", test_time)) sunrise = dt_util.as_local(get_astral_event_next(hass, "sunrise", sunset)) # assert sunset == sunrise @@ -405,13 +392,13 @@ async def test_from_sunset_to_sunrise(hass): async def test_offset(hass): """Test offset.""" - after = hass.config.time_zone.localize(datetime(2019, 1, 10, 18, 0, 0)).astimezone( - pytz.UTC - ) + timedelta(hours=1, minutes=34) + after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=dt_util.UTC) + timedelta( + hours=1, minutes=34 + ) - before = hass.config.time_zone.localize(datetime(2019, 1, 10, 22, 0, 0)).astimezone( - pytz.UTC - ) + timedelta(hours=1, minutes=45) + before = datetime(2019, 1, 10, 22, 0, 0, tzinfo=dt_util.UTC) + timedelta( + hours=1, minutes=45 + ) entity_id = "binary_sensor.evening" config = { @@ -484,9 +471,9 @@ async def test_offset(hass): async def test_offset_overnight(hass): """Test offset overnight.""" - after = hass.config.time_zone.localize(datetime(2019, 1, 10, 18, 0, 0)).astimezone( - pytz.UTC - ) + timedelta(hours=1, minutes=34) + after = datetime(2019, 1, 10, 18, 0, 0, tzinfo=dt_util.UTC) + timedelta( + hours=1, minutes=34 + ) entity_id = "binary_sensor.evening" config = { "binary_sensor": [ @@ -528,9 +515,7 @@ async def test_norwegian_case_winter(hass): hass.config.latitude = 69.6 hass.config.longitude = 18.8 - test_time = hass.config.time_zone.localize(datetime(2010, 1, 1)).astimezone( - pytz.UTC - ) + test_time = datetime(2010, 1, 1, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) ) @@ -645,9 +630,7 @@ async def test_norwegian_case_summer(hass): hass.config.longitude = 18.8 hass.config.elevation = 10.0 - test_time = hass.config.time_zone.localize(datetime(2010, 6, 1)).astimezone( - pytz.UTC - ) + test_time = datetime(2010, 6, 1, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_next(hass, "sunrise", dt_util.as_utc(test_time)) @@ -759,9 +742,7 @@ async def test_norwegian_case_summer(hass): async def test_sun_offset(hass): """Test sun event with offset.""" - test_time = hass.config.time_zone.localize(datetime(2019, 1, 12)).astimezone( - pytz.UTC - ) + test_time = datetime(2019, 1, 12, tzinfo=dt_util.UTC) sunrise = dt_util.as_local( get_astral_event_date(hass, "sunrise", dt_util.as_utc(test_time)) + timedelta(hours=-1, minutes=-30) @@ -881,30 +862,27 @@ async def test_sun_offset(hass): async def test_dst(hass): """Test sun event with offset.""" - hass.config.time_zone = pytz.timezone("CET") - test_time = hass.config.time_zone.localize( - datetime(2019, 3, 30, 3, 0, 0) - ).astimezone(pytz.UTC) + hass.config.time_zone = "CET" + test_time = datetime(2019, 3, 30, 3, 0, 0, tzinfo=dt_util.UTC) config = { "binary_sensor": [ {"platform": "tod", "name": "Day", "after": "2:30", "before": "2:40"} ] } + # Test DST: # after 2019-03-30 03:00 CET the next update should ge scheduled # at 3:30 not 2:30 local time - # Internally the entity_id = "binary_sensor.day" - testtime = test_time with patch( "homeassistant.components.tod.binary_sensor.dt_util.utcnow", - return_value=testtime, + return_value=test_time, ): await async_setup_component(hass, "binary_sensor", config) await hass.async_block_till_done() await hass.async_block_till_done() state = hass.states.get(entity_id) - assert state.attributes["after"] == "2019-03-31T03:30:00+02:00" - assert state.attributes["before"] == "2019-03-31T03:40:00+02:00" - assert state.attributes["next_update"] == "2019-03-31T03:30:00+02:00" + assert state.attributes["after"] == "2019-03-30T03:30:00+01:00" + assert state.attributes["before"] == "2019-03-30T03:40:00+01:00" + assert state.attributes["next_update"] == "2019-03-30T03:30:00+01:00" assert state.state == STATE_OFF diff --git a/tests/components/totalconnect/common.py b/tests/components/totalconnect/common.py index d4285c07425..b092a028c0b 100644 --- a/tests/components/totalconnect/common.py +++ b/tests/components/totalconnect/common.py @@ -9,8 +9,10 @@ from homeassistant.setup import async_setup_component from tests.common import MockConfigEntry +LOCATION_ID = "123456" + LOCATION_INFO_BASIC_NORMAL = { - "LocationID": "123456", + "LocationID": LOCATION_ID, "LocationName": "test", "SecurityDeviceID": "987654", "PhotoURL": "http://www.example.com/some/path/to/file.jpg", @@ -57,13 +59,71 @@ PARTITION_ARMED_AWAY = { "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_AWAY, } +PARTITION_ARMED_CUSTOM = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_CUSTOM_BYPASS, +} + +PARTITION_ARMED_NIGHT = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMED_STAY_NIGHT, +} + +PARTITION_ARMING = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ARMING, +} +PARTITION_DISARMING = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.DISARMING, +} + +PARTITION_TRIGGERED_POLICE = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ALARMING, +} + +PARTITION_TRIGGERED_FIRE = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ALARMING_FIRE_SMOKE, +} + +PARTITION_TRIGGERED_CARBON_MONOXIDE = { + "PartitionID": "1", + "ArmingState": TotalConnectClient.TotalConnectLocation.ALARMING_CARBON_MONOXIDE, +} + +PARTITION_UNKNOWN = { + "PartitionID": "1", + "ArmingState": "99999", +} + + PARTITION_INFO_DISARMED = {0: PARTITION_DISARMED} PARTITION_INFO_ARMED_STAY = {0: PARTITION_ARMED_STAY} PARTITION_INFO_ARMED_AWAY = {0: PARTITION_ARMED_AWAY} +PARTITION_INFO_ARMED_CUSTOM = {0: PARTITION_ARMED_CUSTOM} +PARTITION_INFO_ARMED_NIGHT = {0: PARTITION_ARMED_NIGHT} +PARTITION_INFO_ARMING = {0: PARTITION_ARMING} +PARTITION_INFO_DISARMING = {0: PARTITION_DISARMING} +PARTITION_INFO_TRIGGERED_POLICE = {0: PARTITION_TRIGGERED_POLICE} +PARTITION_INFO_TRIGGERED_FIRE = {0: PARTITION_TRIGGERED_FIRE} +PARTITION_INFO_TRIGGERED_CARBON_MONOXIDE = {0: PARTITION_TRIGGERED_CARBON_MONOXIDE} +PARTITION_INFO_UNKNOWN = {0: PARTITION_UNKNOWN} PARTITIONS_DISARMED = {"PartitionInfo": PARTITION_INFO_DISARMED} PARTITIONS_ARMED_STAY = {"PartitionInfo": PARTITION_INFO_ARMED_STAY} PARTITIONS_ARMED_AWAY = {"PartitionInfo": PARTITION_INFO_ARMED_AWAY} +PARTITIONS_ARMED_CUSTOM = {"PartitionInfo": PARTITION_INFO_ARMED_CUSTOM} +PARTITIONS_ARMED_NIGHT = {"PartitionInfo": PARTITION_INFO_ARMED_NIGHT} +PARTITIONS_ARMING = {"PartitionInfo": PARTITION_INFO_ARMING} +PARTITIONS_DISARMING = {"PartitionInfo": PARTITION_INFO_DISARMING} +PARTITIONS_TRIGGERED_POLICE = {"PartitionInfo": PARTITION_INFO_TRIGGERED_POLICE} +PARTITIONS_TRIGGERED_FIRE = {"PartitionInfo": PARTITION_INFO_TRIGGERED_FIRE} +PARTITIONS_TRIGGERED_CARBON_MONOXIDE = { + "PartitionInfo": PARTITION_INFO_TRIGGERED_CARBON_MONOXIDE +} +PARTITIONS_UNKNOWN = {"PartitionInfo": PARTITION_INFO_UNKNOWN} ZONE_NORMAL = { "ZoneID": "1", @@ -92,9 +152,53 @@ METADATA_ARMED_STAY["Partitions"] = PARTITIONS_ARMED_STAY METADATA_ARMED_AWAY = METADATA_DISARMED.copy() METADATA_ARMED_AWAY["Partitions"] = PARTITIONS_ARMED_AWAY +METADATA_ARMED_CUSTOM = METADATA_DISARMED.copy() +METADATA_ARMED_CUSTOM["Partitions"] = PARTITIONS_ARMED_CUSTOM + +METADATA_ARMED_NIGHT = METADATA_DISARMED.copy() +METADATA_ARMED_NIGHT["Partitions"] = PARTITIONS_ARMED_NIGHT + +METADATA_ARMING = METADATA_DISARMED.copy() +METADATA_ARMING["Partitions"] = PARTITIONS_ARMING + +METADATA_DISARMING = METADATA_DISARMED.copy() +METADATA_DISARMING["Partitions"] = PARTITIONS_DISARMING + +METADATA_TRIGGERED_POLICE = METADATA_DISARMED.copy() +METADATA_TRIGGERED_POLICE["Partitions"] = PARTITIONS_TRIGGERED_POLICE + +METADATA_TRIGGERED_FIRE = METADATA_DISARMED.copy() +METADATA_TRIGGERED_FIRE["Partitions"] = PARTITIONS_TRIGGERED_FIRE + +METADATA_TRIGGERED_CARBON_MONOXIDE = METADATA_DISARMED.copy() +METADATA_TRIGGERED_CARBON_MONOXIDE["Partitions"] = PARTITIONS_TRIGGERED_CARBON_MONOXIDE + +METADATA_UNKNOWN = METADATA_DISARMED.copy() +METADATA_UNKNOWN["Partitions"] = PARTITIONS_UNKNOWN + RESPONSE_DISARMED = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_DISARMED} RESPONSE_ARMED_STAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_STAY} RESPONSE_ARMED_AWAY = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_AWAY} +RESPONSE_ARMED_CUSTOM = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_ARMED_CUSTOM, +} +RESPONSE_ARMED_NIGHT = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMED_NIGHT} +RESPONSE_ARMING = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_ARMING} +RESPONSE_DISARMING = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_DISARMING} +RESPONSE_TRIGGERED_POLICE = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_TRIGGERED_POLICE, +} +RESPONSE_TRIGGERED_FIRE = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_TRIGGERED_FIRE, +} +RESPONSE_TRIGGERED_CARBON_MONOXIDE = { + "ResultCode": 0, + "PanelMetadataAndStatus": METADATA_TRIGGERED_CARBON_MONOXIDE, +} +RESPONSE_UNKNOWN = {"ResultCode": 0, "PanelMetadataAndStatus": METADATA_UNKNOWN} RESPONSE_ARM_SUCCESS = {"ResultCode": TotalConnectClient.TotalConnectClient.ARM_SUCCESS} RESPONSE_ARM_FAILURE = { diff --git a/tests/components/totalconnect/test_alarm_control_panel.py b/tests/components/totalconnect/test_alarm_control_panel.py index 12ad53733b5..a77adea5e27 100644 --- a/tests/components/totalconnect/test_alarm_control_panel.py +++ b/tests/components/totalconnect/test_alarm_control_panel.py @@ -9,21 +9,37 @@ from homeassistant.const import ( ATTR_FRIENDLY_NAME, SERVICE_ALARM_ARM_AWAY, SERVICE_ALARM_ARM_HOME, + SERVICE_ALARM_ARM_NIGHT, SERVICE_ALARM_DISARM, STATE_ALARM_ARMED_AWAY, + STATE_ALARM_ARMED_CUSTOM_BYPASS, STATE_ALARM_ARMED_HOME, + STATE_ALARM_ARMED_NIGHT, + STATE_ALARM_ARMING, STATE_ALARM_DISARMED, + STATE_ALARM_DISARMING, + STATE_ALARM_TRIGGERED, ) from homeassistant.exceptions import HomeAssistantError from .common import ( + LOCATION_ID, RESPONSE_ARM_FAILURE, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_AWAY, + RESPONSE_ARMED_CUSTOM, + RESPONSE_ARMED_NIGHT, RESPONSE_ARMED_STAY, + RESPONSE_ARMING, RESPONSE_DISARM_FAILURE, RESPONSE_DISARM_SUCCESS, RESPONSE_DISARMED, + RESPONSE_DISARMING, + RESPONSE_SUCCESS, + RESPONSE_TRIGGERED_CARBON_MONOXIDE, + RESPONSE_TRIGGERED_FIRE, + RESPONSE_TRIGGERED_POLICE, + RESPONSE_UNKNOWN, RESPONSE_USER_CODE_INVALID, setup_platform, ) @@ -45,6 +61,11 @@ async def test_attributes(hass): mock_request.assert_called_once() assert state.attributes.get(ATTR_FRIENDLY_NAME) == "test" + entity_registry = await hass.helpers.entity_registry.async_get_registry() + entry = entity_registry.async_get(ENTITY_ID) + # TotalConnect alarm device unique_id is the location_id + assert entry.unique_id == LOCATION_ID + async def test_arm_home_success(hass): """Test arm home method success.""" @@ -191,3 +212,135 @@ async def test_disarm_invalid_usercode(hass): await hass.async_block_till_done() assert f"{err.value}" == "TotalConnect failed to disarm test." assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY + + +async def test_arm_night_success(hass): + """Test arm night method success.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_SUCCESS, RESPONSE_ARMED_NIGHT] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + ) + + await hass.async_block_till_done() + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_NIGHT + + +async def test_arm_night_failure(hass): + """Test arm night method failure.""" + responses = [RESPONSE_DISARMED, RESPONSE_ARM_FAILURE, RESPONSE_DISARMED] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + with pytest.raises(HomeAssistantError) as err: + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + ) + await hass.async_block_till_done() + assert f"{err.value}" == "TotalConnect failed to arm night test." + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + +async def test_arming(hass): + """Test arming.""" + responses = [RESPONSE_DISARMED, RESPONSE_SUCCESS, RESPONSE_ARMING] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMED + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_ARM_NIGHT, DATA, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMING + + +async def test_disarming(hass): + """Test disarming.""" + responses = [RESPONSE_ARMED_AWAY, RESPONSE_SUCCESS, RESPONSE_DISARMING] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_ARMED_AWAY + + await hass.services.async_call( + ALARM_DOMAIN, SERVICE_ALARM_DISARM, DATA, blocking=True + ) + assert hass.states.get(ENTITY_ID).state == STATE_ALARM_DISARMING + + +async def test_triggered_fire(hass): + """Test triggered by fire.""" + responses = [RESPONSE_TRIGGERED_FIRE] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_TRIGGERED + assert state.attributes.get("triggered_source") == "Fire/Smoke" + + +async def test_triggered_police(hass): + """Test triggered by police.""" + responses = [RESPONSE_TRIGGERED_POLICE] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_TRIGGERED + assert state.attributes.get("triggered_source") == "Police/Medical" + + +async def test_triggered_carbon_monoxide(hass): + """Test triggered by carbon monoxide.""" + responses = [RESPONSE_TRIGGERED_CARBON_MONOXIDE] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_TRIGGERED + assert state.attributes.get("triggered_source") == "Carbon Monoxide" + + +async def test_armed_custom(hass): + """Test armed custom.""" + responses = [RESPONSE_ARMED_CUSTOM] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == STATE_ALARM_ARMED_CUSTOM_BYPASS + + +async def test_unknown(hass): + """Test unknown arm status.""" + responses = [RESPONSE_UNKNOWN] + with patch( + "homeassistant.components.totalconnect.TotalConnectClient.TotalConnectClient.request", + side_effect=responses, + ): + await setup_platform(hass, ALARM_DOMAIN) + state = hass.states.get(ENTITY_ID) + assert state.state == "unknown" diff --git a/tests/components/totalconnect/test_config_flow.py b/tests/components/totalconnect/test_config_flow.py index 2f89beab0e0..3751abfc361 100644 --- a/tests/components/totalconnect/test_config_flow.py +++ b/tests/components/totalconnect/test_config_flow.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant import data_entry_flow -from homeassistant.components.totalconnect.const import CONF_LOCATION, DOMAIN +from homeassistant.components.totalconnect.const import CONF_USERCODES, DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_PASSWORD @@ -67,7 +67,7 @@ async def test_user_show_locations(hass): # user enters an invalid usercode result2 = await hass.config_entries.flow.async_configure( result["flow_id"], - user_input={CONF_LOCATION: "bad"}, + user_input={CONF_USERCODES: "bad"}, ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM assert result2["step_id"] == "locations" @@ -77,7 +77,7 @@ async def test_user_show_locations(hass): # user enters a valid usercode result3 = await hass.config_entries.flow.async_configure( result2["flow_id"], - user_input={CONF_LOCATION: "7890"}, + user_input={CONF_USERCODES: "7890"}, ) assert result3["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY # client should have sent another request to validate usercode diff --git a/tests/components/totalconnect/test_init.py b/tests/components/totalconnect/test_init.py index b8024dbe70d..ba33d996a9b 100644 --- a/tests/components/totalconnect/test_init.py +++ b/tests/components/totalconnect/test_init.py @@ -2,7 +2,7 @@ from unittest.mock import patch from homeassistant.components.totalconnect.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_ERROR +from homeassistant.config_entries import ConfigEntryState from homeassistant.setup import async_setup_component from .common import CONFIG_DATA @@ -26,4 +26,4 @@ async def test_reauth_started(hass): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() - assert mock_entry.state == ENTRY_STATE_SETUP_ERROR + assert mock_entry.state is ConfigEntryState.SETUP_ERROR diff --git a/tests/components/trace/test_websocket_api.py b/tests/components/trace/test_websocket_api.py index 4c6ff88fa1b..2660fa86879 100644 --- a/tests/components/trace/test_websocket_api.py +++ b/tests/components/trace/test_websocket_api.py @@ -108,6 +108,7 @@ async def test_get_trace( trigger, context_key, condition_results, + enable_custom_integrations, ): """Test tracing a script or automation.""" id = 1 @@ -1227,7 +1228,9 @@ async def test_script_mode_2(hass, hass_ws_client, script_mode, script_execution assert trace["script_execution"] == "finished" -async def test_trace_blueprint_automation(hass, hass_ws_client): +async def test_trace_blueprint_automation( + hass, hass_ws_client, enable_custom_integrations +): """Test trace of blueprint automation.""" id = 1 diff --git a/tests/components/tuya/test_config_flow.py b/tests/components/tuya/test_config_flow.py index ede6e5ac1db..6ea28cf8d2b 100644 --- a/tests/components/tuya/test_config_flow.py +++ b/tests/components/tuya/test_config_flow.py @@ -96,24 +96,6 @@ async def test_user(hass, tuya): assert not result["result"].unique_id -async def test_import(hass, tuya): - """Test import step.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data=TUYA_USER_DATA, - ) - await hass.async_block_till_done() - - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY - assert result["title"] == USERNAME - assert result["data"][CONF_USERNAME] == USERNAME - assert result["data"][CONF_PASSWORD] == PASSWORD - assert result["data"][CONF_COUNTRYCODE] == COUNTRY_CODE - assert result["data"][CONF_PLATFORM] == TUYA_PLATFORM - assert not result["result"].unique_id - - async def test_abort_if_already_setup(hass, tuya): """Test we abort if Tuya is already setup.""" MockConfigEntry(domain=DOMAIN, data=TUYA_USER_DATA).add_to_hass(hass) @@ -126,14 +108,6 @@ async def test_abort_if_already_setup(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == RESULT_SINGLE_INSTANCE - # Should fail, config exist (flow) - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_SINGLE_INSTANCE - async def test_abort_on_invalid_credentials(hass, tuya): """Test when we have invalid credentials.""" @@ -146,13 +120,6 @@ async def test_abort_on_invalid_credentials(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["errors"] == {"base": RESULT_AUTH_FAILED} - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_AUTH_FAILED - async def test_abort_on_connection_error(hass, tuya): """Test when we have a network error.""" @@ -165,13 +132,6 @@ async def test_abort_on_connection_error(hass, tuya): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == RESULT_CONN_ERROR - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_IMPORT}, data=TUYA_USER_DATA - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == RESULT_CONN_ERROR - async def test_options_flow(hass): """Test config flow options.""" diff --git a/tests/components/twentemilieu/test_config_flow.py b/tests/components/twentemilieu/test_config_flow.py index 27fd86d0868..e4e4b0c8335 100644 --- a/tests/components/twentemilieu/test_config_flow.py +++ b/tests/components/twentemilieu/test_config_flow.py @@ -1,7 +1,7 @@ """Tests for the Twente Milieu config flow.""" import aiohttp -from homeassistant import data_entry_flow +from homeassistant import config_entries, data_entry_flow from homeassistant.components.twentemilieu import config_flow from homeassistant.components.twentemilieu.const import ( CONF_HOUSE_LETTER, @@ -9,6 +9,7 @@ from homeassistant.components.twentemilieu.const import ( CONF_POST_CODE, DOMAIN, ) +from homeassistant.config_entries import SOURCE_USER from homeassistant.const import CONF_ID, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant @@ -16,7 +17,6 @@ from tests.common import MockConfigEntry from tests.test_util.aiohttp import AiohttpClientMocker FIXTURE_USER_INPUT = { - CONF_ID: "12345", CONF_POST_CODE: "1234AB", CONF_HOUSE_NUMBER: "1", CONF_HOUSE_LETTER: "A", @@ -25,9 +25,9 @@ FIXTURE_USER_INPUT = { async def test_show_set_form(hass: HomeAssistant) -> None: """Test that the setup form is served.""" - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -41,9 +41,9 @@ async def test_connection_error( "https://twentemilieuapi.ximmio.com/api/FetchAdress", exc=aiohttp.ClientError ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -60,9 +60,9 @@ async def test_invalid_address( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=FIXTURE_USER_INPUT + ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" @@ -73,9 +73,9 @@ async def test_address_already_set_up( hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test we abort if address has already been set up.""" - MockConfigEntry(domain=DOMAIN, data=FIXTURE_USER_INPUT, title="12345").add_to_hass( - hass - ) + MockConfigEntry( + domain=DOMAIN, data={**FIXTURE_USER_INPUT, CONF_ID: "12345"}, title="12345" + ).add_to_hass(hass) aioclient_mock.post( "https://twentemilieuapi.ximmio.com/api/FetchAdress", @@ -83,9 +83,11 @@ async def test_address_already_set_up( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_init( + config_flow.DOMAIN, + context={"source": config_entries.SOURCE_USER}, + data=FIXTURE_USER_INPUT, + ) assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" @@ -101,13 +103,18 @@ async def test_full_flow_implementation( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.TwenteMilieuFlowHandler() - flow.hass = hass - result = await flow.async_step_user(user_input=None) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER} + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "user" - result = await flow.async_step_user(user_input=FIXTURE_USER_INPUT) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + FIXTURE_USER_INPUT, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY assert result["title"] == "12345" assert result["data"][CONF_POST_CODE] == FIXTURE_USER_INPUT[CONF_POST_CODE] diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 1967369e22b..8f88b6adb4c 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -448,6 +448,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "device_tracker" + assert not result["last_step"] assert set( result["data_schema"].schema[CONF_SSID_FILTER].options.keys() ).intersection(("SSID 1", "SSID 2", "SSID 2_IOT", "SSID 3")) @@ -465,6 +466,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "client_control" + assert not result["last_step"] result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -477,6 +479,7 @@ async def test_advanced_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "statistics_sensors" + assert result["last_step"] result = await hass.config_entries.options.async_configure( result["flow_id"], @@ -519,6 +522,7 @@ async def test_simple_option_flow(hass, aioclient_mock): assert result["type"] == data_entry_flow.RESULT_TYPE_FORM assert result["step_id"] == "simple_options" + assert result["last_step"] result = await hass.config_entries.options.async_configure( result["flow_id"], diff --git a/tests/components/unifi/test_device_tracker.py b/tests/components/unifi/test_device_tracker.py index 2018ae39b64..d583cad86c3 100644 --- a/tests/components/unifi/test_device_tracker.py +++ b/tests/components/unifi/test_device_tracker.py @@ -943,8 +943,6 @@ async def test_restoring_client(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/unifi/test_switch.py b/tests/components/unifi/test_switch.py index b8f6e2da553..ad277f18a8d 100644 --- a/tests/components/unifi/test_switch.py +++ b/tests/components/unifi/test_switch.py @@ -793,8 +793,6 @@ async def test_restore_client_succeed(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={}, entry_id=1, ) @@ -885,8 +883,6 @@ async def test_restore_client_no_old_state(hass, aioclient_mock): title="Mock Title", data=ENTRY_CONFIG, source="test", - connection_class=config_entries.CONN_CLASS_LOCAL_POLL, - system_options={}, options={}, entry_id=1, ) diff --git a/tests/components/universal/test_media_player.py b/tests/components/universal/test_media_player.py index 7bf19116d93..54617a61348 100644 --- a/tests/components/universal/test_media_player.py +++ b/tests/components/universal/test_media_player.py @@ -22,7 +22,7 @@ from homeassistant.const import ( STATE_UNKNOWN, ) from homeassistant.core import Context, callback -from homeassistant.setup import async_setup_component +from homeassistant.setup import async_setup_component, setup_component from tests.common import get_test_home_assistant, mock_service @@ -630,7 +630,7 @@ class TestMediaPlayer(unittest.TestCase): def test_supported_features_children_and_cmds(self): """Test supported media commands with children and attrs.""" config = copy(self.config_children_and_attr) - excmd = {"service": "media_player.test", "data": {"entity_id": "test"}} + excmd = {"service": "media_player.test", "data": {}} config["commands"] = { "turn_on": excmd, "turn_off": excmd, @@ -648,6 +648,7 @@ class TestMediaPlayer(unittest.TestCase): "media_next_track": excmd, "media_previous_track": excmd, "toggle": excmd, + "play_media": excmd, "clear_playlist": excmd, } config = validate_config(config) @@ -676,11 +677,185 @@ class TestMediaPlayer(unittest.TestCase): | universal.SUPPORT_STOP | universal.SUPPORT_NEXT_TRACK | universal.SUPPORT_PREVIOUS_TRACK + | universal.SUPPORT_PLAY_MEDIA | universal.SUPPORT_CLEAR_PLAYLIST ) assert check_flags == ump.supported_features + def test_overrides(self): + """Test overrides.""" + config = copy(self.config_children_and_attr) + excmd = {"service": "test.override", "data": {}} + config["name"] = "overridden" + config["commands"] = { + "turn_on": excmd, + "turn_off": excmd, + "volume_up": excmd, + "volume_down": excmd, + "volume_mute": excmd, + "volume_set": excmd, + "select_sound_mode": excmd, + "select_source": excmd, + "repeat_set": excmd, + "shuffle_set": excmd, + "media_play": excmd, + "media_play_pause": excmd, + "media_pause": excmd, + "media_stop": excmd, + "media_next_track": excmd, + "media_previous_track": excmd, + "clear_playlist": excmd, + "play_media": excmd, + "toggle": excmd, + } + setup_component(self.hass, "media_player", {"media_player": config}) + + service = mock_service(self.hass, "test", "override") + self.hass.services.call( + "media_player", + "turn_on", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 1 + self.hass.services.call( + "media_player", + "turn_off", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 2 + self.hass.services.call( + "media_player", + "volume_up", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 3 + self.hass.services.call( + "media_player", + "volume_down", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 4 + self.hass.services.call( + "media_player", + "volume_mute", + service_data={ + "entity_id": "media_player.overridden", + "is_volume_muted": True, + }, + blocking=True, + ) + assert len(service) == 5 + self.hass.services.call( + "media_player", + "volume_set", + service_data={"entity_id": "media_player.overridden", "volume_level": 1}, + blocking=True, + ) + assert len(service) == 6 + self.hass.services.call( + "media_player", + "select_sound_mode", + service_data={ + "entity_id": "media_player.overridden", + "sound_mode": "music", + }, + blocking=True, + ) + assert len(service) == 7 + self.hass.services.call( + "media_player", + "select_source", + service_data={"entity_id": "media_player.overridden", "source": "video1"}, + blocking=True, + ) + assert len(service) == 8 + self.hass.services.call( + "media_player", + "repeat_set", + service_data={"entity_id": "media_player.overridden", "repeat": "all"}, + blocking=True, + ) + assert len(service) == 9 + self.hass.services.call( + "media_player", + "shuffle_set", + service_data={"entity_id": "media_player.overridden", "shuffle": True}, + blocking=True, + ) + assert len(service) == 10 + self.hass.services.call( + "media_player", + "media_play", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 11 + self.hass.services.call( + "media_player", + "media_pause", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 12 + self.hass.services.call( + "media_player", + "media_stop", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 13 + self.hass.services.call( + "media_player", + "media_next_track", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 14 + self.hass.services.call( + "media_player", + "media_previous_track", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 15 + self.hass.services.call( + "media_player", + "clear_playlist", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 16 + self.hass.services.call( + "media_player", + "media_play_pause", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 17 + self.hass.services.call( + "media_player", + "play_media", + service_data={ + "entity_id": "media_player.overridden", + "media_content_id": 1, + "media_content_type": "channel", + }, + blocking=True, + ) + assert len(service) == 18 + self.hass.services.call( + "media_player", + "toggle", + service_data={"entity_id": "media_player.overridden"}, + blocking=True, + ) + assert len(service) == 19 + def test_supported_features_play_pause(self): """Test supported media commands with play_pause function.""" config = copy(self.config_children_and_attr) diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 24938b1e818..c5075aa322b 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -3,7 +3,7 @@ from contextlib import contextmanager from datetime import timedelta from unittest.mock import patch -from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN +from homeassistant.components.sensor import ATTR_STATE_CLASS, STATE_CLASS_MEASUREMENT from homeassistant.components.utility_meter.const import ( ATTR_TARIFF, ATTR_VALUE, @@ -18,6 +18,7 @@ from homeassistant.components.utility_meter.sensor import ( PAUSED, ) from homeassistant.const import ( + ATTR_DEVICE_CLASS, ATTR_ENTITY_ID, ATTR_UNIT_OF_MEASUREMENT, ENERGY_KILO_WATT_HOUR, @@ -52,7 +53,6 @@ async def test_state(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -159,8 +159,73 @@ async def test_state(hass): assert state.state == "0.123" +async def test_device_class(hass): + """Test utility device_class.""" + config = { + "utility_meter": { + "energy_meter": { + "source": "sensor.energy", + }, + "gas_meter": { + "source": "sensor.gas", + }, + } + } + + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id_energy = config[DOMAIN]["energy_meter"]["source"] + hass.states.async_set( + entity_id_energy, 2, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) + entity_id_gas = config[DOMAIN]["gas_meter"]["source"] + hass.states.async_set( + entity_id_gas, 2, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_meter") + assert state is not None + assert state.state == "0" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + state = hass.states.get("sensor.gas_meter") + assert state is not None + assert state.state == "0" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None + + hass.states.async_set( + entity_id_energy, 3, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} + ) + hass.states.async_set( + entity_id_gas, 3, {ATTR_UNIT_OF_MEASUREMENT: "some_archaic_unit"} + ) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_meter") + assert state is not None + assert state.state == "1" + assert state.attributes.get(ATTR_DEVICE_CLASS) == "energy" + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == ENERGY_KILO_WATT_HOUR + + state = hass.states.get("sensor.gas_meter") + assert state is not None + assert state.state == "1" + assert state.attributes.get(ATTR_DEVICE_CLASS) is None + assert state.attributes.get(ATTR_STATE_CLASS) == STATE_CLASS_MEASUREMENT + assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == "some_archaic_unit" + + async def test_restore_state(hass): """Test utility sensor restore state.""" + last_reset = "2020-12-21T00:00:00.013073+00:00" config = { "utility_meter": { "energy_bill": { @@ -177,7 +242,7 @@ async def test_restore_state(hass): "3", attributes={ ATTR_STATUS: PAUSED, - ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + ATTR_LAST_RESET: last_reset, }, ), State( @@ -185,24 +250,25 @@ async def test_restore_state(hass): "6", attributes={ ATTR_STATUS: COLLECTING, - ATTR_LAST_RESET: "2020-12-21T00:00:00.013073+00:00", + ATTR_LAST_RESET: last_reset, }, ), ], ) assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() # restore from cache state = hass.states.get("sensor.energy_bill_onpeak") assert state.state == "3" assert state.attributes.get("status") == PAUSED + assert state.attributes.get("last_reset") == last_reset state = hass.states.get("sensor.energy_bill_offpeak") assert state.state == "6" assert state.attributes.get("status") == COLLECTING + assert state.attributes.get("last_reset") == last_reset # utility_meter is loaded, now set sensors according to utility_meter: hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -227,7 +293,6 @@ async def test_net_consumption(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -262,7 +327,6 @@ async def test_non_net_consumption(hass): } assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) await hass.async_block_till_done() hass.bus.async_fire(EVENT_HOMEASSISTANT_START) @@ -304,15 +368,14 @@ def gen_config(cycle, offset=None): async def _test_self_reset(hass, config, start_time, expect_reset=True): """Test energy sensor self reset.""" - assert await async_setup_component(hass, DOMAIN, config) - assert await async_setup_component(hass, SENSOR_DOMAIN, config) - await hass.async_block_till_done() - - hass.bus.async_fire(EVENT_HOMEASSISTANT_START) - entity_id = config[DOMAIN]["energy_bill"]["source"] - now = dt_util.parse_datetime(start_time) with alter_time(now): + assert await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + entity_id = config[DOMAIN]["energy_bill"]["source"] + async_fire_time_changed(hass, now) hass.states.async_set( entity_id, 1, {ATTR_UNIT_OF_MEASUREMENT: ENERGY_KILO_WATT_HOUR} @@ -345,10 +408,13 @@ async def _test_self_reset(hass, config, start_time, expect_reset=True): state = hass.states.get("sensor.energy_bill") if expect_reset: assert state.attributes.get("last_period") == "2" + assert state.attributes.get("last_reset") == now.isoformat() assert state.state == "3" else: assert state.attributes.get("last_period") == 0 assert state.state == "5" + start_time_str = dt_util.parse_datetime(start_time).isoformat() + assert state.attributes.get("last_reset") == start_time_str async def test_self_reset_quarter_hourly(hass, legacy_patchable_time): diff --git a/tests/components/venstar/__init__.py b/tests/components/venstar/__init__.py new file mode 100644 index 00000000000..908755a585f --- /dev/null +++ b/tests/components/venstar/__init__.py @@ -0,0 +1 @@ +"""Tests for the venstar integration.""" diff --git a/tests/components/venstar/test_climate.py b/tests/components/venstar/test_climate.py new file mode 100644 index 00000000000..9461032060b --- /dev/null +++ b/tests/components/venstar/test_climate.py @@ -0,0 +1,79 @@ +"""The climate tests for the venstar integration.""" + +from homeassistant.components.climate.const import ( + SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, + SUPPORT_TARGET_HUMIDITY, + SUPPORT_TARGET_TEMPERATURE, +) + +from .util import async_init_integration, mock_venstar_devices + +EXPECTED_BASE_SUPPORTED_FEATURES = ( + SUPPORT_TARGET_TEMPERATURE | SUPPORT_FAN_MODE | SUPPORT_PRESET_MODE +) + + +@mock_venstar_devices +async def test_colortouch(hass): + """Test interfacing with a venstar colortouch with attached humidifier.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.colortouch") + assert state.state == "heat" + + expected_attributes = { + "hvac_modes": ["heat", "cool", "off", "auto"], + "min_temp": 7, + "max_temp": 35, + "min_humidity": 0, + "max_humidity": 60, + "fan_modes": ["on", "auto"], + "preset_modes": ["none", "away", "temperature"], + "current_temperature": 21.0, + "temperature": 20.5, + "current_humidity": 41, + "humidity": 30, + "fan_mode": "auto", + "hvac_action": "idle", + "preset_mode": "temperature", + "fan_state": 0, + "hvac_mode": 0, + "friendly_name": "COLORTOUCH", + "supported_features": EXPECTED_BASE_SUPPORTED_FEATURES + | SUPPORT_TARGET_HUMIDITY, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) + + +@mock_venstar_devices +async def test_t2000(hass): + """Test interfacing with a venstar T2000 presently turned off.""" + + await async_init_integration(hass) + + state = hass.states.get("climate.t2000") + assert state.state == "off" + + expected_attributes = { + "hvac_modes": ["heat", "cool", "off", "auto"], + "min_temp": 7, + "max_temp": 35, + "fan_modes": ["on", "auto"], + "preset_modes": ["none", "away", "temperature"], + "current_temperature": 14.0, + "temperature": None, + "fan_mode": "auto", + "hvac_action": "idle", + "preset_mode": "temperature", + "fan_state": 0, + "hvac_mode": 0, + "friendly_name": "T2000", + "supported_features": EXPECTED_BASE_SUPPORTED_FEATURES, + } + # Only test for a subset of attributes in case + # HA changes the implementation and a new one appears + assert all(item in state.attributes.items() for item in expected_attributes.items()) diff --git a/tests/components/venstar/util.py b/tests/components/venstar/util.py new file mode 100644 index 00000000000..b86f8475798 --- /dev/null +++ b/tests/components/venstar/util.py @@ -0,0 +1,57 @@ +"""Tests for the venstar integration.""" + +import requests_mock + +from homeassistant.components.climate.const import DOMAIN +from homeassistant.const import CONF_HOST, CONF_PLATFORM +from homeassistant.core import HomeAssistant +from homeassistant.setup import async_setup_component + +from tests.common import load_fixture + +TEST_MODELS = ["t2k", "colortouch"] + + +def mock_venstar_devices(f): + """Decorate function to mock a Venstar Colortouch and T2000 thermostat API.""" + + async def wrapper(hass): + # Mock thermostats are: + # Venstar T2000, FW 4.38 + # Venstar "colortouch" T7850, FW 5.1 + with requests_mock.mock() as m: + for model in TEST_MODELS: + m.get( + f"http://venstar-{model}.localdomain/", + text=load_fixture(f"venstar/{model}_root.json"), + ) + m.get( + f"http://venstar-{model}.localdomain/query/info", + text=load_fixture(f"venstar/{model}_info.json"), + ) + m.get( + f"http://venstar-{model}.localdomain/query/sensors", + text=load_fixture(f"venstar/{model}_sensors.json"), + ) + return await f(hass) + + return wrapper + + +async def async_init_integration( + hass: HomeAssistant, + skip_setup: bool = False, +): + """Set up the venstar integration in Home Assistant.""" + platform_config = [] + for model in TEST_MODELS: + platform_config.append( + { + CONF_PLATFORM: "venstar", + CONF_HOST: f"venstar-{model}.localdomain", + } + ) + config = {DOMAIN: platform_config} + + await async_setup_component(hass, DOMAIN, config) + await hass.async_block_till_done() diff --git a/tests/components/vera/test_init.py b/tests/components/vera/test_init.py index c828ef55fcd..2008912bae2 100644 --- a/tests/components/vera/test_init.py +++ b/tests/components/vera/test_init.py @@ -11,7 +11,7 @@ from homeassistant.components.vera import ( CONF_LIGHTS, DOMAIN, ) -from homeassistant.config_entries import ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er @@ -148,7 +148,7 @@ async def test_unload( for config_entry in entries: assert await hass.config_entries.async_unload(config_entry.entry_id) - assert config_entry.state == ENTRY_STATE_NOT_LOADED + assert config_entry.state is ConfigEntryState.NOT_LOADED async def test_async_setup_entry_error( diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index b9af9450132..f850487fe26 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -44,8 +44,6 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: with patch( "homeassistant.components.verisure.config_flow.Verisure", ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -72,7 +70,6 @@ async def test_full_user_flow_single_installation(hass: HomeAssistant) -> None: } assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -107,8 +104,6 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non assert result2["errors"] is None with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -126,7 +121,6 @@ async def test_full_user_flow_multiple_installations(hass: HomeAssistant) -> Non } assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -220,8 +214,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: "homeassistant.components.verisure.config_flow.Verisure.login", return_value=True, ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ) as mock_setup_entry: @@ -243,7 +235,6 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: } assert len(mock_verisure.mock_calls) == 1 - assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 @@ -365,8 +356,6 @@ async def test_options_flow( entry.add_to_hass(hass) with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ), patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ): @@ -397,8 +386,6 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: entry.add_to_hass(hass) with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ), patch( "homeassistant.components.verisure.async_setup_entry", return_value=True, ): @@ -422,185 +409,3 @@ async def test_options_flow_code_format_mismatch(hass: HomeAssistant) -> None: assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "init" assert result["errors"] == {"base": "code_format_mismatch"} - - -# -# Below this line are tests that can be removed once the YAML configuration -# has been removed from this integration. -# -@pytest.mark.parametrize( - "giid,installations", - [ - ("12345", TEST_INSTALLATION), - ("12345", TEST_INSTALLATIONS), - (None, TEST_INSTALLATION), - ], -) -async def test_imports( - hass: HomeAssistant, giid: str | None, installations: dict[str, str] -) -> None: - """Test a YAML import with/without known giid on single/multiple installations.""" - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=installations - ) - mock_verisure.login.return_value = True - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: giid, - CONF_LOCK_CODE_DIGITS: 10, - CONF_LOCK_DEFAULT_CODE: "123456", - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - - assert result["type"] == RESULT_TYPE_CREATE_ENTRY - assert result["title"] == "ascending (12345th street)" - assert result["data"] == { - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: "12345", - CONF_LOCK_CODE_DIGITS: 10, - CONF_LOCK_DEFAULT_CODE: "123456", - CONF_PASSWORD: "SuperS3cr3t!", - } - - assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_imports_invalid_login(hass: HomeAssistant) -> None: - """Test a YAML import that results in a invalid login.""" - with patch( - "homeassistant.components.verisure.config_flow.Verisure.login", - side_effect=VerisureLoginError, - ): - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: None, - CONF_LOCK_CODE_DIGITS: None, - CONF_LOCK_DEFAULT_CODE: None, - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - - assert result["step_id"] == "user" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {"base": "invalid_auth"} - - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure, patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATION - ) - mock_verisure.login.return_value = True - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - { - "email": "verisure_my_pages@example.com", - "password": "SuperS3cr3t!", - }, - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { - CONF_GIID: "12345", - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "SuperS3cr3t!", - } - - assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -async def test_imports_needs_user_installation_choice(hass: HomeAssistant) -> None: - """Test a YAML import that needs to use to decide on the installation.""" - with patch( - "homeassistant.components.verisure.config_flow.Verisure", - ) as mock_verisure: - type(mock_verisure.return_value).installations = PropertyMock( - return_value=TEST_INSTALLATIONS - ) - mock_verisure.login.return_value = True - - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_GIID: None, - CONF_LOCK_CODE_DIGITS: None, - CONF_LOCK_DEFAULT_CODE: None, - CONF_PASSWORD: "SuperS3cr3t!", - }, - ) - - assert result["step_id"] == "installation" - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] is None - - with patch( - "homeassistant.components.verisure.async_setup", return_value=True - ) as mock_setup, patch( - "homeassistant.components.verisure.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], {"giid": "12345"} - ) - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["title"] == "ascending (12345th street)" - assert result2["data"] == { - CONF_GIID: "12345", - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "SuperS3cr3t!", - } - - assert len(mock_verisure.mock_calls) == 2 - assert len(mock_setup.mock_calls) == 1 - assert len(mock_setup_entry.mock_calls) == 1 - - -@pytest.mark.parametrize("giid", ["12345", None]) -async def test_import_already_exists(hass: HomeAssistant, giid: str | None) -> None: - """Test that import flow aborts if exists.""" - MockConfigEntry(domain=DOMAIN, data={}, unique_id="12345").add_to_hass(hass) - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_IMPORT}, - data={ - CONF_EMAIL: "verisure_my_pages@example.com", - CONF_PASSWORD: "SuperS3cr3t!", - CONF_GIID: giid, - }, - ) - - assert result["type"] == RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" diff --git a/tests/components/vizio/conftest.py b/tests/components/vizio/conftest.py index 8124827dbf0..2c32b07cc1a 100644 --- a/tests/components/vizio/conftest.py +++ b/tests/components/vizio/conftest.py @@ -68,6 +68,16 @@ def vizio_data_coordinator_update_fixture(): yield +@pytest.fixture(name="vizio_data_coordinator_update_failure") +def vizio_data_coordinator_update_failure_fixture(): + """Mock get data coordinator update failure.""" + with patch( + "homeassistant.components.vizio.gen_apps_list_from_url", + return_value=None, + ): + yield + + @pytest.fixture(name="vizio_no_unique_id") def vizio_no_unique_id_fixture(): """Mock no vizio unique ID returrned.""" diff --git a/tests/components/vizio/test_init.py b/tests/components/vizio/test_init.py index 16e2a5bb769..ccda9253ec7 100644 --- a/tests/components/vizio/test_init.py +++ b/tests/components/vizio/test_init.py @@ -1,4 +1,6 @@ """Tests for Vizio init.""" +from datetime import timedelta + import pytest from homeassistant.components.media_player.const import DOMAIN as MP_DOMAIN @@ -6,10 +8,11 @@ from homeassistant.components.vizio.const import DOMAIN from homeassistant.const import STATE_UNAVAILABLE from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component +import homeassistant.util.dt as dt_util from .const import MOCK_SPEAKER_CONFIG, MOCK_USER_VALID_TV_CONFIG, UNIQUE_ID -from tests.common import MockConfigEntry +from tests.common import MockConfigEntry, async_fire_time_changed async def test_setup_component( @@ -71,3 +74,31 @@ async def test_speaker_load_and_unload( for entity in entities: assert hass.states.get(entity).state == STATE_UNAVAILABLE assert DOMAIN not in hass.data + + +async def test_coordinator_update_failure( + hass: HomeAssistant, + vizio_connect: pytest.fixture, + vizio_bypass_update: pytest.fixture, + vizio_data_coordinator_update_failure: pytest.fixture, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test coordinator update failure after 10 days.""" + now = dt_util.now() + config_entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_USER_VALID_TV_CONFIG, unique_id=UNIQUE_ID + ) + config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + assert len(hass.states.async_entity_ids(MP_DOMAIN)) == 1 + assert DOMAIN in hass.data + + # Failing 25 days in a row should result in a single log message + # (first one after 10 days, next one would be at 30 days) + for days in range(1, 25): + async_fire_time_changed(hass, now + timedelta(days=days)) + await hass.async_block_till_done() + + err_msg = "Unable to retrieve the apps list from the external server" + assert len([record for record in caplog.records if err_msg in record.msg]) == 1 diff --git a/tests/components/volumio/test_config_flow.py b/tests/components/volumio/test_config_flow.py index fed967f9ffc..b4b3c8f24ed 100644 --- a/tests/components/volumio/test_config_flow.py +++ b/tests/components/volumio/test_config_flow.py @@ -242,7 +242,7 @@ async def test_discovery_updates_unique_id(hass): "name": "dummy", "id": TEST_DISCOVERY_RESULT["id"], }, - state=config_entries.ENTRY_STATE_SETUP_RETRY, + state=config_entries.ConfigEntryState.SETUP_RETRY, ) entry.add_to_hass(hass) diff --git a/tests/components/wallbox/__init__.py b/tests/components/wallbox/__init__.py new file mode 100644 index 00000000000..35bf3cee242 --- /dev/null +++ b/tests/components/wallbox/__init__.py @@ -0,0 +1 @@ +"""Tests for the Wallbox integration.""" diff --git a/tests/components/wallbox/test_config_flow.py b/tests/components/wallbox/test_config_flow.py new file mode 100644 index 00000000000..074f67abe2c --- /dev/null +++ b/tests/components/wallbox/test_config_flow.py @@ -0,0 +1,128 @@ +"""Test the Wallbox config flow.""" +from unittest.mock import patch + +from voluptuous.schema_builder import raises + +from homeassistant import config_entries, data_entry_flow +from homeassistant.components.wallbox import CannotConnect, InvalidAuth, config_flow +from homeassistant.components.wallbox.const import DOMAIN +from homeassistant.core import HomeAssistant + + +async def test_show_set_form(hass: HomeAssistant) -> None: + """Test that the setup form is served.""" + flow = config_flow.ConfigFlow() + flow.hass = hass + result = await flow.async_step_user(user_input=None) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + +async def test_form_invalid_auth(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", + side_effect=InvalidAuth, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.wallbox.config_flow.WallboxHub.async_authenticate", + side_effect=CannotConnect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "station": "12345", + "username": "test-username", + "password": "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_validate_input(hass): + """Test we can validate input.""" + data = { + "station": "12345", + "username": "test-username", + "password": "test-password", + } + + def alternate_authenticate_method(): + return None + + def alternate_get_charger_status_method(station): + data = '{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}' + return data + + with patch( + "wallbox.Wallbox.authenticate", + side_effect=alternate_authenticate_method, + ), patch( + "wallbox.Wallbox.getChargerStatus", + side_effect=alternate_get_charger_status_method, + ): + + result = await config_flow.validate_input(hass, data) + + assert result == {"title": "Wallbox Portal"} + + +async def test_configflow_class(): + """Test configFlow class.""" + configflow = config_flow.ConfigFlow() + assert configflow + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=TypeError, + ), raises(Exception): + assert await configflow.async_step_user(True) + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + side_effect=CannotConnect, + ), raises(Exception): + assert await configflow.async_step_user(True) + + with patch( + "homeassistant.components.wallbox.config_flow.validate_input", + ), raises(Exception): + assert await configflow.async_step_user(True) + + +def test_cannot_connect_class(): + """Test cannot Connect class.""" + cannot_connect = CannotConnect + assert cannot_connect + + +def test_invalid_auth_class(): + """Test invalid auth class.""" + invalid_auth = InvalidAuth + assert invalid_auth diff --git a/tests/components/wallbox/test_init.py b/tests/components/wallbox/test_init.py new file mode 100644 index 00000000000..892e77dc7f6 --- /dev/null +++ b/tests/components/wallbox/test_init.py @@ -0,0 +1,165 @@ +"""Test Wallbox Init Component.""" +import json + +import pytest +import requests_mock +from voluptuous.schema_builder import raises + +from homeassistant.components import wallbox +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME +from homeassistant.helpers.typing import HomeAssistantType + +from tests.common import MockConfigEntry + +entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", +) + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + +test_response_rounding_error = json.loads( + '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' +) + + +async def test_wallbox_setup_entry(hass: HomeAssistantType): + """Test Wallbox Setup.""" + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/12345", + text='{"Temperature": 100, "Location": "Toronto", "Datetime": "2020-07-23", "Units": "Celsius"}', + status_code=200, + ) + assert await wallbox.async_setup_entry(hass, entry) + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":404}', + status_code=404, + ) + assert await wallbox.async_setup_entry(hass, entry) is False + + +async def test_wallbox_unload_entry(hass: HomeAssistantType): + """Test Wallbox Unload.""" + hass.data[DOMAIN] = {"connections": {entry.entry_id: entry}} + + assert await wallbox.async_unload_entry(hass, entry) + + hass.data[DOMAIN] = {"fail_entry": entry} + + with pytest.raises(KeyError): + await wallbox.async_unload_entry(hass, entry) + + +async def test_get_data(hass: HomeAssistantType): + """Test hub class, get_data.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response, + status_code=200, + ) + assert await hub.async_get_data() + + +async def test_get_data_rounding_error(hass: HomeAssistantType): + """Test hub class, get_data with rounding error.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m: + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + json=test_response_rounding_error, + status_code=200, + ) + assert await hub.async_get_data() + + +async def test_authentication_exception(hass: HomeAssistantType): + """Test hub class, authentication raises exception.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) + + assert await hub.async_authenticate() + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=404) + + assert await hub.async_authenticate() + + with requests_mock.Mocker() as m, raises(wallbox.InvalidAuth): + m.get("https://api.wall-box.com/auth/token/user", text="data", status_code=403) + m.get( + "https://api.wall-box.com/chargers/status/test", + json=test_response, + status_code=403, + ) + assert await hub.async_get_data() + + +async def test_get_data_exception(hass: HomeAssistantType): + """Test hub class, authentication raises exception.""" + + station = ("12345",) + username = ("test-username",) + password = "test-password" + + hub = wallbox.WallboxHub(station, username, password, hass) + + with requests_mock.Mocker() as m, raises(ConnectionError): + m.get( + "https://api.wall-box.com/auth/token/user", + text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', + status_code=200, + ) + m.get( + "https://api.wall-box.com/chargers/status/('12345',)", + text="data", + status_code=404, + ) + assert await hub.async_get_data() diff --git a/tests/components/wallbox/test_sensor.py b/tests/components/wallbox/test_sensor.py new file mode 100644 index 00000000000..5c0c3511a30 --- /dev/null +++ b/tests/components/wallbox/test_sensor.py @@ -0,0 +1,81 @@ +"""Test Wallbox Switch component.""" + +import json +from unittest.mock import MagicMock + +from homeassistant.components.wallbox import sensor +from homeassistant.components.wallbox.const import CONF_STATION, DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME + +from tests.common import MockConfigEntry + +entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "test_username", + CONF_PASSWORD: "test_password", + CONF_STATION: "12345", + }, + entry_id="testEntry", +) + +test_response = json.loads( + '{"charging_power": 0,"max_available_power": 25,"charging_speed": 0,"added_range": 372,"added_energy": 44.697}' +) + +test_response_rounding_error = json.loads( + '{"charging_power": "XX","max_available_power": "xx","charging_speed": 0,"added_range": "xx","added_energy": "XX"}' +) + +CONF_STATION = ("12345",) +CONF_USERNAME = ("test-username",) +CONF_PASSWORD = "test-password" + +# wallbox = WallboxHub(CONF_STATION, CONF_USERNAME, CONF_PASSWORD, hass) + + +async def test_wallbox_sensor_class(): + """Test wallbox sensor class.""" + + coordinator = MagicMock(return_value="connected") + idx = 1 + ent = "charging_power" + + wallboxSensor = sensor.WallboxSensor(coordinator, idx, ent, entry) + + assert wallboxSensor.icon == "mdi:ev-station" + assert wallboxSensor.unit_of_measurement == "kW" + assert wallboxSensor.name == "Mock Title Charging Power" + assert wallboxSensor.state + + +# async def test_wallbox_updater(hass: HomeAssistantType): +# """Test wallbox updater.""" +# with requests_mock.Mocker() as m: +# m.get( +# "https://api.wall-box.com/auth/token/user", +# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', +# status_code=200, +# ) +# m.get( +# "https://api.wall-box.com/chargers/status/('12345',)", +# json=test_response, +# status_code=200, +# ) +# await sensor.wallbox_updater(wallbox, hass) + + +# async def test_wallbox_updater_rounding_error(hass: HomeAssistantType): +# """Test wallbox updater rounding error.""" +# with requests_mock.Mocker() as m: +# m.get( +# "https://api.wall-box.com/auth/token/user", +# text='{"jwt":"fakekeyhere","user_id":12345,"ttl":145656758,"error":false,"status":200}', +# status_code=200, +# ) +# m.get( +# "https://api.wall-box.com/chargers/status/('12345',)", +# json=test_response_rounding_error, +# status_code=200, +# ) +# await sensor.wallbox_updater(wallbox, hass) diff --git a/tests/components/waze_travel_time/conftest.py b/tests/components/waze_travel_time/conftest.py index dd5b343cc16..237b476aa25 100644 --- a/tests/components/waze_travel_time/conftest.py +++ b/tests/components/waze_travel_time/conftest.py @@ -35,6 +35,16 @@ def bypass_setup_fixture(): yield +@pytest.fixture(name="bypass_platform_setup") +def bypass_platform_setup_fixture(): + """Bypass platform setup.""" + with patch( + "homeassistant.components.waze_travel_time.sensor.async_setup_entry", + return_value=True, + ): + yield + + @pytest.fixture(name="mock_update") def mock_update_fixture(): """Mock an update to the sensor.""" diff --git a/tests/components/waze_travel_time/test_config_flow.py b/tests/components/waze_travel_time/test_config_flow.py index b6690be4d86..f0f8a0f3bde 100644 --- a/tests/components/waze_travel_time/test_config_flow.py +++ b/tests/components/waze_travel_time/test_config_flow.py @@ -145,8 +145,125 @@ async def test_import(hass, validate_config_entry, mock_update): } -async def test_dupe_id(hass, validate_config_entry, bypass_setup): - """Test setting up the same entry twice fails.""" +async def _setup_dupe_import(hass, mock_update): + """Set up dupe import.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + await hass.async_block_till_done() + + +async def test_dupe_import(hass, mock_update): + """Test duplicate import.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "already_configured" + + +async def test_dupe_import_false_check_different_options_value(hass, mock_update): + """Test false duplicate import check when options value differs.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_UNITS: CONF_UNIT_SYSTEM_IMPERIAL, + CONF_VEHICLE_TYPE: "car", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_default_option(hass, mock_update): + """Test false duplicate import check when option with a default is missing.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_INCL_FILTER: "include", + CONF_REALTIME: False, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe_import_false_check_no_default_option(hass, mock_update): + """Test false duplicate import check option when option with no default is miissing.""" + await _setup_dupe_import(hass, mock_update) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + CONF_ORIGIN: "location1", + CONF_DESTINATION: "location2", + CONF_REGION: "US", + CONF_AVOID_FERRIES: True, + CONF_AVOID_SUBSCRIPTION_ROADS: True, + CONF_AVOID_TOLL_ROADS: True, + CONF_EXCL_FILTER: "exclude", + CONF_REALTIME: False, + CONF_VEHICLE_TYPE: "taxi", + }, + ) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + +async def test_dupe(hass, validate_config_entry, bypass_setup): + """Test setting up the same entry data twice is OK.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) @@ -182,8 +299,7 @@ async def test_dupe_id(hass, validate_config_entry, bypass_setup): ) await hass.async_block_till_done() - assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result2["reason"] == "already_configured" + assert result2["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_invalid_config_entry(hass, invalidate_config_entry): diff --git a/tests/components/waze_travel_time/test_init.py b/tests/components/waze_travel_time/test_init.py new file mode 100644 index 00000000000..bf8f6a95844 --- /dev/null +++ b/tests/components/waze_travel_time/test_init.py @@ -0,0 +1,21 @@ +"""Test Waze Travel Time initialization.""" +from homeassistant.components.waze_travel_time.const import DOMAIN +from homeassistant.helpers.entity_registry import async_get + +from tests.common import MockConfigEntry + + +async def test_migration(hass, bypass_platform_setup): + """Test migration logic for unique id.""" + config_entry = MockConfigEntry( + domain=DOMAIN, version=1, entry_id="test", unique_id="test" + ) + ent_reg = async_get(hass) + ent_entry = ent_reg.async_get_or_create( + "sensor", DOMAIN, unique_id="replaceable_unique_id", config_entry=config_entry + ) + entity_id = ent_entry.entity_id + config_entry.add_to_hass(hass) + await hass.config_entries.async_setup(config_entry.entry_id) + assert config_entry.unique_id is None + assert ent_reg.async_get(entity_id).unique_id == config_entry.entry_id diff --git a/tests/components/webhook/test_init.py b/tests/components/webhook/test_init.py index 44c45c1e9d8..63d4a6e134d 100644 --- a/tests/components/webhook/test_init.py +++ b/tests/components/webhook/test_init.py @@ -141,7 +141,9 @@ async def test_webhook_head(hass, mock_client): assert hooks[0][2].method == "HEAD" -async def test_listing_webhook(hass, hass_ws_client, hass_access_token): +async def test_listing_webhook( + hass, hass_ws_client, hass_access_token, enable_custom_integrations +): """Test unregistering a webhook.""" assert await async_setup_component(hass, "webhook", {}) client = await hass_ws_client(hass, hass_access_token) diff --git a/tests/components/websocket_api/test_connection.py b/tests/components/websocket_api/test_connection.py index 55126ff1333..1d6bf5f2f6b 100644 --- a/tests/components/websocket_api/test_connection.py +++ b/tests/components/websocket_api/test_connection.py @@ -1,6 +1,7 @@ """Test WebSocket Connection class.""" import asyncio import logging +from unittest.mock import Mock import voluptuous as vol @@ -8,6 +9,8 @@ from homeassistant import exceptions from homeassistant.components import websocket_api from homeassistant.components.websocket_api import const +from tests.common import MockUser + async def test_send_big_result(hass, websocket_client): """Test sending big results over the WS.""" @@ -31,8 +34,10 @@ async def test_send_big_result(hass, websocket_client): async def test_exception_handling(): """Test handling of exceptions.""" send_messages = [] + user = MockUser() + refresh_token = Mock() conn = websocket_api.ActiveConnection( - logging.getLogger(__name__), None, send_messages.append, None, None + logging.getLogger(__name__), None, send_messages.append, user, refresh_token ) for (exc, code, err) in ( diff --git a/tests/components/wemo/test_init.py b/tests/components/wemo/test_init.py index 1164af7cf95..c44bdb659c5 100644 --- a/tests/components/wemo/test_init.py +++ b/tests/components/wemo/test_init.py @@ -117,20 +117,26 @@ async def test_discovery(hass, pywemo_registry): with patch( "pywemo.discover_devices", return_value=pywemo_devices ) as mock_discovery: - assert await async_setup_component( - hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} - ) - await pywemo_registry.semaphore.acquire() # Returns after platform setup. - mock_discovery.assert_called() - pywemo_devices.append(create_device(2)) + with patch( + "homeassistant.components.wemo.WemoDiscovery.discover_statics" + ) as mock_discover_statics: + assert await async_setup_component( + hass, DOMAIN, {DOMAIN: {CONF_DISCOVERY: True}} + ) + await pywemo_registry.semaphore.acquire() # Returns after platform setup. + mock_discovery.assert_called() + mock_discover_statics.assert_called() + pywemo_devices.append(create_device(2)) - # Test that discovery runs periodically and the async_dispatcher_send code works. - async_fire_time_changed( - hass, - dt.utcnow() - + timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1), - ) - await hass.async_block_till_done() + # Test that discovery runs periodically and the async_dispatcher_send code works. + async_fire_time_changed( + hass, + dt.utcnow() + + timedelta(seconds=WemoDiscovery.ADDITIONAL_SECONDS_BETWEEN_SCANS + 1), + ) + await hass.async_block_till_done() + # Test that discover_statics runs during discovery + assert mock_discover_statics.call_count == 3 # Verify that the expected number of devices were setup. entity_reg = er.async_get(hass) diff --git a/tests/components/wilight/test_init.py b/tests/components/wilight/test_init.py index 4f6654d3436..24efdaaa8e1 100644 --- a/tests/components/wilight/test_init.py +++ b/tests/components/wilight/test_init.py @@ -5,11 +5,7 @@ import pytest import pywilight from pywilight.const import DOMAIN -from homeassistant.config_entries import ( - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.wilight import ( @@ -47,7 +43,7 @@ async def test_config_entry_not_ready(hass: HomeAssistant) -> None: """Test the WiLight configuration entry not ready.""" entry = await setup_integration(hass) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry(hass: HomeAssistant, dummy_device_from_host) -> None: @@ -55,11 +51,11 @@ async def test_unload_config_entry(hass: HomeAssistant, dummy_device_from_host) entry = await setup_integration(hass) assert entry.entry_id in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) await hass.async_block_till_done() if DOMAIN in hass.data: assert entry.entry_id not in hass.data[DOMAIN] - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED diff --git a/tests/components/withings/common.py b/tests/components/withings/common.py index 4d3552bf662..aea2d0152b2 100644 --- a/tests/components/withings/common.py +++ b/tests/components/withings/common.py @@ -7,7 +7,6 @@ from urllib.parse import urlparse from aiohttp.test_utils import TestClient import arrow -import pytz from withings_api.common import ( MeasureGetMeasResponse, NotifyAppli, @@ -40,6 +39,7 @@ from homeassistant.core import HomeAssistant from homeassistant.helpers import config_entry_oauth2_flow from homeassistant.helpers.config_entry_oauth2_flow import AUTH_CALLBACK_PATH from homeassistant.setup import async_setup_component +from homeassistant.util import dt as dt_util from tests.test_util.aiohttp import AiohttpClientMocker @@ -77,7 +77,7 @@ def new_profile_config( measuregrps=[], more=False, offset=0, - timezone=pytz.UTC, + timezone=dt_util.UTC, updatetime=arrow.get(12345), ), api_response_sleep_get_summary=api_response_sleep_get_summary diff --git a/tests/components/withings/test_sensor.py b/tests/components/withings/test_sensor.py index 71e69967796..7e337da8afb 100644 --- a/tests/components/withings/test_sensor.py +++ b/tests/components/withings/test_sensor.py @@ -3,7 +3,6 @@ from typing import Any from unittest.mock import patch import arrow -import pytz from withings_api.common import ( GetSleepSummaryData, GetSleepSummarySerie, @@ -29,6 +28,7 @@ from homeassistant.components.withings.const import Measurement from homeassistant.core import HomeAssistant, State from homeassistant.helpers import entity_registry as er from homeassistant.helpers.entity_registry import EntityRegistry +from homeassistant.util import dt as dt_util from .common import ComponentFactory, new_profile_config @@ -189,7 +189,7 @@ PERSON0 = new_profile_config( ), ), more=False, - timezone=pytz.UTC, + timezone=dt_util.UTC, updatetime=arrow.get("2019-08-01"), offset=0, ), @@ -198,7 +198,7 @@ PERSON0 = new_profile_config( offset=0, series=( GetSleepSummarySerie( - timezone=pytz.UTC, + timezone=dt_util.UTC, model=SleepModel.SLEEP_MONITOR, startdate=arrow.get("2019-02-01"), enddate=arrow.get("2019-02-01"), @@ -225,7 +225,7 @@ PERSON0 = new_profile_config( ), ), GetSleepSummarySerie( - timezone=pytz.UTC, + timezone=dt_util.UTC, model=SleepModel.SLEEP_MONITOR, startdate=arrow.get("2019-02-01"), enddate=arrow.get("2019-02-01"), diff --git a/tests/components/wled/test_config_flow.py b/tests/components/wled/test_config_flow.py index 4ed1723be77..e828c632451 100644 --- a/tests/components/wled/test_config_flow.py +++ b/tests/components/wled/test_config_flow.py @@ -4,11 +4,15 @@ from unittest.mock import MagicMock, patch import aiohttp from wled import WLEDConnectionError -from homeassistant import data_entry_flow -from homeassistant.components.wled import config_flow +from homeassistant.components.wled.const import DOMAIN from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import CONF_HOST, CONF_MAC, CONF_NAME, CONTENT_TYPE_JSON from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from . import init_integration @@ -16,176 +20,8 @@ from tests.common import load_fixture from tests.test_util.aiohttp import AiohttpClientMocker -async def test_show_user_form(hass: HomeAssistant) -> None: - """Test that the user set up form is served.""" - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - ) - - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -async def test_show_zeroconf_confirm_form(hass: HomeAssistant) -> None: - """Test that the zeroconf confirmation form is served.""" - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF, CONF_NAME: "test"} - result = await flow.async_step_zeroconf_confirm() - - assert result["description_placeholders"] == {CONF_NAME: "test"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -async def test_show_zerconf_form( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test that the zeroconf confirmation form is served.""" - aioclient_mock.get( - "http://192.168.1.123:80/json/", - text=load_fixture("wled/rgb.json"), - headers={"Content-Type": CONTENT_TYPE_JSON}, - ) - - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf( - {"host": "192.168.1.123", "hostname": "example.local.", "properties": {}} - ) - - assert flow.context[CONF_HOST] == "192.168.1.123" - assert flow.context[CONF_NAME] == "example" - assert result["description_placeholders"] == {CONF_NAME: "example"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we show user form on WLED connection error.""" - aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "example.com"}, - ) - - assert result["errors"] == {"base": "cannot_connect"} - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_zeroconf_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, - ) - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_zeroconf_confirm_connection_error( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow on WLED connection error.""" - aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={ - "source": SOURCE_ZEROCONF, - CONF_HOST: "example.com", - CONF_NAME: "test", - }, - data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, - ) - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - -@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) -async def test_zeroconf_no_data( - update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort if zeroconf provides no data.""" - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - result = await flow.async_step_zeroconf() - - assert result["reason"] == "cannot_connect" - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - - -async def test_user_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_USER}, - data={CONF_HOST: "192.168.1.123"}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_zeroconf_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - -async def test_zeroconf_with_mac_device_exists_abort( - hass: HomeAssistant, aioclient_mock: AiohttpClientMocker -) -> None: - """Test we abort zeroconf flow if WLED device already configured.""" - await init_integration(hass, aioclient_mock) - - result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, - context={"source": SOURCE_ZEROCONF}, - data={ - "host": "192.168.1.123", - "hostname": "example.local.", - "properties": {CONF_MAC: "aabbccddeeff"}, - }, - ) - - assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT - assert result["reason"] == "already_configured" - - async def test_full_user_flow_implementation( - hass: HomeAssistant, aioclient_mock + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker ) -> None: """Test the full manual user flow from start to finish.""" aioclient_mock.get( @@ -195,21 +31,23 @@ async def test_full_user_flow_implementation( ) result = await hass.config_entries.flow.async_init( - config_flow.DOMAIN, + DOMAIN, context={"source": SOURCE_USER}, ) - assert result["step_id"] == "user" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: "192.168.1.123"} ) + assert result.get("title") == "192.168.1.123" + assert result.get("type") == RESULT_TYPE_CREATE_ENTRY + assert "data" in result assert result["data"][CONF_HOST] == "192.168.1.123" assert result["data"][CONF_MAC] == "aabbccddeeff" - assert result["title"] == "192.168.1.123" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY async def test_full_zeroconf_flow_implementation( @@ -222,21 +60,140 @@ async def test_full_zeroconf_flow_implementation( headers={"Content-Type": CONTENT_TYPE_JSON}, ) - flow = config_flow.WLEDFlowHandler() - flow.hass = hass - flow.context = {"source": SOURCE_ZEROCONF} - result = await flow.async_step_zeroconf( - {"host": "192.168.1.123", "hostname": "example.local.", "properties": {}} + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, ) - assert flow.context[CONF_HOST] == "192.168.1.123" - assert flow.context[CONF_NAME] == "example" - assert result["description_placeholders"] == {CONF_NAME: "example"} - assert result["step_id"] == "zeroconf_confirm" - assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 - result = await flow.async_step_zeroconf_confirm(user_input={}) - assert result["data"][CONF_HOST] == "192.168.1.123" - assert result["data"][CONF_MAC] == "aabbccddeeff" - assert result["title"] == "example" - assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + assert result.get("description_placeholders") == {CONF_NAME: "example"} + assert result.get("step_id") == "zeroconf_confirm" + assert result.get("type") == RESULT_TYPE_FORM + assert "flow_id" in result + + flow = flows[0] + assert "context" in flow + assert flow["context"][CONF_HOST] == "192.168.1.123" + assert flow["context"][CONF_NAME] == "example" + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={} + ) + + assert result2.get("title") == "example" + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + + assert "data" in result2 + assert result2["data"][CONF_HOST] == "192.168.1.123" + assert result2["data"][CONF_MAC] == "aabbccddeeff" + + +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +async def test_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we show user form on WLED connection error.""" + aioclient_mock.get("http://example.com/json/", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "example.com"}, + ) + + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("step_id") == "user" + assert result.get("errors") == {"base": "cannot_connect"} + + +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +async def test_zeroconf_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://192.168.1.123/json/", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" + + +@patch("homeassistant.components.wled.WLED.update", side_effect=WLEDConnectionError) +async def test_zeroconf_confirm_connection_error( + update_mock: MagicMock, hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow on WLED connection error.""" + aioclient_mock.get("http://192.168.1.123:80/json/", exc=aiohttp.ClientError) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_ZEROCONF, + CONF_HOST: "example.com", + CONF_NAME: "test", + }, + data={"host": "192.168.1.123", "hostname": "example.com.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "cannot_connect" + + +async def test_user_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_USER}, + data={CONF_HOST: "192.168.1.123"}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_zeroconf_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={"host": "192.168.1.123", "hostname": "example.local.", "properties": {}}, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" + + +async def test_zeroconf_with_mac_device_exists_abort( + hass: HomeAssistant, aioclient_mock: AiohttpClientMocker +) -> None: + """Test we abort zeroconf flow if WLED device already configured.""" + await init_integration(hass, aioclient_mock) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_ZEROCONF}, + data={ + "host": "192.168.1.123", + "hostname": "example.local.", + "properties": {CONF_MAC: "aabbccddeeff"}, + }, + ) + + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" diff --git a/tests/components/wled/test_init.py b/tests/components/wled/test_init.py index edce49cfd80..8db7e266e80 100644 --- a/tests/components/wled/test_init.py +++ b/tests/components/wled/test_init.py @@ -4,7 +4,7 @@ from unittest.mock import MagicMock, patch from wled import WLEDConnectionError from homeassistant.components.wled.const import DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.config_entries import ConfigEntryState from homeassistant.core import HomeAssistant from tests.components.wled import init_integration @@ -17,7 +17,7 @@ async def test_config_entry_not_ready( ) -> None: """Test the WLED configuration entry not ready.""" entry = await init_integration(hass, aioclient_mock) - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_unload_config_entry( diff --git a/tests/components/wled/test_sensor.py b/tests/components/wled/test_sensor.py index f20e2f0419a..fcd36dd70a9 100644 --- a/tests/components/wled/test_sensor.py +++ b/tests/components/wled/test_sensor.py @@ -6,6 +6,8 @@ import pytest from homeassistant.components.sensor import ( DEVICE_CLASS_CURRENT, + DEVICE_CLASS_SIGNAL_STRENGTH, + DEVICE_CLASS_TIMESTAMP, DOMAIN as SENSOR_DOMAIN, ) from homeassistant.components.wled.const import ( @@ -108,7 +110,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_uptime") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:clock-outline" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_TIMESTAMP assert state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) is None assert state.state == "2019-11-11T09:10:00+00:00" @@ -138,7 +140,7 @@ async def test_sensors( state = hass.states.get("sensor.wled_rgb_light_wifi_rssi") assert state - assert state.attributes.get(ATTR_ICON) == "mdi:wifi" + assert state.attributes.get(ATTR_DEVICE_CLASS) == DEVICE_CLASS_SIGNAL_STRENGTH assert ( state.attributes.get(ATTR_UNIT_OF_MEASUREMENT) == SIGNAL_STRENGTH_DECIBELS_MILLIWATT diff --git a/tests/components/xiaomi_miio/test_vacuum.py b/tests/components/xiaomi_miio/test_vacuum.py index 23e5d8884b3..fe0466472fa 100644 --- a/tests/components/xiaomi_miio/test_vacuum.py +++ b/tests/components/xiaomi_miio/test_vacuum.py @@ -5,7 +5,6 @@ from unittest.mock import MagicMock, patch from miio import DeviceException import pytest -from pytz import utc from homeassistant.components.vacuum import ( ATTR_BATTERY_ICON, @@ -55,6 +54,7 @@ from homeassistant.const import ( STATE_ON, STATE_UNAVAILABLE, ) +from homeassistant.util import dt as dt_util from .test_config_flow import TEST_MAC @@ -106,12 +106,12 @@ def mirobo_is_got_error_fixture(): mock_timer_1 = MagicMock() mock_timer_1.enabled = True mock_timer_1.cron = "5 5 1 8 1" - mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_timer_2 = MagicMock() mock_timer_2.enabled = False mock_timer_2.cron = "5 5 1 8 2" - mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] @@ -180,12 +180,12 @@ def mirobo_is_on_fixture(): mock_timer_1 = MagicMock() mock_timer_1.enabled = True mock_timer_1.cron = "5 5 1 8 1" - mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_1.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_timer_2 = MagicMock() mock_timer_2.enabled = False mock_timer_2.cron = "5 5 1 8 2" - mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc) + mock_timer_2.next_schedule = datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC) mock_vacuum.timer.return_value = [mock_timer_1, mock_timer_2] @@ -255,12 +255,12 @@ async def test_xiaomi_vacuum_services(hass, caplog, mock_mirobo_is_got_error): { "enabled": True, "cron": "5 5 1 8 1", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, { "enabled": False, "cron": "5 5 1 8 2", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, ] @@ -353,12 +353,12 @@ async def test_xiaomi_specific_services(hass, caplog, mock_mirobo_is_on): { "enabled": True, "cron": "5 5 1 8 1", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, { "enabled": False, "cron": "5 5 1 8 2", - "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=utc), + "next_schedule": datetime(2020, 5, 23, 13, 21, 10, tzinfo=dt_util.UTC), }, ] diff --git a/tests/components/yeelight/test_config_flow.py b/tests/components/yeelight/test_config_flow.py index 6a1508d7896..dd5ae85e89e 100644 --- a/tests/components/yeelight/test_config_flow.py +++ b/tests/components/yeelight/test_config_flow.py @@ -1,7 +1,9 @@ """Test the Yeelight config flow.""" from unittest.mock import MagicMock, patch -from homeassistant import config_entries +import pytest + +from homeassistant import config_entries, setup from homeassistant.components.yeelight import ( CONF_MODE_MUSIC, CONF_MODEL, @@ -17,8 +19,10 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) +from homeassistant.components.yeelight.config_flow import CannotConnect from homeassistant.const import CONF_DEVICE, CONF_HOST, CONF_ID, CONF_NAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import RESULT_TYPE_ABORT, RESULT_TYPE_FORM from . import ( ID, @@ -205,7 +209,7 @@ async def test_manual(hass: HomeAssistant): ) await hass.async_block_till_done() assert result4["type"] == "create_entry" - assert result4["title"] == IP_ADDRESS + assert result4["title"] == "color 0x000000000015243f" assert result4["data"] == {CONF_HOST: IP_ADDRESS} # Duplicate @@ -286,3 +290,112 @@ async def test_manual_no_capabilities(hass: HomeAssistant): type(mocked_bulb).get_properties.assert_called_once() assert result["type"] == "create_entry" assert result["data"] == {CONF_HOST: IP_ADDRESS} + + +async def test_discovered_by_homekit_and_dhcp(hass): + """Test we get the form with homekit and abort for dhcp source when we get both.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_HOMEKIT}, + data={"host": "1.2.3.4", "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result2 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": "aa:bb:cc:dd:ee:ff"}, + ) + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "already_in_progress" + + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.4", "macaddress": "00:00:00:00:00:00"}, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "already_in_progress" + + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", side_effect=CannotConnect): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_DHCP}, + data={"ip": "1.2.3.5", "macaddress": "00:00:00:00:00:01"}, + ) + assert result3["type"] == RESULT_TYPE_ABORT + assert result3["reason"] == "cannot_connect" + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + {"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + ), + ( + config_entries.SOURCE_HOMEKIT, + {"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ), + ], +) +async def test_discovered_by_dhcp_or_homekit(hass, source, data): + """Test we can setup when discovered from dhcp or homekit.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=data, + ) + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] is None + + with patch(f"{MODULE}.async_setup", return_value=True) as mock_async_setup, patch( + f"{MODULE}.async_setup_entry", + return_value=True, + ) as mock_async_setup_entry: + result2 = await hass.config_entries.flow.async_configure(result["flow_id"], {}) + assert result2["type"] == "create_entry" + assert result2["data"] == {CONF_HOST: IP_ADDRESS, CONF_ID: "0x000000000015243f"} + assert mock_async_setup.called + assert mock_async_setup_entry.called + + +@pytest.mark.parametrize( + "source, data", + [ + ( + config_entries.SOURCE_DHCP, + {"ip": IP_ADDRESS, "macaddress": "aa:bb:cc:dd:ee:ff"}, + ), + ( + config_entries.SOURCE_HOMEKIT, + {"host": IP_ADDRESS, "properties": {"id": "aa:bb:cc:dd:ee:ff"}}, + ), + ], +) +async def test_discovered_by_dhcp_or_homekit_failed_to_get_id(hass, source, data): + """Test we abort if we cannot get the unique id when discovered from dhcp or homekit.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + + mocked_bulb = _mocked_bulb() + type(mocked_bulb).get_capabilities = MagicMock(return_value=None) + with patch(f"{MODULE_CONFIG_FLOW}.yeelight.Bulb", return_value=mocked_bulb): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": source}, + data=data, + ) + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "cannot_connect" diff --git a/tests/components/yeelight/test_init.py b/tests/components/yeelight/test_init.py index b6a59809d30..8a37c2b283e 100644 --- a/tests/components/yeelight/test_init.py +++ b/tests/components/yeelight/test_init.py @@ -11,7 +11,14 @@ from homeassistant.components.yeelight import ( DOMAIN, NIGHTLIGHT_SWITCH_TYPE_LIGHT, ) -from homeassistant.const import CONF_DEVICES, CONF_HOST, CONF_NAME, STATE_UNAVAILABLE +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_DEVICES, + CONF_HOST, + CONF_ID, + CONF_NAME, + STATE_UNAVAILABLE, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er from homeassistant.setup import async_setup_component @@ -35,6 +42,77 @@ from . import ( from tests.common import MockConfigEntry +async def test_ip_changes_fallback_discovery(hass: HomeAssistant): + """Test Yeelight ip changes and we fallback to discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_ID: ID, + CONF_HOST: "5.5.5.5", + }, + unique_id=ID, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + mocked_bulb.get_capabilities = MagicMock( + side_effect=[OSError, CAPABILITIES, CAPABILITIES] + ) + + _discovered_devices = [ + { + "capabilities": CAPABILITIES, + "ip": IP_ADDRESS, + } + ] + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb), patch( + f"{MODULE}.discover_bulbs", return_value=_discovered_devices + ): + assert await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + binary_sensor_entity_id = ENTITY_BINARY_SENSOR_TEMPLATE.format( + f"yeelight_color_{ID}" + ) + entity_registry = er.async_get(hass) + assert entity_registry.async_get(binary_sensor_entity_id) is None + + await hass.async_block_till_done() + + type(mocked_bulb).get_properties = MagicMock(None) + + hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() + await hass.async_block_till_done() + await hass.async_block_till_done() + + entity_registry = er.async_get(hass) + assert entity_registry.async_get(binary_sensor_entity_id) is not None + + +async def test_ip_changes_id_missing_cannot_fallback(hass: HomeAssistant): + """Test Yeelight ip changes and we fallback to discovery.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_HOST: "5.5.5.5", + }, + ) + config_entry.add_to_hass(hass) + + mocked_bulb = _mocked_bulb(True) + mocked_bulb.bulb_type = BulbType.WhiteTempMood + mocked_bulb.get_capabilities = MagicMock( + side_effect=[OSError, CAPABILITIES, CAPABILITIES] + ) + + with patch(f"{MODULE}.Bulb", return_value=mocked_bulb): + assert not await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state is ConfigEntryState.SETUP_RETRY + + async def test_setup_discovery(hass: HomeAssistant): """Test setting up Yeelight by discovery.""" config_entry = MockConfigEntry(domain=DOMAIN, data=CONFIG_ENTRY_DATA) @@ -182,6 +260,7 @@ async def test_bulb_off_while_adding_in_ha(hass: HomeAssistant): hass.data[DOMAIN][DATA_CONFIG_ENTRIES][config_entry.entry_id][DATA_DEVICE].update() await hass.async_block_till_done() + await hass.async_block_till_done() entity_registry = er.async_get(hass) assert entity_registry.async_get(binary_sensor_entity_id) is not None diff --git a/tests/components/zeroconf/test_init.py b/tests/components/zeroconf/test_init.py index 0ad15c947cf..ef0ab1fda60 100644 --- a/tests/components/zeroconf/test_init.py +++ b/tests/components/zeroconf/test_init.py @@ -1,14 +1,7 @@ """Test Zeroconf component setup process.""" -from unittest.mock import patch +from unittest.mock import call, patch -from zeroconf import ( - BadTypeInNameException, - Error as ZeroconfError, - InterfaceChoice, - IPVersion, - ServiceInfo, - ServiceStateChange, -) +from zeroconf import InterfaceChoice, IPVersion, ServiceInfo, ServiceStateChange from homeassistant.components import zeroconf from homeassistant.components.zeroconf import CONF_DEFAULT_INTERFACE, CONF_IPV6 @@ -149,8 +142,10 @@ async def test_setup(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_service_info_mock + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -181,8 +176,9 @@ async def test_setup_with_overly_long_url_and_name(hass, mock_zeroconf, caplog): hass.config, "location_name", "\u00dcBER \u00dcber German Umlaut long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string long string", + ), patch( + "homeassistant.components.zeroconf.ServiceInfo.request", ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_START) await hass.async_block_till_done() @@ -195,8 +191,10 @@ async def test_setup_with_default_interface(hass, mock_zeroconf): """Test default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: True}} ) @@ -210,8 +208,10 @@ async def test_setup_without_default_interface(hass, mock_zeroconf): """Test without default interface config.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_DEFAULT_INTERFACE: False}} ) @@ -223,8 +223,10 @@ async def test_setup_without_ipv6(hass, mock_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: False}} ) @@ -238,8 +240,10 @@ async def test_setup_with_ipv6(hass, mock_zeroconf): """Test without ipv6.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component( hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {CONF_IPV6: True}} ) @@ -253,8 +257,10 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): """Test without ipv6 as default.""" with patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -262,20 +268,6 @@ async def test_setup_with_ipv6_default(hass, mock_zeroconf): assert mock_zeroconf.called_with() -async def test_service_with_invalid_name(hass, mock_zeroconf, caplog): - """Test we do not crash on service with an invalid name.""" - with patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = BadTypeInNameException - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert len(mock_service_browser.mock_calls) == 1 - assert "Failed to get info for device" in caplog.text - - async def test_zeroconf_match_macaddress(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" @@ -300,10 +292,10 @@ async def test_zeroconf_match_macaddress(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "FFAADDCC11DD" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -333,10 +325,10 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = ( - get_zeroconf_info_mock_manufacturer("Samsung Electronics") - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Samsung Electronics"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -346,6 +338,38 @@ async def test_zeroconf_match_manufacturer(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "samsungtv" +async def test_zeroconf_match_manufacturer_not_present(hass, mock_zeroconf): + """Test matchers reject when a property is missing.""" + + def http_only_service_update_mock(zeroconf, services, handlers): + """Call service update handler.""" + handlers[0]( + zeroconf, + "_airplay._tcp.local.", + "s1000._airplay._tcp.local.", + ServiceStateChange.Added, + ) + + with patch.dict( + zc_gen.ZEROCONF, + {"_airplay._tcp.local.": [{"domain": "samsungtv", "manufacturer": "samsung*"}]}, + clear=True, + ), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("aabbccddeeff"), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 0 + + async def test_zeroconf_no_match(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" @@ -366,10 +390,10 @@ async def test_zeroconf_no_match(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_zeroconf_info_mock( - "FFAADDCC11DD" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock("FFAADDCC11DD"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -398,10 +422,10 @@ async def test_zeroconf_no_match_manufacturer(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=http_only_service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = ( - get_zeroconf_info_mock_manufacturer("Not Samsung Electronics") - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_zeroconf_info_mock_manufacturer("Not Samsung Electronics"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -424,10 +448,10 @@ async def test_homekit_match_partial_space(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "LIFX bulb", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("LIFX bulb", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -451,10 +475,10 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("Rachio-fa46ba", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -464,6 +488,33 @@ async def test_homekit_match_partial_dash(hass, mock_zeroconf): assert mock_config_flow.mock_calls[0][1][0] == "rachio" +async def test_homekit_match_partial_fnmatch(hass, mock_zeroconf): + """Test matching homekit devices with fnmatch.""" + with patch.dict( + zc_gen.ZEROCONF, + {"_hap._tcp.local.": [{"domain": "homekit_controller"}]}, + clear=True, + ), patch.dict(zc_gen.HOMEKIT, {"YLDP*": "yeelight"}, clear=True,), patch.object( + hass.config_entries.flow, "async_init" + ) as mock_config_flow, patch.object( + zeroconf, + "HaServiceBrowser", + side_effect=lambda *args, **kwargs: service_update_mock( + *args, **kwargs, limit_service="_hap._tcp.local." + ), + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("YLDP13YL", HOMEKIT_STATUS_UNPAIRED), + ): + assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + assert len(mock_service_browser.mock_calls) == 1 + assert len(mock_config_flow.mock_calls) == 1 + assert mock_config_flow.mock_calls[0][1][0] == "yeelight" + + async def test_homekit_match_full(hass, mock_zeroconf): """Test configured options for a device are loaded via config entry.""" with patch.dict( @@ -478,10 +529,10 @@ async def test_homekit_match_full(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._udp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "BSB002", HOMEKIT_STATUS_UNPAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("BSB002", HOMEKIT_STATUS_UNPAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -505,10 +556,10 @@ async def test_homekit_already_paired(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "tado", HOMEKIT_STATUS_PAIRED - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("tado", HOMEKIT_STATUS_PAIRED), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -533,10 +584,10 @@ async def test_homekit_invalid_paring_status(hass, mock_zeroconf): side_effect=lambda *args, **kwargs: service_update_mock( *args, **kwargs, limit_service="_hap._tcp.local." ), - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( - "tado", b"invalid" - ) + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock("tado", b"invalid"), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -556,10 +607,12 @@ async def test_homekit_not_paired(hass, mock_zeroconf): hass.config_entries.flow, "async_init" ) as mock_config_flow, patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ) as mock_service_browser: - mock_zeroconf.get_service_info.side_effect = get_homekit_info_mock( + ) as mock_service_browser, patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_homekit_info_mock( "this_will_not_match_any_integration", HOMEKIT_STATUS_UNPAIRED - ) + ), + ): assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() @@ -604,90 +657,128 @@ async def test_get_instance(hass, mock_zeroconf): async def test_removed_ignored(hass, mock_zeroconf): """Test we remove it when a zeroconf entry is removed.""" - mock_zeroconf.get_service_info.side_effect = ZeroconfError def service_update_mock(zeroconf, services, handlers): """Call service update handler.""" handlers[0]( - zeroconf, "_service.added", "name._service.added", ServiceStateChange.Added + zeroconf, + "_service.added.local.", + "name._service.added.local.", + ServiceStateChange.Added, ) handlers[0]( zeroconf, - "_service.updated", - "name._service.updated", + "_service.updated.local.", + "name._service.updated.local.", ServiceStateChange.Updated, ) handlers[0]( zeroconf, - "_service.removed", - "name._service.removed", + "_service.removed.local.", + "name._service.removed.local.", ServiceStateChange.Removed, ) - with patch.object(zeroconf, "HaServiceBrowser", side_effect=service_update_mock): + with patch.object( + zeroconf, "HaServiceBrowser", side_effect=service_update_mock + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, + ) as mock_service_info: assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert len(mock_zeroconf.get_service_info.mock_calls) == 2 - assert mock_zeroconf.get_service_info.mock_calls[0][1][0] == "_service.added" - assert mock_zeroconf.get_service_info.mock_calls[1][1][0] == "_service.updated" + assert len(mock_service_info.mock_calls) == 2 + import pprint + + pprint.pprint(mock_service_info.mock_calls[0][1]) + assert mock_service_info.mock_calls[0][1][0] == "_service.added.local." + assert mock_service_info.mock_calls[1][1][0] == "_service.updated.local." -async def test_async_detect_interfaces_setting_non_loopback_route(hass, mock_zeroconf): +_ADAPTER_WITH_DEFAULT_ENABLED = [ + { + "auto": True, + "default": True, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + } +] + + +async def test_async_detect_interfaces_setting_non_loopback_route(hass): """Test without default interface config and the route returns a non-loopback address.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( + with patch( + "homeassistant.components.zeroconf.models.HaZeroconf" + ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.IPRoute.route", - return_value=_ROUTE_NO_LOOPBACK, + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTER_WITH_DEFAULT_ENABLED, + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.Default) + assert mock_zc.mock_calls[0] == call(interfaces=InterfaceChoice.Default) -async def test_async_detect_interfaces_setting_loopback_route(hass, mock_zeroconf): - """Test without default interface config and the route returns a loopback address.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ), patch( - "homeassistant.components.zeroconf.IPRoute.route", return_value=_ROUTE_LOOPBACK - ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) +_ADAPTERS_WITH_MANUAL_CONFIG = [ + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [], + "ipv6": [ + { + "address": "2001:db8::", + "network_prefix": 8, + "flowinfo": 1, + "scope_id": 1, + } + ], + "name": "eth0", + }, + { + "auto": True, + "default": False, + "enabled": True, + "ipv4": [{"address": "192.168.1.5", "network_prefix": 23}], + "ipv6": [], + "name": "eth1", + }, + { + "auto": False, + "default": False, + "enabled": False, + "ipv4": [{"address": "169.254.3.2", "network_prefix": 16}], + "ipv6": [], + "name": "vtun0", + }, +] -async def test_async_detect_interfaces_setting_empty_route(hass, mock_zeroconf): +async def test_async_detect_interfaces_setting_empty_route(hass): """Test without default interface config and the route returns nothing.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( - zeroconf, "HaServiceBrowser", side_effect=service_update_mock - ), patch("homeassistant.components.zeroconf.IPRoute.route", return_value=[]): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock - assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) - hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) - await hass.async_block_till_done() - - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) - - -async def test_async_detect_interfaces_setting_exception(hass, mock_zeroconf): - """Test without default interface config and the route throws an exception.""" - with patch.object(hass.config_entries.flow, "async_init"), patch.object( + with patch( + "homeassistant.components.zeroconf.models.HaZeroconf" + ) as mock_zc, patch.object(hass.config_entries.flow, "async_init"), patch.object( zeroconf, "HaServiceBrowser", side_effect=service_update_mock ), patch( - "homeassistant.components.zeroconf.IPRoute.route", side_effect=AttributeError + "homeassistant.components.zeroconf.network.async_get_adapters", + return_value=_ADAPTERS_WITH_MANUAL_CONFIG, + ), patch( + "homeassistant.components.zeroconf.ServiceInfo", + side_effect=get_service_info_mock, ): - mock_zeroconf.get_service_info.side_effect = get_service_info_mock assert await async_setup_component(hass, zeroconf.DOMAIN, {zeroconf.DOMAIN: {}}) hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) await hass.async_block_till_done() - assert mock_zeroconf.called_with(interface_choice=InterfaceChoice.All) + assert mock_zc.mock_calls[0] == call(interfaces=[1, "192.168.1.5"]) diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 6b1a6fe4f98..ecf5759b835 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -5,7 +5,6 @@ from datetime import datetime from unittest.mock import MagicMock, patch import pytest -from pytz import utc import voluptuous as vol from homeassistant.bootstrap import async_setup_component @@ -19,6 +18,7 @@ from homeassistant.components.zwave import ( from homeassistant.components.zwave.binary_sensor import get_device from homeassistant.const import ATTR_ENTITY_ID, ATTR_NAME from homeassistant.helpers import device_registry as dr, entity_registry as er +from homeassistant.util import dt as dt_util from tests.common import async_fire_time_changed, mock_registry from tests.mock.zwave import MockEntityValues, MockNetwork, MockNode, MockValue @@ -140,7 +140,7 @@ async def test_auto_heal_midnight(hass, mock_openzwave, legacy_patchable_time): network = hass.data[zwave.DATA_NETWORK] assert not network.heal.called - time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) + time = datetime(2017, 5, 6, 0, 0, 0, tzinfo=dt_util.UTC) async_fire_time_changed(hass, time) await hass.async_block_till_done() await hass.async_block_till_done() @@ -156,7 +156,7 @@ async def test_auto_heal_disabled(hass, mock_openzwave): network = hass.data[zwave.DATA_NETWORK] assert not network.heal.called - time = utc.localize(datetime(2017, 5, 6, 0, 0, 0)) + time = datetime(2017, 5, 6, 0, 0, 0, tzinfo=dt_util.UTC) async_fire_time_changed(hass, time) await hass.async_block_till_done() assert not network.heal.called diff --git a/tests/components/zwave/test_lock.py b/tests/components/zwave/test_lock.py index 9050f06f87b..04d46620013 100644 --- a/tests/components/zwave/test_lock.py +++ b/tests/components/zwave/test_lock.py @@ -286,8 +286,6 @@ async def setup_ozw(hass, mock_openzwave): "Mock Title", {"usb_path": "mock-path", "network_key": "mock-key"}, "test", - config_entries.CONN_CLASS_LOCAL_PUSH, - system_options={}, ) await hass.config_entries.async_forward_entry_setup(config_entry, "lock") await hass.async_block_till_done() diff --git a/tests/components/zwave_js/conftest.py b/tests/components/zwave_js/conftest.py index e9078bcf467..12db8bafb77 100644 --- a/tests/components/zwave_js/conftest.py +++ b/tests/components/zwave_js/conftest.py @@ -1,6 +1,7 @@ """Provide common Z-Wave JS fixtures.""" import asyncio import copy +import io import json from unittest.mock import AsyncMock, patch @@ -10,8 +11,6 @@ from zwave_js_server.model.driver import Driver from zwave_js_server.model.node import Node from zwave_js_server.version import VersionInfo -from homeassistant.helpers.device_registry import async_get as async_get_device_registry - from tests.common import MockConfigEntry, load_fixture # Add-on fixtures @@ -30,7 +29,12 @@ def mock_addon_info(addon_info_side_effect): "homeassistant.components.zwave_js.addon.async_get_addon_info", side_effect=addon_info_side_effect, ) as addon_info: - addon_info.return_value = {} + addon_info.return_value = { + "options": {}, + "state": None, + "update_available": False, + "version": None, + } yield addon_info @@ -52,7 +56,6 @@ def mock_addon_installed(addon_info): @pytest.fixture(name="addon_options") def mock_addon_options(addon_info): """Mock add-on options.""" - addon_info.return_value["options"] = {} return addon_info.return_value["options"] @@ -133,12 +136,6 @@ def create_snapshot_fixture(): yield create_shapshot -@pytest.fixture(name="device_registry") -async def device_registry_fixture(hass): - """Return the device registry.""" - return async_get_device_registry(hass) - - @pytest.fixture(name="controller_state", scope="session") def controller_state_fixture(): """Load the controller state fixture data.""" @@ -156,6 +153,18 @@ def version_state_fixture(): } +@pytest.fixture(name="log_config_state") +def log_config_state_fixture(): + """Return log config state fixture data.""" + return { + "enabled": True, + "level": "info", + "logToFile": False, + "filename": "", + "forceConsole": False, + } + + @pytest.fixture(name="multisensor_6_state", scope="session") def multisensor_6_state_fixture(): """Load the multisensor 6 node state fixture data.""" @@ -180,6 +189,12 @@ def bulb_6_multi_color_state_fixture(): return json.loads(load_fixture("zwave_js/bulb_6_multi_color_state.json")) +@pytest.fixture(name="light_color_null_values_state", scope="session") +def light_color_null_values_state_fixture(): + """Load the light color null values node state fixture data.""" + return json.loads(load_fixture("zwave_js/light_color_null_values_state.json")) + + @pytest.fixture(name="eaton_rf9640_dimmer_state", scope="session") def eaton_rf9640_dimmer_state_fixture(): """Load the eaton rf9640 dimmer node state fixture data.""" @@ -240,6 +255,12 @@ def climate_heatit_z_trm3_state_fixture(): return json.loads(load_fixture("zwave_js/climate_heatit_z_trm3_state.json")) +@pytest.fixture(name="climate_heatit_z_trm2fx_state", scope="session") +def climate_heatit_z_trm2fx_state_fixture(): + """Load the climate HEATIT Z-TRM2fx thermostat node state fixture data.""" + return json.loads(load_fixture("zwave_js/climate_heatit_z_trm2fx_state.json")) + + @pytest.fixture(name="nortek_thermostat_state", scope="session") def nortek_thermostat_state_fixture(): """Load the nortek thermostat node state fixture data.""" @@ -276,6 +297,12 @@ def iblinds_v2_state_fixture(): return json.loads(load_fixture("zwave_js/cover_iblinds_v2_state.json")) +@pytest.fixture(name="qubino_shutter_state", scope="session") +def qubino_shutter_state_fixture(): + """Load the Qubino Shutter node state fixture data.""" + return json.loads(load_fixture("zwave_js/cover_qubino_shutter_state.json")) + + @pytest.fixture(name="aeon_smart_switch_6_state", scope="session") def aeon_smart_switch_6_state_fixture(): """Load the AEON Labs (ZW096) Smart Switch 6 node state fixture data.""" @@ -349,8 +376,14 @@ def zem_31_state_fixture(): return json.loads(load_fixture("zwave_js/zen_31_state.json")) +@pytest.fixture(name="wallmote_central_scene_state", scope="session") +def wallmote_central_scene_state_fixture(): + """Load the wallmote central scene node state fixture data.""" + return json.loads(load_fixture("zwave_js/wallmote_central_scene_state.json")) + + @pytest.fixture(name="client") -def mock_client_fixture(controller_state, version_state): +def mock_client_fixture(controller_state, version_state, log_config_state): """Mock a client.""" with patch( @@ -373,7 +406,7 @@ def mock_client_fixture(controller_state, version_state): client.connect = AsyncMock(side_effect=connect) client.listen = AsyncMock(side_effect=listen) client.disconnect = AsyncMock(side_effect=disconnect) - client.driver = Driver(client, controller_state) + client.driver = Driver(client, controller_state, log_config_state) client.version = VersionInfo.from_message(version_state) client.ws_server_url = "ws://test:3000/zjs" @@ -413,6 +446,14 @@ def bulb_6_multi_color_fixture(client, bulb_6_multi_color_state): return node +@pytest.fixture(name="light_color_null_values") +def light_color_null_values_fixture(client, light_color_null_values_state): + """Mock a node with current color value item being null.""" + node = Node(client, copy.deepcopy(light_color_null_values_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="eaton_rf9640_dimmer") def eaton_rf9640_dimmer_fixture(client, eaton_rf9640_dimmer_state): """Mock a Eaton RF9640 (V4 compatible) dimmer node.""" @@ -484,6 +525,14 @@ def climate_heatit_z_trm3_fixture(client, climate_heatit_z_trm3_state): return node +@pytest.fixture(name="climate_heatit_z_trm2fx") +def climate_heatit_z_trm2fx_fixture(client, climate_heatit_z_trm2fx_state): + """Mock a climate radio HEATIT Z-TRM2fx node.""" + node = Node(client, copy.deepcopy(climate_heatit_z_trm2fx_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="nortek_thermostat") def nortek_thermostat_fixture(client, nortek_thermostat_state): """Mock a nortek thermostat node.""" @@ -589,6 +638,14 @@ def iblinds_cover_fixture(client, iblinds_v2_state): return node +@pytest.fixture(name="qubino_shutter") +def qubino_shutter_cover_fixture(client, qubino_shutter_state): + """Mock a Qubino flush shutter node.""" + node = Node(client, copy.deepcopy(qubino_shutter_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + @pytest.fixture(name="aeon_smart_switch_6") def aeon_smart_switch_6_fixture(client, aeon_smart_switch_6_state): """Mock an AEON Labs (ZW096) Smart Switch 6 node.""" @@ -665,3 +722,17 @@ def zen_31_fixture(client, zen_31_state): node = Node(client, copy.deepcopy(zen_31_state)) client.driver.controller.nodes[node.node_id] = node return node + + +@pytest.fixture(name="wallmote_central_scene") +def wallmote_central_scene_fixture(client, wallmote_central_scene_state): + """Mock a wallmote central scene node.""" + node = Node(client, copy.deepcopy(wallmote_central_scene_state)) + client.driver.controller.nodes[node.node_id] = node + return node + + +@pytest.fixture(name="firmware_file") +def firmware_file_fixture(): + """Return mock firmware file stream.""" + return io.BytesIO(bytes(10)) diff --git a/tests/components/zwave_js/test_api.py b/tests/components/zwave_js/test_api.py index 57961ee89e4..c498c4201ae 100644 --- a/tests/components/zwave_js/test_api.py +++ b/tests/components/zwave_js/test_api.py @@ -2,9 +2,15 @@ import json from unittest.mock import patch +import pytest from zwave_js_server.const import LogLevel from zwave_js_server.event import Event -from zwave_js_server.exceptions import InvalidNewValue, NotFoundError, SetValueFailed +from zwave_js_server.exceptions import ( + FailedCommand, + InvalidNewValue, + NotFoundError, + SetValueFailed, +) from homeassistant.components.websocket_api.const import ERR_NOT_FOUND from homeassistant.components.zwave_js.api import ( @@ -59,7 +65,7 @@ async def test_network_status(hass, integration, hass_ws_client): assert msg["error"]["code"] == ERR_NOT_LOADED -async def test_node_status(hass, integration, multisensor_6, hass_ws_client): +async def test_node_status(hass, multisensor_6, integration, hass_ws_client): """Test the node status websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -113,8 +119,139 @@ async def test_node_status(hass, integration, multisensor_6, hass_ws_client): assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_node_metadata(hass, wallmote_central_scene, integration, hass_ws_client): + """Test the node metadata websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + node = wallmote_central_scene + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/node_metadata", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + result = msg["result"] + + assert result[NODE_ID] == 35 + assert result["inclusion"] == ( + "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave " + "primary controller into inclusion mode. Press the Program Switch of ZP3111 " + "for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, " + "otherwise, ZP3111 will go to sleep after 20 seconds." + ) + assert result["exclusion"] == ( + "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave " + "primary controller into \u201cexclusion\u201d mode, and following its " + "instruction to delete the ZP3111 to the controller. Press the Program Switch " + "of ZP3111 once to be excluded." + ) + assert result["reset"] == ( + "Remove cover to triggered tamper switch, LED flash once & send out Alarm " + "Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send " + "the \u201cDevice Reset Locally Notification\u201d command and reset to the " + "factory default. (Remark: This is to be used only in the case of primary " + "controller being inoperable or otherwise unavailable.)" + ) + assert result["manual"] == ( + "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + ) + assert not result["wakeup"] + assert ( + result["device_database_url"] + == "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0" + ) + + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/node_metadata", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/node_metadata", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_ping_node( + hass, wallmote_central_scene, integration, client, hass_ws_client +): + """Test the ping_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + node = wallmote_central_scene + + client.async_send_command.return_value = {"responded": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test getting non-existent node fails + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 99999, + } + ) + msg = await ws_client.receive_json() + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 5, + TYPE: "zwave_js/ping_node", + ENTRY_ID: entry.entry_id, + NODE_ID: node.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_add_node( - hass, integration, client, hass_ws_client, nortek_thermostat_added_event + hass, nortek_thermostat_added_event, integration, client, hass_ws_client ): """Test the add_node websocket command.""" entry = integration @@ -146,7 +283,7 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "node added" node_details = { - "node_id": 53, + "node_id": 67, "status": 0, "ready": False, } @@ -155,7 +292,50 @@ async def test_add_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "device registered" # Check the keys of the device item - assert list(msg["event"]["device"]) == ["name", "id"] + assert list(msg["event"]["device"]) == ["name", "id", "manufacturer", "model"] + + # Test receiving interview events + event = Event( + type="interview started", + data={"source": "node", "event": "interview started", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview started" + + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": 67, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview stage completed" + assert msg["event"]["stage"] == "NodeInfo" + + event = Event( + type="interview completed", + data={"source": "node", "event": "interview completed", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview completed" + + event = Event( + type="interview failed", + data={"source": "node", "event": "interview failed", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview failed" # Test sending command with not loaded entry fails await hass.config_entries.async_unload(entry.entry_id) @@ -246,9 +426,6 @@ async def test_remove_node( msg = await ws_client.receive_json() assert msg["event"]["event"] == "exclusion started" - # Add mock node to controller - client.driver.controller.nodes[67] = nortek_thermostat - dev_reg = dr.async_get(hass) # Create device registry entry for mock node @@ -282,8 +459,398 @@ async def test_remove_node( assert msg["error"]["code"] == ERR_NOT_LOADED +async def test_replace_failed_node( + hass, + nortek_thermostat, + integration, + client, + hass_ws_client, + nortek_thermostat_added_event, + nortek_thermostat_removed_event, +): + """Test the replace_failed_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + dev_reg = dr.async_get(hass) + + # Create device registry entry for mock node + dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "3245146787-67")}, + name="Node 67", + ) + + client.async_send_command.return_value = {"success": True} + + # Order of events we receive for a successful replacement is `inclusion started`, + # `inclusion stopped`, `node removed`, `node added`, then interview stages. + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + event = Event( + type="inclusion started", + data={ + "source": "controller", + "event": "inclusion started", + "secure": False, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "inclusion started" + + event = Event( + type="inclusion stopped", + data={ + "source": "controller", + "event": "inclusion stopped", + "secure": False, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "inclusion stopped" + + # Fire node removed event + client.driver.receive_event(nortek_thermostat_removed_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node removed" + + # Verify device was removed from device registry + device = dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, + ) + assert device is None + + client.driver.receive_event(nortek_thermostat_added_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node added" + node_details = { + "node_id": 67, + "status": 0, + "ready": False, + } + assert msg["event"]["node"] == node_details + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "device registered" + # Check the keys of the device item + assert list(msg["event"]["device"]) == ["name", "id", "manufacturer", "model"] + + # Test receiving interview events + event = Event( + type="interview started", + data={"source": "node", "event": "interview started", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview started" + + event = Event( + type="interview stage completed", + data={ + "source": "node", + "event": "interview stage completed", + "stageName": "NodeInfo", + "nodeId": 67, + }, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview stage completed" + assert msg["event"]["stage"] == "NodeInfo" + + event = Event( + type="interview completed", + data={"source": "node", "event": "interview completed", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview completed" + + event = Event( + type="interview failed", + data={"source": "node", "event": "interview failed", "nodeId": 67}, + ) + client.driver.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "interview failed" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/replace_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_remove_failed_node( + hass, + nortek_thermostat, + integration, + client, + hass_ws_client, + nortek_thermostat_removed_event, +): + """Test the remove_failed_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/remove_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + dev_reg = dr.async_get(hass) + + # Create device registry entry for mock node + device = dev_reg.async_get_or_create( + config_entry_id=entry.entry_id, + identifiers={(DOMAIN, "3245146787-67")}, + name="Node 67", + ) + + # Fire node removed event + client.driver.receive_event(nortek_thermostat_removed_event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "node removed" + + # Verify device was removed from device registry + device = dev_reg.async_get_device( + identifiers={(DOMAIN, "3245146787-67")}, + ) + assert device is None + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/remove_failed_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_begin_healing_network( + hass, + integration, + client, + hass_ws_client, +): + """Test the begin_healing_network websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/begin_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/begin_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_subscribe_heal_network_progress( + hass, integration, client, hass_ws_client +): + """Test the subscribe_heal_network_progress command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_heal_network_progress", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + # Fire heal network progress + event = Event( + "heal network progress", + { + "source": "controller", + "event": "heal network progress", + "progress": {67: "pending"}, + }, + ) + client.driver.controller.receive_event(event) + msg = await ws_client.receive_json() + assert msg["event"]["event"] == "heal network progress" + assert msg["event"]["heal_node_status"] == {"67": "pending"} + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/subscribe_heal_network_progress", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_stop_healing_network( + hass, + integration, + client, + hass_ws_client, +): + """Test the stop_healing_network websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/stop_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/stop_healing_network", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_heal_node( + hass, + integration, + client, + hass_ws_client, +): + """Test the heal_node websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command.return_value = {"success": True} + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + assert msg["result"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 4, + TYPE: "zwave_js/heal_node", + ENTRY_ID: entry.entry_id, + NODE_ID: 67, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + async def test_refresh_node_info( - hass, client, integration, hass_ws_client, multisensor_6 + hass, client, multisensor_6, integration, hass_ws_client ): """Test that the refresh_node_info WS API call works.""" entry = integration @@ -382,7 +949,7 @@ async def test_refresh_node_info( async def test_refresh_node_values( - hass, client, integration, hass_ws_client, multisensor_6 + hass, client, multisensor_6, integration, hass_ws_client ): """Test that the refresh_node_values WS API call works.""" entry = integration @@ -435,7 +1002,7 @@ async def test_refresh_node_values( async def test_refresh_node_cc_values( - hass, client, integration, hass_ws_client, multisensor_6 + hass, client, multisensor_6, integration, hass_ws_client ): """Test that the refresh_node_cc_values WS API call works.""" entry = integration @@ -665,7 +1232,7 @@ async def test_set_config_parameter( assert msg["error"]["code"] == ERR_NOT_LOADED -async def test_get_config_parameters(hass, integration, multisensor_6, hass_ws_client): +async def test_get_config_parameters(hass, multisensor_6, integration, hass_ws_client): """Test the get config parameters websocket command.""" entry = integration ws_client = await hass_ws_client(hass) @@ -737,13 +1304,74 @@ async def test_dump_view(integration, hass_client): assert json.loads(await resp.text()) == [{"hello": "world"}, {"second": "msg"}] -async def test_dump_view_invalid_entry_id(integration, hass_client): +async def test_firmware_upload_view( + hass, multisensor_6, integration, hass_client, firmware_file +): + """Test the HTTP firmware upload view.""" + client = await hass_client() + with patch( + "homeassistant.components.zwave_js.api.begin_firmware_update", + ) as mock_cmd: + resp = await client.post( + f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + data={"file": firmware_file}, + ) + assert mock_cmd.call_args[0][1:4] == (multisensor_6, "file", bytes(10)) + assert json.loads(await resp.text()) is None + + +async def test_firmware_upload_view_failed_command( + hass, multisensor_6, integration, hass_client, firmware_file +): + """Test failed command for the HTTP firmware upload view.""" + client = await hass_client() + with patch( + "homeassistant.components.zwave_js.api.begin_firmware_update", + side_effect=FailedCommand("test", "test"), + ): + resp = await client.post( + f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + data={"file": firmware_file}, + ) + assert resp.status == 400 + + +async def test_firmware_upload_view_invalid_payload( + hass, multisensor_6, integration, hass_client +): + """Test an invalid payload for the HTTP firmware upload view.""" + client = await hass_client() + resp = await client.post( + f"/api/zwave_js/firmware/upload/{integration.entry_id}/{multisensor_6.node_id}", + data={"wrong_key": bytes(10)}, + ) + assert resp.status == 400 + + +@pytest.mark.parametrize( + "method, url", + [ + ("get", "/api/zwave_js/dump/INVALID"), + ("post", "/api/zwave_js/firmware/upload/INVALID/1"), + ], +) +async def test_view_invalid_entry_id(integration, hass_client, method, url): """Test an invalid config entry id parameter.""" client = await hass_client() - resp = await client.get("/api/zwave_js/dump/INVALID") + resp = await client.request(method, url) assert resp.status == 400 +@pytest.mark.parametrize( + "method, url", [("post", "/api/zwave_js/firmware/upload/{}/111")] +) +async def test_view_invalid_node_id(integration, hass_client, method, url): + """Test an invalid config entry id parameter.""" + client = await hass_client() + resp = await client.request(method, url.format(integration.entry_id)) + assert resp.status == 404 + + async def test_subscribe_logs(hass, integration, client, hass_ws_client): """Test the subscribe_logs websocket command.""" entry = integration @@ -1082,3 +1710,293 @@ async def test_data_collection(hass, client, integration, hass_ws_client): assert not msg["success"] assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_abort_firmware_update( + hass, client, multisensor_6, integration, hass_ws_client +): + """Test that the abort_firmware_update WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command_no_wait.return_value = {} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + assert msg["success"] + + assert len(client.async_send_command_no_wait.call_args_list) == 1 + args = client.async_send_command_no_wait.call_args[0][0] + assert args["command"] == "node.abort_firmware_update" + assert args["nodeId"] == multisensor_6.node_id + + +async def test_abort_firmware_update_failures( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test failures for the abort_firmware_update websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: "fake_entry_id", + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with improper node ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id + 100, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/abort_firmware_update", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_subscribe_firmware_update_status( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test the subscribe_firmware_update_status websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + + client.async_send_command_no_wait.return_value = {} + + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + + msg = await ws_client.receive_json() + assert msg["success"] + + event = Event( + type="firmware update progress", + data={ + "source": "node", + "event": "firmware update progress", + "nodeId": multisensor_6.node_id, + "sentFragments": 1, + "totalFragments": 10, + }, + ) + multisensor_6.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "firmware update progress", + "sent_fragments": 1, + "total_fragments": 10, + } + + event = Event( + type="firmware update finished", + data={ + "source": "node", + "event": "firmware update finished", + "nodeId": multisensor_6.node_id, + "status": 255, + "waitTime": 10, + }, + ) + multisensor_6.receive_event(event) + + msg = await ws_client.receive_json() + assert msg["event"] == { + "event": "firmware update finished", + "status": 255, + "wait_time": 10, + } + + +async def test_subscribe_firmware_update_status_failures( + hass, integration, multisensor_6, client, hass_ws_client +): + """Test failures for the subscribe_firmware_update_status websocket command.""" + entry = integration + ws_client = await hass_ws_client(hass) + # Test sending command with improper entry ID fails + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: "fake_entry_id", + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with improper node ID fails + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id + 100, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/subscribe_firmware_update_status", + ENTRY_ID: entry.entry_id, + NODE_ID: multisensor_6.node_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + +async def test_check_for_config_updates(hass, client, integration, hass_ws_client): + """Test that the check_for_config_updates WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + # Test we can get log configuration + client.async_send_command.return_value = { + "updateAvailable": True, + "newVersion": "test", + } + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + config_update = msg["result"] + assert config_update["update_available"] + assert config_update["new_version"] == "test" + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/check_for_config_updates", + ENTRY_ID: "INVALID", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND + + +async def test_install_config_update(hass, client, integration, hass_ws_client): + """Test that the install_config_update WS API call works.""" + entry = integration + ws_client = await hass_ws_client(hass) + + # Test we can get log configuration + client.async_send_command.return_value = {"success": True} + await ws_client.send_json( + { + ID: 1, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + assert msg["result"] + assert msg["success"] + + # Test sending command with not loaded entry fails + await hass.config_entries.async_unload(entry.entry_id) + await hass.async_block_till_done() + + await ws_client.send_json( + { + ID: 2, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: entry.entry_id, + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_LOADED + + await ws_client.send_json( + { + ID: 3, + TYPE: "zwave_js/install_config_update", + ENTRY_ID: "INVALID", + } + ) + msg = await ws_client.receive_json() + + assert not msg["success"] + assert msg["error"]["code"] == ERR_NOT_FOUND diff --git a/tests/components/zwave_js/test_climate.py b/tests/components/zwave_js/test_climate.py index 8682ce98b5b..a1b86b14ebc 100644 --- a/tests/components/zwave_js/test_climate.py +++ b/tests/components/zwave_js/test_climate.py @@ -28,6 +28,7 @@ from homeassistant.components.climate.const import ( SERVICE_SET_PRESET_MODE, SERVICE_SET_TEMPERATURE, SUPPORT_FAN_MODE, + SUPPORT_PRESET_MODE, SUPPORT_TARGET_TEMPERATURE, SUPPORT_TARGET_TEMPERATURE_RANGE, ) @@ -436,8 +437,11 @@ async def test_setpoint_thermostat(hass, client, climate_danfoss_lc_13, integrat client.async_send_command_no_wait.reset_mock() -async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integration): - """Test a thermostat v2 command class entity.""" +async def test_thermostat_heatit_z_trm3( + hass, client, climate_heatit_z_trm3, integration +): + """Test a heatit Z-TRM3 entity.""" + node = climate_heatit_z_trm3 state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) assert state @@ -453,6 +457,99 @@ async def test_thermostat_heatit(hass, client, climate_heatit_z_trm3, integratio assert state.attributes[ATTR_MIN_TEMP] == 5 assert state.attributes[ATTR_MAX_TEMP] == 35 + # Try switching to external sensor + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 24, + "args": { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Sensor mode", + "newValue": 4, + "prevValue": 2, + }, + }, + ) + node.receive_event(event) + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 0 + + # Try switching to floor sensor + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 24, + "args": { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Sensor mode", + "newValue": 0, + "prevValue": 4, + }, + }, + ) + node.receive_event(event) + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 25.5 + + +async def test_thermostat_heatit_z_trm2fx( + hass, client, climate_heatit_z_trm2fx, integration +): + """Test a heatit Z-TRM2fx entity.""" + node = climate_heatit_z_trm2fx + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + + assert state + assert state.state == HVAC_MODE_HEAT + assert state.attributes[ATTR_HVAC_MODES] == [ + HVAC_MODE_OFF, + HVAC_MODE_HEAT, + HVAC_MODE_COOL, + ] + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 28.8 + assert state.attributes[ATTR_TEMPERATURE] == 29 + assert ( + state.attributes[ATTR_SUPPORTED_FEATURES] + == SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE + ) + assert state.attributes[ATTR_MIN_TEMP] == 7 + assert state.attributes[ATTR_MAX_TEMP] == 35 + + # Try switching to external sensor + event = Event( + type="value updated", + data={ + "source": "node", + "event": "value updated", + "nodeId": 24, + "args": { + "commandClassName": "Configuration", + "commandClass": 112, + "endpoint": 0, + "property": 2, + "propertyName": "Sensor mode", + "newValue": 4, + "prevValue": 2, + }, + }, + ) + node.receive_event(event) + state = hass.states.get(CLIMATE_FLOOR_THERMOSTAT_ENTITY) + assert state + assert state.attributes[ATTR_CURRENT_TEMPERATURE] == 0 + async def test_thermostat_srt321_hrt4_zw(hass, client, srt321_hrt4_zw, integration): """Test a climate entity from a HRT4-ZW / SRT321 thermostat device. diff --git a/tests/components/zwave_js/test_cover.py b/tests/components/zwave_js/test_cover.py index 2378453e31a..9d7a16ac8cf 100644 --- a/tests/components/zwave_js/test_cover.py +++ b/tests/components/zwave_js/test_cover.py @@ -3,7 +3,10 @@ from zwave_js_server.event import Event from homeassistant.components.cover import ( ATTR_CURRENT_POSITION, + DEVICE_CLASS_BLIND, DEVICE_CLASS_GARAGE, + DEVICE_CLASS_SHUTTER, + DEVICE_CLASS_WINDOW, DOMAIN, SERVICE_CLOSE_COVER, SERVICE_OPEN_COVER, @@ -19,6 +22,8 @@ from homeassistant.const import ( WINDOW_COVER_ENTITY = "cover.zws_12" GDC_COVER_ENTITY = "cover.aeon_labs_garage_door_controller_gen5" +BLIND_COVER_ENTITY = "cover.window_blind_controller" +SHUTTER_COVER_ENTITY = "cover.flush_shutter_dc" async def test_window_cover(hass, client, chain_actuator_zws12, integration): @@ -27,6 +32,8 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): state = hass.states.get(WINDOW_COVER_ENTITY) assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_WINDOW + assert state.state == "closed" assert state.attributes[ATTR_CURRENT_POSITION] == 0 @@ -299,6 +306,22 @@ async def test_window_cover(hass, client, chain_actuator_zws12, integration): assert state.state == "closed" +async def test_blind_cover(hass, client, iblinds_v2, integration): + """Test a blind cover entity.""" + state = hass.states.get(BLIND_COVER_ENTITY) + + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_BLIND + + +async def test_shutter_cover(hass, client, qubino_shutter, integration): + """Test a shutter cover entity.""" + state = hass.states.get(SHUTTER_COVER_ENTITY) + + assert state + assert state.attributes[ATTR_DEVICE_CLASS] == DEVICE_CLASS_SHUTTER + + async def test_motor_barrier_cover(hass, client, gdc_zw062, integration): """Test the cover entity.""" node = gdc_zw062 diff --git a/tests/components/zwave_js/test_discovery.py b/tests/components/zwave_js/test_discovery.py index cbc9d756292..8914019cd43 100644 --- a/tests/components/zwave_js/test_discovery.py +++ b/tests/components/zwave_js/test_discovery.py @@ -1,4 +1,11 @@ """Test discovery of entities for device-specific schemas for the Z-Wave JS integration.""" +import pytest + +from homeassistant.components.zwave_js.discovery import ( + FirmwareVersionRange, + ZWaveDiscoverySchema, + ZWaveValueDiscoverySchema, +) async def test_iblinds_v2(hass, client, iblinds_v2, integration): @@ -48,3 +55,13 @@ async def test_vision_security_zl7432( state = hass.states.get(entity_id) assert state assert state.attributes["assumed_state"] + + +async def test_firmware_version_range_exception(hass): + """Test FirmwareVersionRange exception.""" + with pytest.raises(ValueError): + ZWaveDiscoverySchema( + "test", + ZWaveValueDiscoverySchema(command_class=1), + firmware_version_range=FirmwareVersionRange(), + ) diff --git a/tests/components/zwave_js/test_init.py b/tests/components/zwave_js/test_init.py index f9784e0f9b8..67d5a416a91 100644 --- a/tests/components/zwave_js/test_init.py +++ b/tests/components/zwave_js/test_init.py @@ -9,13 +9,7 @@ from zwave_js_server.model.node import Node from homeassistant.components.hassio.handler import HassioAPIError from homeassistant.components.zwave_js.const import DOMAIN from homeassistant.components.zwave_js.helpers import get_device_id -from homeassistant.config_entries import ( - CONN_CLASS_LOCAL_PUSH, - DISABLED_USER, - ENTRY_STATE_LOADED, - ENTRY_STATE_NOT_LOADED, - ENTRY_STATE_SETUP_RETRY, -) +from homeassistant.config_entries import DISABLED_USER, ConfigEntryState from homeassistant.const import STATE_UNAVAILABLE from homeassistant.helpers import device_registry as dr, entity_registry as er @@ -40,12 +34,12 @@ async def test_entry_setup_unload(hass, client, integration): entry = integration assert client.connect.call_count == 1 - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_unload(entry.entry_id) assert client.disconnect.call_count == 1 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED async def test_home_assistant_stop(hass, client, integration): @@ -63,7 +57,7 @@ async def test_initialized_timeout(hass, client, connect_timeout): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY async def test_enabled_statistics(hass, client): @@ -131,13 +125,12 @@ async def test_listen_failure(hass, client, error): await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY -async def test_on_node_added_ready( - hass, multisensor_6_state, client, integration, device_registry -): +async def test_on_node_added_ready(hass, multisensor_6_state, client, integration): """Test we handle a ready node added event.""" + dev_reg = dr.async_get(hass) node = Node(client, multisensor_6_state) event = {"node": node} air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" @@ -145,7 +138,7 @@ async def test_on_node_added_ready( state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity and device not yet added - assert not device_registry.async_get_device( + assert not dev_reg.async_get_device( identifiers={(DOMAIN, air_temperature_device_id)} ) @@ -156,9 +149,7 @@ async def test_on_node_added_ready( assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) async def test_unique_id_migration_dupes( @@ -479,10 +470,9 @@ async def test_old_entity_migration_notification_binary_sensor( ) -async def test_on_node_added_not_ready( - hass, multisensor_6_state, client, integration, device_registry -): +async def test_on_node_added_not_ready(hass, multisensor_6_state, client, integration): """Test we handle a non ready node added event.""" + dev_reg = dr.async_get(hass) node_data = deepcopy(multisensor_6_state) # Copy to allow modification in tests. node = Node(client, node_data) node.data["ready"] = False @@ -492,7 +482,7 @@ async def test_on_node_added_not_ready( state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity and device not yet added - assert not device_registry.async_get_device( + assert not dev_reg.async_get_device( identifiers={(DOMAIN, air_temperature_device_id)} ) @@ -502,9 +492,7 @@ async def test_on_node_added_not_ready( state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity not yet added but device added in registry - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) node.data["ready"] = True node.emit("ready", event) @@ -516,10 +504,9 @@ async def test_on_node_added_not_ready( assert state.state != STATE_UNAVAILABLE -async def test_existing_node_ready( - hass, client, multisensor_6, integration, device_registry -): +async def test_existing_node_ready(hass, client, multisensor_6, integration): """Test we handle a ready node that exists during integration setup.""" + dev_reg = dr.async_get(hass) node = multisensor_6 air_temperature_device_id = f"{client.driver.controller.home_id}-{node.node_id}" @@ -527,9 +514,7 @@ async def test_existing_node_ready( assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) async def test_null_name(hass, client, null_name_check, integration): @@ -538,8 +523,9 @@ async def test_null_name(hass, client, null_name_check, integration): assert hass.states.get(f"switch.node_{node.node_id}") -async def test_existing_node_not_ready(hass, client, multisensor_6, device_registry): +async def test_existing_node_not_ready(hass, client, multisensor_6): """Test we handle a non ready node that exists during integration setup.""" + dev_reg = dr.async_get(hass) node = multisensor_6 node.data = deepcopy(node.data) # Copy to allow modification in tests. node.data["ready"] = False @@ -554,7 +540,7 @@ async def test_existing_node_not_ready(hass, client, multisensor_6, device_regis state = hass.states.get(AIR_TEMPERATURE_SENSOR) assert not state # entity not yet added - assert device_registry.async_get_device( # device should be added + assert dev_reg.async_get_device( # device should be added identifiers={(DOMAIN, air_temperature_device_id)} ) @@ -566,9 +552,7 @@ async def test_existing_node_not_ready(hass, client, multisensor_6, device_regis assert state # entity and device added assert state.state != STATE_UNAVAILABLE - assert device_registry.async_get_device( - identifiers={(DOMAIN, air_temperature_device_id)} - ) + assert dev_reg.async_get_device(identifiers={(DOMAIN, air_temperature_device_id)}) async def test_start_addon( @@ -584,7 +568,6 @@ async def test_start_addon( entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={"use_addon": True, "usb_path": device, "network_key": network_key}, ) entry.add_to_hass(hass) @@ -592,7 +575,7 @@ async def test_start_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert install_addon.call_count == 0 assert set_addon_options.call_count == 1 assert set_addon_options.call_args == call( @@ -616,7 +599,6 @@ async def test_install_addon( entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={"use_addon": True, "usb_path": device, "network_key": network_key}, ) entry.add_to_hass(hass) @@ -624,7 +606,7 @@ async def test_install_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert install_addon.call_count == 1 assert install_addon.call_args == call(hass, "core_zwave_js") assert set_addon_options.call_count == 1 @@ -650,7 +632,6 @@ async def test_addon_info_failure( entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={"use_addon": True, "usb_path": device, "network_key": network_key}, ) entry.add_to_hass(hass) @@ -658,7 +639,49 @@ async def test_addon_info_failure( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY + assert install_addon.call_count == 0 + assert start_addon.call_count == 0 + + +@pytest.mark.parametrize( + "old_device, new_device, old_network_key, new_network_key", + [("/old_test", "/new_test", "old123", "new123")], +) +async def test_addon_options_changed( + hass, + client, + addon_installed, + addon_running, + install_addon, + addon_options, + start_addon, + old_device, + new_device, + old_network_key, + new_network_key, +): + """Test update config entry data on entry setup if add-on options changed.""" + addon_options["device"] = new_device + addon_options["network_key"] = new_network_key + entry = MockConfigEntry( + domain=DOMAIN, + title="Z-Wave JS", + data={ + "url": "ws://host1:3001", + "use_addon": True, + "usb_path": old_device, + "network_key": old_network_key, + }, + ) + entry.add_to_hass(hass) + + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state == ConfigEntryState.LOADED + assert entry.data["usb_path"] == new_device + assert entry.data["network_key"] == new_network_key assert install_addon.call_count == 0 assert start_addon.call_count == 0 @@ -690,17 +713,18 @@ async def test_update_addon( create_shapshot_side_effect, ): """Test update the Z-Wave JS add-on during entry setup.""" + device = "/test" + network_key = "abc123" + addon_options["device"] = device + addon_options["network_key"] = network_key addon_info.return_value["version"] = addon_version addon_info.return_value["update_available"] = update_available create_shapshot.side_effect = create_shapshot_side_effect update_addon.side_effect = update_addon_side_effect client.connect.side_effect = InvalidServerVersion("Invalid version") - device = "/test" - network_key = "abc123" entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={ "url": "ws://host1:3001", "use_addon": True, @@ -713,7 +737,7 @@ async def test_update_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_SETUP_RETRY + assert entry.state is ConfigEntryState.SETUP_RETRY assert create_shapshot.call_count == snapshot_calls assert update_addon.call_count == update_calls @@ -721,8 +745,8 @@ async def test_update_addon( @pytest.mark.parametrize( "stop_addon_side_effect, entry_state", [ - (None, ENTRY_STATE_NOT_LOADED), - (HassioAPIError("Boom"), ENTRY_STATE_LOADED), + (None, ConfigEntryState.NOT_LOADED), + (HassioAPIError("Boom"), ConfigEntryState.LOADED), ], ) async def test_stop_addon( @@ -739,10 +763,11 @@ async def test_stop_addon( stop_addon.side_effect = stop_addon_side_effect device = "/test" network_key = "abc123" + addon_options["device"] = device + addon_options["network_key"] = network_key entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={ "url": "ws://host1:3001", "use_addon": True, @@ -755,7 +780,7 @@ async def test_stop_addon( await hass.config_entries.async_setup(entry.entry_id) await hass.async_block_till_done() - assert entry.state == ENTRY_STATE_LOADED + assert entry.state is ConfigEntryState.LOADED await hass.config_entries.async_set_disabled_by(entry.entry_id, DISABLED_USER) await hass.async_block_till_done() @@ -773,23 +798,21 @@ async def test_remove_entry( entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={"integration_created_addon": False}, ) entry.add_to_hass(hass) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 1 await hass.config_entries.async_remove(entry.entry_id) - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 # test successful remove with created add-on entry = MockConfigEntry( domain=DOMAIN, title="Z-Wave JS", - connection_class=CONN_CLASS_LOCAL_PUSH, data={"integration_created_addon": True}, ) entry.add_to_hass(hass) @@ -807,7 +830,7 @@ async def test_remove_entry( ) assert uninstall_addon.call_count == 1 assert uninstall_addon.call_args == call(hass, "core_zwave_js") - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 stop_addon.reset_mock() create_shapshot.reset_mock() @@ -824,7 +847,7 @@ async def test_remove_entry( assert stop_addon.call_args == call(hass, "core_zwave_js") assert create_shapshot.call_count == 0 assert uninstall_addon.call_count == 0 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to stop the Z-Wave JS add-on" in caplog.text stop_addon.side_effect = None @@ -848,7 +871,7 @@ async def test_remove_entry( partial=True, ) assert uninstall_addon.call_count == 0 - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to create a snapshot of the Z-Wave JS add-on" in caplog.text create_shapshot.side_effect = None @@ -873,7 +896,7 @@ async def test_remove_entry( ) assert uninstall_addon.call_count == 1 assert uninstall_addon.call_args == call(hass, "core_zwave_js") - assert entry.state == ENTRY_STATE_NOT_LOADED + assert entry.state is ConfigEntryState.NOT_LOADED assert len(hass.config_entries.async_entries(DOMAIN)) == 0 assert "Failed to uninstall the Z-Wave JS add-on" in caplog.text diff --git a/tests/components/zwave_js/test_light.py b/tests/components/zwave_js/test_light.py index cf45f2b9a79..2d9cf06b095 100644 --- a/tests/components/zwave_js/test_light.py +++ b/tests/components/zwave_js/test_light.py @@ -5,11 +5,14 @@ from zwave_js_server.event import Event from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, - ATTR_WHITE_VALUE, + ATTR_RGBW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, + SUPPORT_TRANSITION, ) from homeassistant.const import ATTR_SUPPORTED_FEATURES, STATE_OFF, STATE_ON @@ -30,7 +33,8 @@ async def test_light(hass, client, bulb_6_multi_color, integration): assert state.state == STATE_OFF assert state.attributes[ATTR_MIN_MIREDS] == 153 assert state.attributes[ATTR_MAX_MIREDS] == 370 - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 51 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TRANSITION + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp", "hs"] # Test turning on await hass.services.async_call( @@ -86,8 +90,10 @@ async def test_light(hass, client, bulb_6_multi_color, integration): state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" assert state.attributes[ATTR_BRIGHTNESS] == 255 assert state.attributes[ATTR_COLOR_TEMP] == 370 + assert ATTR_RGB_COLOR not in state.attributes # Test turning on with same brightness await hass.services.async_call( @@ -231,8 +237,10 @@ async def test_light(hass, client, bulb_6_multi_color, integration): state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_BRIGHTNESS] == 255 assert state.attributes[ATTR_RGB_COLOR] == (255, 76, 255) + assert ATTR_COLOR_TEMP not in state.attributes client.async_send_command.reset_mock() @@ -352,9 +360,10 @@ async def test_light(hass, client, bulb_6_multi_color, integration): state = hass.states.get(BULB_6_MULTI_COLOR_LIGHT_ENTITY) assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" assert state.attributes[ATTR_BRIGHTNESS] == 255 assert state.attributes[ATTR_COLOR_TEMP] == 170 - assert state.attributes[ATTR_RGB_COLOR] == (255, 255, 255) + assert ATTR_RGB_COLOR not in state.attributes # Test turning on with same color temp await hass.services.async_call( @@ -415,20 +424,20 @@ async def test_optional_light(hass, client, aeon_smart_switch_6, integration): assert state.state == STATE_ON -async def test_white_value_light(hass, client, zen_31, integration): +async def test_rgbw_light(hass, client, zen_31, integration): """Test the light entity.""" zen_31 state = hass.states.get(ZEN_31_ENTITY) assert state assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 177 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TRANSITION # Test turning on await hass.services.async_call( "light", "turn_on", - {"entity_id": ZEN_31_ENTITY, ATTR_WHITE_VALUE: 128}, + {"entity_id": ZEN_31_ENTITY, ATTR_RGBW_COLOR: (0, 0, 0, 128)}, blocking=True, ) @@ -451,7 +460,7 @@ async def test_white_value_light(hass, client, zen_31, integration): }, "value": {"blue": 70, "green": 159, "red": 255, "warmWhite": 141}, } - assert args["value"] == {"warmWhite": 128} + assert args["value"] == {"blue": 0, "green": 0, "red": 0, "warmWhite": 128} args = client.async_send_command.call_args_list[1][0][0] assert args["command"] == "node.set_value" @@ -477,3 +486,14 @@ async def test_white_value_light(hass, client, zen_31, integration): assert args["value"] == 255 client.async_send_command.reset_mock() + + +async def test_light_none_color_value(hass, light_color_null_values, integration): + """Test the light entity can handle None value in current color Value.""" + entity_id = "light.repeater" + state = hass.states.get(entity_id) + + assert state + assert state.state == STATE_ON + assert state.attributes[ATTR_SUPPORTED_FEATURES] == SUPPORT_TRANSITION + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] diff --git a/tests/components/zwave_js/test_services.py b/tests/components/zwave_js/test_services.py index 956361d3953..3c08c49a36f 100644 --- a/tests/components/zwave_js/test_services.py +++ b/tests/components/zwave_js/test_services.py @@ -599,6 +599,7 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): ) assert len(client.async_send_command.call_args_list) == 1 + args = client.async_send_command.call_args[0][0] assert args["command"] == "node.set_value" assert args["nodeId"] == 5 @@ -619,3 +620,17 @@ async def test_set_value(hass, client, climate_danfoss_lc_13, integration): "value": 0, } assert args["value"] == 2 + + # Test missing device and entities keys + with pytest.raises(vol.MultipleInvalid): + await hass.services.async_call( + DOMAIN, + SERVICE_SET_VALUE, + { + ATTR_COMMAND_CLASS: 117, + ATTR_PROPERTY: "local", + ATTR_VALUE: 2, + ATTR_WAIT_FOR_RESULT: True, + }, + blocking=True, + ) diff --git a/tests/conftest.py b/tests/conftest.py index 3fc2dc748cb..1f5ffc80d0d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from homeassistant import core as ha, loader, runner, util from homeassistant.auth.const import GROUP_ID_ADMIN, GROUP_ID_READ_ONLY from homeassistant.auth.models import Credentials from homeassistant.auth.providers import homeassistant, legacy_api_password -from homeassistant.components import mqtt +from homeassistant.components import mqtt, recorder from homeassistant.components.websocket_api.auth import ( TYPE_AUTH, TYPE_AUTH_OK, @@ -39,6 +39,8 @@ from tests.common import ( # noqa: E402, isort:skip MockUser, async_fire_mqtt_message, async_test_home_assistant, + get_test_home_assistant, + init_recorder_component, mock_storage as mock_storage, ) from tests.test_util.aiohttp import mock_aiohttp_client # noqa: E402, isort:skip @@ -478,7 +480,7 @@ async def mqtt_mock(hass, mqtt_client_mock, mqtt_config): @pytest.fixture def mock_zeroconf(): """Mock zeroconf.""" - with patch("homeassistant.components.zeroconf.HaZeroconf") as mock_zc: + with patch("homeassistant.components.zeroconf.models.HaZeroconf") as mock_zc: yield mock_zc.return_value @@ -595,3 +597,36 @@ def legacy_patchable_time(): def enable_custom_integrations(hass): """Enable custom integrations defined in the test dir.""" hass.data.pop(loader.DATA_CUSTOM_COMPONENTS) + + +@pytest.fixture +def enable_statistics(): + """Fixture to control enabling of recorder's statistics compilation. + + To enable statistics, tests can be marked with: + @pytest.mark.parametrize("enable_statistics", [True]) + """ + return False + + +@pytest.fixture +def hass_recorder(enable_statistics): + """Home Assistant fixture with in-memory recorder.""" + hass = get_test_home_assistant() + stats = recorder.Recorder.async_hourly_statistics if enable_statistics else None + with patch( + "homeassistant.components.recorder.Recorder.async_hourly_statistics", + side_effect=stats, + autospec=True, + ): + + def setup_recorder(config=None): + """Set up with params.""" + init_recorder_component(hass, config) + hass.start() + hass.block_till_done() + hass.data[recorder.DATA_INSTANCE].block_till_done() + return hass + + yield setup_recorder + hass.stop() diff --git a/tests/fixtures/elgato/settings-color.json b/tests/fixtures/elgato/settings-color.json new file mode 100644 index 00000000000..14a78c6fcaf --- /dev/null +++ b/tests/fixtures/elgato/settings-color.json @@ -0,0 +1,10 @@ +{ + "powerOnBehavior": 2, + "powerOnHue": 40.0, + "powerOnSaturation": 15.0, + "powerOnBrightness": 40, + "powerOnTemperature": 0, + "switchOnDurationMs": 150, + "switchOffDurationMs": 400, + "colorChangeDurationMs": 150 +} diff --git a/tests/fixtures/elgato/settings.json b/tests/fixtures/elgato/settings.json new file mode 100644 index 00000000000..bd918e24526 --- /dev/null +++ b/tests/fixtures/elgato/settings.json @@ -0,0 +1,8 @@ +{ + "powerOnBehavior": 1, + "powerOnBrightness": 20, + "powerOnTemperature": 213, + "switchOnDurationMs": 100, + "switchOffDurationMs": 300, + "colorChangeDurationMs": 100 +} diff --git a/tests/fixtures/elgato/state-color.json b/tests/fixtures/elgato/state-color.json new file mode 100644 index 00000000000..b49a6f6dd80 --- /dev/null +++ b/tests/fixtures/elgato/state-color.json @@ -0,0 +1,11 @@ +{ + "numberOfLights": 1, + "lights": [ + { + "on": 1, + "hue": 358.0, + "saturation": 6.0, + "brightness": 50 + } + ] +} diff --git a/tests/fixtures/gios/sensors.json b/tests/fixtures/gios/sensors.json index 3103a2bf16e..62732552172 100644 --- a/tests/fixtures/gios/sensors.json +++ b/tests/fixtures/gios/sensors.json @@ -1,47 +1,47 @@ { - "SO2": { + "so2": { "values": [ { "date": "2020-07-31 15:00:00", "value": 4.35478 }, { "date": "2020-07-31 14:00:00", "value": 4.25478 }, { "date": "2020-07-31 13:00:00", "value": 4.34309 } ] }, - "C6H6": { + "c6h6": { "values": [ { "date": "2020-07-31 15:00:00", "value": 0.23789 }, { "date": "2020-07-31 14:00:00", "value": 0.22789 }, { "date": "2020-07-31 13:00:00", "value": 0.21315 } ] }, - "CO": { + "co": { "values": [ { "date": "2020-07-31 15:00:00", "value": 251.874 }, { "date": "2020-07-31 14:00:00", "value": 250.874 }, { "date": "2020-07-31 13:00:00", "value": 251.097 } ] }, - "NO2": { + "no2": { "values": [ { "date": "2020-07-31 15:00:00", "value": 7.13411 }, { "date": "2020-07-31 14:00:00", "value": 7.33411 }, { "date": "2020-07-31 13:00:00", "value": 9.32578 } ] }, - "O3": { + "o3": { "values": [ { "date": "2020-07-31 15:00:00", "value": 95.7768 }, { "date": "2020-07-31 14:00:00", "value": 93.7768 }, { "date": "2020-07-31 13:00:00", "value": 89.4232 } ] }, - "PM2.5": { + "pm2.5": { "values": [ { "date": "2020-07-31 15:00:00", "value": 4 }, { "date": "2020-07-31 14:00:00", "value": 4 }, { "date": "2020-07-31 13:00:00", "value": 5 } ] }, - "PM10": { + "pm10": { "values": [ { "date": "2020-07-31 15:00:00", "value": 16.8344 }, { "date": "2020-07-31 14:00:00", "value": 17.8344 }, diff --git a/tests/fixtures/homekit_controller/haa_fan.json b/tests/fixtures/homekit_controller/haa_fan.json new file mode 100644 index 00000000000..b06ccdf9644 --- /dev/null +++ b/tests/fixtures/homekit_controller/haa_fan.json @@ -0,0 +1,257 @@ +[ + { + "aid": 1, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 2, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 1, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "C718B3-1" + }, + { + "aid": 2, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 2, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 2, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "format": "bool" + } + ] + }, + { + "iid": 8, + "type": "00000040-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 1, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "bool", + "value": false + }, + { + "aid": 1, + "iid": 10, + "type": "00000029-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "float", + "unit": "percentage", + "minValue": 0, + "maxValue": 3, + "minStep": 1, + "value": 3 + } + ] + }, + { + "iid": 1000, + "type": "000000A2-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": true, + "characteristics": [ + { + "aid": 1, + "iid": 1001, + "type": "00000037-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "1.1.0" + } + ] + }, + { + "iid": 1010, + "type": "F0000100-0218-2017-81BF-AF2B7C833922", + "primary": false, + "hidden": true, + "characteristics": [ + { + "aid": 1, + "iid": 1011, + "type": "F0000101-0218-2017-81BF-AF2B7C833922", + "perms": [ + "pr", + "pw", + "hd" + ], + "description": "Update", + "format": "string", + "value": "" + }, + { + "aid": 1, + "iid": 1012, + "type": "F0000102-0218-2017-81BF-AF2B7C833922", + "perms": [ + "pr", + "pw", + "hd" + ], + "description": "Setup", + "format": "string", + "value": "" + } + ] + } + ] + }, + { + "aid": 2, + "services": [ + { + "iid": 1, + "type": "0000003E-0000-1000-8000-0026BB765291", + "primary": false, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 2, + "type": "00000023-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "HAA-C718B3" + }, + { + "aid": 2, + "iid": 3, + "type": "00000020-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "Jos\u00e9 A. Jim\u00e9nez Campos" + }, + { + "aid": 2, + "iid": 4, + "type": "00000030-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "C718B3-2" + }, + { + "aid": 2, + "iid": 5, + "type": "00000021-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "RavenSystem HAA" + }, + { + "aid": 2, + "iid": 6, + "type": "00000052-0000-1000-8000-0026BB765291", + "perms": [ + "pr" + ], + "format": "string", + "value": "5.0.18" + }, + { + "aid": 2, + "iid": 7, + "type": "00000014-0000-1000-8000-0026BB765291", + "perms": [ + "pw" + ], + "format": "bool" + } + ] + }, + { + "iid": 8, + "type": "00000049-0000-1000-8000-0026BB765291", + "primary": true, + "hidden": false, + "characteristics": [ + { + "aid": 2, + "iid": 9, + "type": "00000025-0000-1000-8000-0026BB765291", + "perms": [ + "pr", + "pw", + "ev" + ], + "ev": true, + "format": "bool", + "value": false + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/fixtures/homekit_controller/netamo_doorbell.json b/tests/fixtures/homekit_controller/netamo_doorbell.json new file mode 100644 index 00000000000..450b419f30d --- /dev/null +++ b/tests/fixtures/homekit_controller/netamo_doorbell.json @@ -0,0 +1,341 @@ +[ + { + "aid" : 1, + "services" : [ + { + "hidden" : true, + "iid" : 53, + "characteristics" : [ + { + "format" : "bool", + "iid" : 54, + "perms" : [ + "pw" + ], + "type" : "4D05AE82-5A22-5BD6-A730-B7F8B4F3218D" + }, + { + "value" : "g738658", + "format" : "string", + "type" : "00F44C18-042E-5C4E-9A4C-561D44DCD804", + "perms" : [ + "pr" + ], + "iid" : 55 + } + ], + "primary" : false, + "type" : "EA22EA53-6227-55EA-AC24-73ACF3EEA0E8" + }, + { + "type" : "0000003E-0000-1000-8000-0026BB765291", + "primary" : false, + "iid" : 1, + "characteristics" : [ + { + "format" : "string", + "value" : "Netatmo-Doorbell-g738658", + "iid" : 2, + "perms" : [ + "pr" + ], + "type" : "00000023-0000-1000-8000-0026BB765291" + }, + { + "iid" : 3, + "type" : "00000020-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "value" : "Netatmo", + "format" : "string" + }, + { + "format" : "string", + "value" : "Netatmo Doorbell", + "perms" : [ + "pr" + ], + "type" : "00000021-0000-1000-8000-0026BB765291", + "iid" : 4 + }, + { + "format" : "string", + "value" : "g738658", + "perms" : [ + "pr" + ], + "type" : "00000030-0000-1000-8000-0026BB765291", + "iid" : 5 + }, + { + "iid" : 6, + "perms" : [ + "pr" + ], + "type" : "00000052-0000-1000-8000-0026BB765291", + "format" : "string", + "value" : "80.0.0" + }, + { + "type" : "00000014-0000-1000-8000-0026BB765291", + "perms" : [ + "pw" + ], + "iid" : 7, + "format" : "bool" + }, + { + "value" : "+nvrOo7/HvM=", + "format" : "data", + "iid" : 56, + "type" : "220", + "perms" : [ + "pr" + ] + } + ], + "hidden" : false + }, + { + "hidden" : false, + "iid" : 29, + "characteristics" : [ + { + "format" : "string", + "value" : "1.1.0", + "perms" : [ + "pr" + ], + "type" : "00000037-0000-1000-8000-0026BB765291", + "iid" : 30 + } + ], + "type" : "000000A2-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "type" : "00000121-0000-1000-8000-0026BB765291", + "primary" : true, + "characteristics" : [ + { + "value" : null, + "format" : "uint8", + "type" : "00000073-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "ev" + ], + "iid" : 50 + }, + { + "value" : "Doorbell", + "format" : "string", + "type" : "00000023-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "iid" : 57 + } + ], + "iid" : 49, + "hidden" : false + }, + { + "hidden" : false, + "iid" : 51, + "characteristics" : [ + { + "value" : false, + "format" : "bool", + "type" : "0000011A-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw", + "ev" + ], + "iid" : 52 + } + ], + "type" : "00000113-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "hidden" : false, + "characteristics" : [ + { + "value" : false, + "format" : "bool", + "iid" : 9, + "type" : "0000011A-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw", + "ev" + ] + } + ], + "iid" : 8, + "type" : "00000112-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "hidden" : false, + "iid" : 10, + "characteristics" : [ + { + "iid" : 11, + "type" : "00000022-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "ev" + ], + "value" : false, + "format" : "bool" + }, + { + "perms" : [ + "pr" + ], + "type" : "00000023-0000-1000-8000-0026BB765291", + "iid" : 12, + "format" : "string", + "value" : "Motion Sensor" + } + ], + "type" : "00000085-0000-1000-8000-0026BB765291", + "primary" : false + }, + { + "primary" : false, + "type" : "00000110-0000-1000-8000-0026BB765291", + "characteristics" : [ + { + "format" : "tlv8", + "value" : "AQEA", + "perms" : [ + "pr", + "ev" + ], + "type" : "00000120-0000-1000-8000-0026BB765291", + "iid" : 14 + }, + { + "iid" : 15, + "type" : "00000114-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "value" : "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", + "format" : "tlv8" + }, + { + "iid" : 16, + "type" : "00000115-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "value" : "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", + "format" : "tlv8" + }, + { + "value" : "AgECAAACAQAAAAIBAQ==", + "format" : "tlv8", + "type" : "00000116-0000-1000-8000-0026BB765291", + "perms" : [ + "pr" + ], + "iid" : 17 + }, + { + "iid" : 19, + "type" : "00000117-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw" + ], + "value" : "AQA=", + "format" : "tlv8" + }, + { + "iid" : 18, + "perms" : [ + "pr", + "pw" + ], + "type" : "00000118-0000-1000-8000-0026BB765291", + "format" : "tlv8", + "value" : "ARDpyds+onxNHb4xI0H6deS3AgEAAxgBAQACCzEwLjEwLjYwLjExAwJRwwQCUsMENQEBAQIgyWEU3zQuPNAsFAm1DM3ZSdp0Vh7kGuVQ+vqtS5Qa09YDDr5ebeow7eweCsu3FYh/BTUBAQECIFPPdRRI86ozZNB/WU/e8Em4N1lSsJhttOWoJly3XNEMAw7Zm8TgFdAof+wvoCQTYgYE/r0D9wcEC1Pwjg==" + } + ], + "iid" : 13, + "hidden" : false + }, + { + "iid" : 20, + "characteristics" : [ + { + "iid" : 21, + "type" : "00000120-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "ev" + ], + "value" : "AQEA", + "format" : "tlv8" + }, + { + "format" : "tlv8", + "value" : "AVUBAQACFgEBAQAAAQECAgEAAAACAQIDAQAEAQADCwECgAcCAjgEAwEeAAADCwECAAUCAtACAwEeAAADCwECgAICAmgBAwEeAAADCwECQAECAvAAAwEe", + "perms" : [ + "pr" + ], + "type" : "00000114-0000-1000-8000-0026BB765291", + "iid" : 22 + }, + { + "format" : "tlv8", + "value" : "ARMBAQMCDgEBAQIBAQAAAgEAAwEBAgEA", + "iid" : 23, + "perms" : [ + "pr" + ], + "type" : "00000115-0000-1000-8000-0026BB765291" + }, + { + "format" : "tlv8", + "value" : "AgECAAACAQAAAAIBAQ==", + "perms" : [ + "pr" + ], + "type" : "00000116-0000-1000-8000-0026BB765291", + "iid" : 24 + }, + { + "format" : "tlv8", + "value" : "AQA=", + "perms" : [ + "pr", + "pw" + ], + "type" : "00000117-0000-1000-8000-0026BB765291", + "iid" : 26 + }, + { + "iid" : 25, + "type" : "00000118-0000-1000-8000-0026BB765291", + "perms" : [ + "pr", + "pw" + ], + "value" : "ARDu4+F49fZMSatQjcfR8FGVAgEAAxgBAQACCzEwLjEwLjYwLjExAwJbwwQCXMMENQEBAQIg9nqVm+80ccYh/S3vKKfbcUGH7VgggHRwp1e1x63+kpkDDnAxnJxfEz8KDp6xKoPhBTUBAQECILLYad+aKdzVbhGz55ywh0RYX9DTyY7HdSRf8y8tUi1kAw4DRngrGhYBdnrjELUzGgYEf+ysuwcESU05wg==", + "format" : "tlv8" + } + ], + "primary" : false, + "type" : "00000110-0000-1000-8000-0026BB765291", + "hidden" : false + } + ] + } +] diff --git a/tests/fixtures/hunterdouglas_powerview/fwversion.json b/tests/fixtures/hunterdouglas_powerview/fwversion.json new file mode 100644 index 00000000000..96d301802ff --- /dev/null +++ b/tests/fixtures/hunterdouglas_powerview/fwversion.json @@ -0,0 +1,10 @@ +{ + "firmware": { + "mainProcessor": { + "name": "PowerView Hub", + "revision": 1, + "subRevision": 1, + "build": 857 + } + } + } \ No newline at end of file diff --git a/tests/fixtures/ip-api.com.json b/tests/fixtures/ip-api.com.json deleted file mode 100644 index d31d4560589..00000000000 --- a/tests/fixtures/ip-api.com.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "as": "AS20001 Time Warner Cable Internet LLC", - "city": "San Diego", - "country": "United States", - "countryCode": "US", - "isp": "Time Warner Cable", - "lat": 32.8594, - "lon": -117.2073, - "org": "Time Warner Cable", - "query": "1.2.3.4", - "region": "CA", - "regionName": "California", - "status": "success", - "timezone": "America\/Los_Angeles", - "zip": "92122" -} diff --git a/tests/fixtures/ipapi.co.json b/tests/fixtures/ipapi.co.json deleted file mode 100644 index f1dc58a756b..00000000000 --- a/tests/fixtures/ipapi.co.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "ip": "1.2.3.4", - "city": "Bern", - "region": "Bern", - "region_code": "BE", - "country": "CH", - "country_name": "Switzerland", - "continent_code": "EU", - "in_eu": false, - "postal": "3000", - "latitude": 46.9480278, - "longitude": 7.4490812, - "timezone": "Europe/Zurich", - "utc_offset": "+0100", - "country_calling_code": "+41", - "currency": "CHF", - "languages": "de-CH,fr-CH,it-CH,rm", - "asn": "AS6830", - "org": "Liberty Global B.V." -} \ No newline at end of file diff --git a/tests/fixtures/netatmo/homestatus.json b/tests/fixtures/netatmo/homestatus.json index 5d508ea03b0..490bf999045 100644 --- a/tests/fixtures/netatmo/homestatus.json +++ b/tests/fixtures/netatmo/homestatus.json @@ -27,7 +27,6 @@ "type": "NATherm1", "firmware_revision": 65, "rf_strength": 58, - "battery_level": 3793, "boiler_valve_comfort_boost": false, "boiler_status": false, "anticipating": false, @@ -40,7 +39,6 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 51, - "battery_level": 3025, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" }, @@ -50,7 +48,6 @@ "type": "NRV", "firmware_revision": 79, "rf_strength": 59, - "battery_level": 2329, "bridge": "12:34:56:00:fa:d0", "battery_state": "full" } diff --git a/tests/fixtures/venstar/colortouch_info.json b/tests/fixtures/venstar/colortouch_info.json new file mode 100644 index 00000000000..44812beb762 --- /dev/null +++ b/tests/fixtures/venstar/colortouch_info.json @@ -0,0 +1 @@ +{"name":"COLORTOUCH","mode":1,"state":0,"fan":0,"fanstate":0,"tempunits":0,"schedule":0,"schedulepart":255,"away":0,"spacetemp":71.0,"heattemp":69.0,"cooltemp":74.0,"cooltempmin":35.0,"cooltempmax":99.0,"heattempmin":35.00,"heattempmax":99.0,"activestage":0,"hum_active":0,"hum":41,"hum_setpoint":30,"dehum_setpoint":99,"setpointdelta":2.0,"availablemodes":0} diff --git a/tests/fixtures/venstar/colortouch_root.json b/tests/fixtures/venstar/colortouch_root.json new file mode 100644 index 00000000000..820f88210b6 --- /dev/null +++ b/tests/fixtures/venstar/colortouch_root.json @@ -0,0 +1 @@ +{"api_ver":7,"type":"residential","model":"COLORTOUCH","firmware":"5.1"} \ No newline at end of file diff --git a/tests/fixtures/venstar/colortouch_sensors.json b/tests/fixtures/venstar/colortouch_sensors.json new file mode 100644 index 00000000000..a1ba04753b8 --- /dev/null +++ b/tests/fixtures/venstar/colortouch_sensors.json @@ -0,0 +1 @@ +{"sensors":[{"name":"Thermostat","temp":70.0,"hum":41},{"name":"Space Temp","temp":70.0}]} diff --git a/tests/fixtures/venstar/t2k_info.json b/tests/fixtures/venstar/t2k_info.json new file mode 100644 index 00000000000..81398dad391 --- /dev/null +++ b/tests/fixtures/venstar/t2k_info.json @@ -0,0 +1 @@ +{"name":"T2000","mode":0,"state":0,"activestage":0,"fan":0,"fanstate":0,"tempunits":0,"schedule":0,"schedulepart":1,"away":0,"spacetemp":14.5,"heattemp":10.0,"cooltemp":29.5,"cooltempmin":2.0,"cooltempmax":37.0,"heattempmin":2.0,"heattempmax":37.0,"setpointdelta":2,"availablemodes":2} diff --git a/tests/fixtures/venstar/t2k_root.json b/tests/fixtures/venstar/t2k_root.json new file mode 100644 index 00000000000..5f7449181a6 --- /dev/null +++ b/tests/fixtures/venstar/t2k_root.json @@ -0,0 +1 @@ +{"api_ver": 7, "type": "residential", "model": "T2000", "firmware": "4.38"} \ No newline at end of file diff --git a/tests/fixtures/venstar/t2k_sensors.json b/tests/fixtures/venstar/t2k_sensors.json new file mode 100644 index 00000000000..120b5820088 --- /dev/null +++ b/tests/fixtures/venstar/t2k_sensors.json @@ -0,0 +1 @@ +{"sensors": [{"name":"Thermostat","temp":14},{"name":"Space Temp","temp":14}]} \ No newline at end of file diff --git a/tests/fixtures/whoami.json b/tests/fixtures/whoami.json new file mode 100644 index 00000000000..c805ef30558 --- /dev/null +++ b/tests/fixtures/whoami.json @@ -0,0 +1,14 @@ +{ + "ip": "1.2.3.4", + "city": "Gotham", + "continent": "Earth", + "country": "XX", + "latitude": "12.34567", + "longitude": "12.34567", + "postal_code": "12345", + "region_code": "00", + "region": "Gotham", + "timezone": "Earth/Gotham", + "iso_time": "2021-05-12T11:29:15.752Z", + "timestamp": 1620818956 +} \ No newline at end of file diff --git a/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json b/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json new file mode 100644 index 00000000000..2526e346a53 --- /dev/null +++ b/tests/fixtures/zwave_js/climate_heatit_z_trm2fx_state.json @@ -0,0 +1,1444 @@ +{ + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": false, + "manufacturerId": 411, + "productId": 514, + "productType": 3, + "firmwareVersion": "3.6", + "zwavePlusVersion": 1, + "deviceConfig": { + "filename": "/usr/src/node_modules/@zwave-js/config/config/devices/0x019b/z-trm2fx_3.0.json", + "manufacturer": "ThermoFloor", + "manufacturerId": 411, + "label": "Z-TRM2fx", + "description": "Floor thermostat", + "devices": [ + { + "productType": 3, + "productId": 514 + } + ], + "firmwareVersion": { + "min": "3.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "compat": { + "valueIdRegex": {}, + "skipConfigurationInfoQuery": true + }, + "isEmbedded": true + }, + "label": "Z-TRM2fx", + "neighbors": [6, 7, 8, 11, 12, 13, 14, 15, 16, 17, 19, 20, 24, 25], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": false, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 26, + "index": 0, + "installerIcon": 4608, + "userIcon": 4609, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 1, + "installerIcon": 4608, + "userIcon": 4609, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 2, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 3, + "installerIcon": 3328, + "userIcon": 3329, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 33, + "label": "Multilevel Sensor" + }, + "specific": { + "key": 1, + "label": "Routing Multilevel Sensor" + }, + "mandatorySupportedCCs": [32, 49], + "mandatoryControlledCCs": [] + } + }, + { + "nodeId": 26, + "index": 4, + "installerIcon": 1792, + "userIcon": 1793, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 16, + "label": "Binary Switch" + }, + "specific": { + "key": 1, + "label": "Binary Power Switch" + }, + "mandatorySupportedCCs": [32, 37, 39], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 411 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 514 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "5.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["3.6"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "sdkVersion", + "propertyName": "sdkVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "6.71.3" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkAPIVersion", + "propertyName": "applicationFrameworkAPIVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "3.1.1" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationFrameworkBuildNumber", + "propertyName": "applicationFrameworkBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 52445 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceVersion", + "propertyName": "hostInterfaceVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hostInterfaceBuildNumber", + "propertyName": "hostInterfaceBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolVersion", + "propertyName": "zWaveProtocolVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "5.3.0" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "zWaveProtocolBuildNumber", + "propertyName": "zWaveProtocolBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 43 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationVersion", + "propertyName": "applicationVersion", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": "unused" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "applicationBuildNumber", + "propertyName": "applicationBuildNumber", + "ccVersion": 3, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 96, + "commandClassName": "Multi Channel", + "property": "endpointIndizes", + "propertyName": "endpointIndizes", + "ccVersion": 4, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + }, + "value": [1, 2, 3, 4] + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Operation mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Operation mode", + "default": 0, + "min": 0, + "max": 11, + "states": { + "0": "Off. (default)", + "1": "Heating mode", + "2": "Cooling mode (not implemented)", + "11": "Energy saving heating mode" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Sensor mode", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Sensor mode", + "default": 0, + "min": 0, + "max": 4, + "states": { + "0": "F-mode, floor sensor mode", + "3": "A2-mode, external room sensor mode", + "4": "A2F-mode, external sensor with floor limitation" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Floor sensor type", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor type", + "default": 0, + "min": 0, + "max": 5, + "states": { + "0": "10K-NTC (default)", + "1": "12K-NTC", + "2": "15K-NTC", + "3": "22K-NTC", + "4": "33K-NTC", + "5": "47K-NTC" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Temperature control hysteresis (DIFF I)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature control hysteresis (DIFF I)", + "default": 5, + "min": 3, + "max": 30, + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 5, + "propertyName": "Floor minimum temperature limit (FLo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor minimum temperature limit (FLo)", + "default": 50, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 6, + "propertyName": "Floor maximum temperature (FHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor maximum temperature (FHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 7, + "propertyName": "Air (A2) minimum temperature limit (ALo)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air (A2) minimum temperature limit (ALo)", + "default": 50, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 50 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 8, + "propertyName": "Air (A2) maximum temperature limit (AHi)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Air (A2) maximum temperature limit (AHi)", + "default": 400, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 400 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 9, + "propertyName": "Heating mode setpoint (CO)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Heating mode setpoint (CO)", + "default": 210, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 290 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Energy saving mode setpoint (ECO)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Energy saving mode setpoint (ECO)", + "default": 180, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 250 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 11, + "propertyName": "Cooling setpoint (COOL)", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Cooling setpoint (COOL)", + "default": 210, + "min": 50, + "max": 400, + "unit": "oC", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 200 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 12, + "propertyName": "Floor sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Floor sensor calibration", + "default": 0, + "min": -40, + "max": 40, + "unit": "oC", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 13, + "propertyName": "External sensor calibration", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "External sensor calibration", + "default": 0, + "min": -40, + "max": 40, + "unit": "oC", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 14, + "propertyName": "Temperature display", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature display", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Display setpoint temperature (default)", + "1": "Display measured temperature" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 15, + "propertyName": "Button brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 16, + "propertyName": "Button brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Button brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 17, + "propertyName": "Display brightness - dimmed state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - dimmed state", + "default": 50, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 18, + "propertyName": "Display brightness - active state", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Display brightness - active state", + "default": 100, + "min": 0, + "max": 100, + "unit": "%", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 19, + "propertyName": "Temperature report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report interval", + "default": 60, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 20, + "propertyName": "Temperature report hysteresis", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Temperature report hysteresis", + "default": 10, + "min": 1, + "max": 100, + "unit": "oC", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 21, + "propertyName": "Meter report interval", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report interval", + "default": 60, + "min": 0, + "max": 32767, + "unit": "seconds", + "valueSize": 2, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 60 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 22, + "propertyName": "Meter report delta value", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Meter report delta value", + "default": 10, + "min": 0, + "max": 127, + "unit": "kWh/10", + "valueSize": 1, + "format": 0, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 1, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + } + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "mode", + "propertyName": "mode", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Thermostat mode", + "min": 0, + "max": 255, + "states": { + "0": "Off", + "1": "Heat", + "2": "Cool", + "11": "Energy heat" + } + }, + "value": 1 + }, + { + "endpoint": 1, + "commandClass": 64, + "commandClassName": "Thermostat Mode", + "property": "manufacturerData", + "propertyName": "manufacturerData", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": true + } + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 1, + "propertyName": "setpoint", + "propertyKeyName": "Heating", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 1 + }, + "unit": "\u00b0C" + }, + "value": 29 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 2, + "propertyName": "setpoint", + "propertyKeyName": "Cooling", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 2 + }, + "unit": "\u00b0C" + }, + "value": 20 + }, + { + "endpoint": 1, + "commandClass": 67, + "commandClassName": "Thermostat Setpoint", + "property": "setpoint", + "propertyKey": 11, + "propertyName": "setpoint", + "propertyKeyName": "Energy Save Heating", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "ccSpecific": { + "setpointType": 11 + }, + "unit": "\u00b0C" + }, + "value": 25 + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + } + }, + { + "endpoint": 1, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 0, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + } + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 2, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 2, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 32, + "commandClassName": "Basic", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 0 + }, + { + "endpoint": 3, + "commandClass": 32, + "commandClassName": "Basic", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 3, + "commandClass": 49, + "commandClassName": "Multilevel Sensor", + "property": "Air temperature", + "propertyName": "Air temperature", + "ccVersion": 0, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Air temperature", + "ccSpecific": { + "sensorType": 1, + "scale": 0 + }, + "unit": "\u00b0C" + }, + "value": 28.8 + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": false + }, + { + "endpoint": 4, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 0, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "endpoint": 4, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 0 + }, + "unit": "kWh" + }, + "value": 795.7 + }, + { + "endpoint": 4, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 2 + }, + "unit": "W" + }, + "value": 493.57 + }, + { + "endpoint": 4, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66561, + "propertyName": "value", + "propertyKeyName": "Electric_V_Consumed", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [V]", + "ccSpecific": { + "meterType": 1, + "rateType": 1, + "scale": 4 + }, + "unit": "V" + }, + "value": 237.1 + } + ], + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 8, + "label": "Thermostat" + }, + "specific": { + "key": 6, + "label": "General Thermostat V2" + }, + "mandatorySupportedCCs": [32, 114, 64, 67, 134], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 64, + "name": "Thermostat Mode", + "version": 3, + "isSecure": false + }, + { + "id": 67, + "name": "Thermostat Setpoint", + "version": 3, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 3, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 108, + "name": "Supervision", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 3, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 4, + "isSecure": false + }, + { + "id": 50, + "name": "Meter", + "version": 3, + "isSecure": false + } + ], + "interviewStage": "Complete" +} diff --git a/tests/fixtures/zwave_js/controller_state.json b/tests/fixtures/zwave_js/controller_state.json index df026e8fd2c..d4bf58a53ce 100644 --- a/tests/fixtures/zwave_js/controller_state.json +++ b/tests/fixtures/zwave_js/controller_state.json @@ -91,7 +91,8 @@ 239 ], "sucNodeId": 1, - "supportsTimers": false + "supportsTimers": false, + "isHealNetworkActive": false }, "nodes": [ ] diff --git a/tests/fixtures/zwave_js/cover_qubino_shutter_state.json b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json new file mode 100644 index 00000000000..65725606e1c --- /dev/null +++ b/tests/fixtures/zwave_js/cover_qubino_shutter_state.json @@ -0,0 +1,765 @@ +{ + "nodeId": 5, + "index": 0, + "installerIcon": 6656, + "userIcon": 6656, + "status": 4, + "ready": true, + "deviceClass": { + "basic": { "key": 4, "label": "Routing Slave" }, + "generic": { "key": 17, "label": "Routing Slave" }, + "specific": { "key": 7, "label": "Routing Slave" }, + "mandatorySupportedCCs": [], + "mandatoryControlCCs": [] + }, + "isListening": true, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 345, + "productId": 83, + "productType": 3, + "firmwareVersion": "7.2", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 5, + "deviceConfig": { + "manufacturerId": 345, + "manufacturer": "Qubino", + "label": "ZMNHOD", + "description": "Flush Shutter DC", + "devices": [{ "productType": "0x0003", "productId": "0x0053" }], + "firmwareVersion": { "min": "0.0", "max": "255.255" }, + "paramInformation": { "_map": {} } + }, + "label": "ZMNHOD", + "neighbors": [1, 2], + "interviewAttempts": 1, + "endpoints": [ + { "nodeId": 5, "index": 0, "installerIcon": 6656, "userIcon": 6656 } + ], + "commandClasses": [], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "min": 0, + "max": 99, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 3, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 3, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 99, + "label": "Current value" + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 3, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { "switchType": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Current value" + }, + "value": "unknown" + }, + { + "endpoint": 0, + "commandClass": 37, + "commandClassName": "Binary Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Target value" + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 345 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 83 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.38" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["7.2"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 65537, + "propertyName": "value", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh]", + "unit": "kWh", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 65537, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "value", + "propertyKey": 66049, + "propertyName": "value", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W]", + "unit": "W", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "deltaTime", + "propertyKey": 66049, + "propertyName": "deltaTime", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W] (prev. time delta)", + "unit": "s", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "reset", + "propertyName": "reset", + "ccVersion": 4, + "metadata": { + "type": "boolean", + "readable": false, + "writeable": true, + "label": "Reset accumulated values" + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 65537, + "propertyName": "previousValue", + "propertyKeyName": "Electric_kWh_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [kWh] (prev. value)", + "unit": "kWh", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 0 } + } + }, + { + "endpoint": 0, + "commandClass": 50, + "commandClassName": "Meter", + "property": "previousValue", + "propertyKey": 66049, + "propertyName": "previousValue", + "propertyKeyName": "Electric_W_Consumed", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Electric Consumed [W] (prev. value)", + "unit": "W", + "ccSpecific": { "meterType": 1, "rateType": 1, "scale": 2 } + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmType", + "propertyName": "alarmType", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Alarm Type" + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "alarmLevel", + "propertyName": "alarmLevel", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Alarm Level" + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Over-load status", + "propertyName": "Power Management", + "propertyKeyName": "Over-load status", + "ccVersion": 5, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Over-load status", + "states": { "0": "idle", "8": "Over-load detected" }, + "ccSpecific": { "notificationType": 8 } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 10, + "propertyName": "Activate/deactivate functions ALL ON / ALL OFF", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 65535, + "default": 255, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "ALL ON is not active, ALL OFF is not active", + "1": "ALL ON is not active ALL OFF active", + "2": "ALL ON is not active ALL OFF is not active", + "255": "ALL ON active, ALL OFF active" + }, + "label": "Activate/deactivate functions ALL ON / ALL OFF", + "isFromConfig": true + }, + "value": 255 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 40, + "propertyName": "Power report (Watts) on power change for Q1 or Q2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 1, + "format": 0, + "allowManualEntry": true, + "label": "Power report (Watts) on power change for Q1 or Q2", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 42, + "propertyName": "Power report (Watts) by time interval for Q1 or Q2", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 300, + "format": 0, + "allowManualEntry": true, + "label": "Power report (Watts) by time interval for Q1 or Q2", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 71, + "propertyName": "Operating modes", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Shutter mode.", + "1": "Venetian mode (up/down and slate rotation)" + }, + "label": "Operating modes", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 72, + "propertyName": "Slats tilting full turn time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 150, + "format": 0, + "allowManualEntry": true, + "label": "Slats tilting full turn time", + "isFromConfig": true + }, + "value": 630 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 73, + "propertyName": "Slats position", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 1, + "format": 1, + "allowManualEntry": false, + "states": { + "0": "Return to previous position only with Z-wave", + "1": "Return to previous position with Z-wave or button" + }, + "label": "Slats position", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 74, + "propertyName": "Motor moving up/down time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 0, + "max": 32767, + "default": 0, + "format": 0, + "allowManualEntry": true, + "label": "Motor moving up/down time", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 76, + "propertyName": "Motor operation detection", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 100, + "default": 6, + "format": 0, + "allowManualEntry": true, + "label": "Motor operation detection", + "isFromConfig": true + }, + "value": 10 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 78, + "propertyName": "Forced Shutter DC calibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 255, + "default": 0, + "format": 1, + "allowManualEntry": false, + "states": { "0": "Default", "1": "Start calibration process." }, + "label": "Forced Shutter DC calibration", + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 85, + "propertyName": "Power consumption max delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 50, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Power consumption max delay time", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 86, + "propertyName": "Power consumption at limit switch delay time", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 3, + "max": 50, + "default": 8, + "format": 0, + "allowManualEntry": true, + "label": "Power consumption at limit switch delay time", + "isFromConfig": true + }, + "value": 8 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 90, + "propertyName": "Time delay for next motor movement", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 1, + "max": 30, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Time delay for next motor movement", + "isFromConfig": true + }, + "value": 5 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 110, + "propertyName": "Temperature sensor offset settings", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 2, + "min": 1, + "max": 32536, + "default": 32536, + "format": 0, + "allowManualEntry": true, + "label": "Temperature sensor offset settings", + "isFromConfig": true + }, + "value": 32536 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 120, + "propertyName": "Digital temperature sensor reporting", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 127, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Digital temperature sensor reporting", + "isFromConfig": true + }, + "value": 5 + } + ] +} diff --git a/tests/fixtures/zwave_js/light_color_null_values_state.json b/tests/fixtures/zwave_js/light_color_null_values_state.json new file mode 100644 index 00000000000..46bc9f29b06 --- /dev/null +++ b/tests/fixtures/zwave_js/light_color_null_values_state.json @@ -0,0 +1,682 @@ +{ + "nodeId": 39, + "index": 0, + "installerIcon": 6912, + "userIcon": 6912, + "status": 4, + "ready": true, + "isListening": true, + "isRouting": true, + "isSecure": "unknown", + "manufacturerId": 134, + "productId": 117, + "productType": 4, + "firmwareVersion": "1.5", + "zwavePlusVersion": 1, + "name": "Repeater", + "location": "Dining Room", + "deviceConfig": { + "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0086/zw117.json", + "manufacturer": "AEON Labs", + "manufacturerId": 134, + "label": "ZW117", + "description": "Range Extender 6", + "devices": [ + { + "productType": 4, + "productId": 117 + }, + { + "productType": 260, + "productId": 117 + }, + { + "productType": 516, + "productId": 117 + }, + { + "productType": 2564, + "productId": 117 + }, + { + "productType": 7172, + "productId": 117 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "associations": {}, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "Turn the primary controller of Z-Wave network into inclusion mode, press the Z-Wave Button on Range Extender 6", + "exclusion": "Turn the primary controller of Z-Wave network into exclusion mode, press the Z-Wave Button on Range Extender 6", + "reset": "Press and hold the Z-Wave Button for 20 seconds and then release it.\nUse this procedure only in the event that your primary network controller is missing or inoperable", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2520/Range%20Extender%206%20manual.pdf" + }, + "isEmbedded": true + }, + "label": "ZW117", + "neighbors": [4, 17, 28, 29], + "interviewAttempts": 1, + "endpoints": [ + { + "nodeId": 39, + "index": 0, + "installerIcon": 6912, + "userIcon": 6912, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 15, + "label": "Repeater Slave" + }, + "specific": { + "key": 1, + "label": "Repeater Slave" + }, + "mandatorySupportedCCs": [32], + "mandatoryControlledCCs": [] + } + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "targetValue", + "propertyName": "targetValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Target value", + "min": 0, + "max": 99 + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 2, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Transition duration" + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "currentValue", + "propertyName": "currentValue", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Current value", + "min": 0, + "max": 99 + }, + "value": 99 + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Up", + "propertyName": "Up", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Up)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "property": "Down", + "propertyName": "Down", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Perform a level change (Down)", + "ccSpecific": { + "switchType": 2 + } + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "duration", + "propertyName": "duration", + "ccVersion": 1, + "metadata": { + "type": "duration", + "readable": true, + "writeable": true, + "label": "Remaining duration" + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 2, + "propertyName": "currentColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Red color.", + "label": "Current value (Red)", + "min": 0, + "max": 255 + }, + "value": null + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 3, + "propertyName": "currentColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Green color.", + "label": "Current value (Green)", + "min": 0, + "max": 255 + }, + "value": null + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyKey": 4, + "propertyName": "currentColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "description": "The current value of the Blue color.", + "label": "Current value (Blue)", + "min": 0, + "max": 255 + }, + "value": null + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "currentColor", + "propertyName": "currentColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Current Color" + }, + "value": { + "red": null, + "green": null, + "blue": null + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "hexColor", + "propertyName": "hexColor", + "ccVersion": 1, + "metadata": { + "type": "color", + "readable": true, + "writeable": true, + "label": "RGB Color", + "minLength": 6, + "maxLength": 7 + }, + "value": "00ff00" + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 2, + "propertyName": "targetColor", + "propertyKeyName": "Red", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Red color.", + "label": "Target value (Red)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 3, + "propertyName": "targetColor", + "propertyKeyName": "Green", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Green color.", + "label": "Target value (Green)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyKey": 4, + "propertyName": "targetColor", + "propertyKeyName": "Blue", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "description": "The target value of the Blue color.", + "label": "Target value (Blue)", + "min": 0, + "max": 255 + } + }, + { + "endpoint": 0, + "commandClass": 51, + "commandClassName": "Color Switch", + "property": "targetColor", + "propertyName": "targetColor", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": true, + "label": "Target Color" + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 82, + "propertyName": "LED Indicator", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "LED Indicator", + "default": 1, + "min": 0, + "max": 1, + "states": { + "0": "On for 2 seconds", + "1": "Disable" + }, + "valueSize": 1, + "format": 0, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 200, + "propertyName": "Partner ID", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Partner ID", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Aeotec", + "1": "Other" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 252, + "propertyName": "Lock Configuration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Lock Configuration", + "default": 0, + "min": 0, + "max": 1, + "states": { + "0": "Disable", + "1": "Enable" + }, + "valueSize": 1, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 254, + "propertyName": "Device Tag", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "label": "Device Tag", + "default": 0, + "min": 0, + "max": 65535, + "valueSize": 2, + "format": 1, + "allowManualEntry": true, + "isFromConfig": true + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 255, + "propertyName": "Reset to Factory Default Setting", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "label": "Reset to Factory Default Setting", + "default": 0, + "min": 1, + "max": 1431655765, + "states": { + "1": "Resets all configuration parameters to default setting", + "1431655765": "Reset the product to factory default setting and exclude from Z-Wave network" + }, + "valueSize": 4, + "format": 1, + "allowManualEntry": false, + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Manufacturer ID", + "min": 0, + "max": 65535 + }, + "value": 134 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product type", + "min": 0, + "max": 65535 + }, + "value": 4 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "label": "Product ID", + "min": 0, + "max": 65535 + }, + "value": 117 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.54" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["1.5"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ], + "interviewStage": 6, + "isFrequentListening": false, + "maxDataRate": 100000, + "supportedDataRates": [40000, 100000], + "protocolVersion": 3, + "supportsBeaming": true, + "supportsSecurity": false, + "nodeType": 1, + "zwavePlusNodeType": 0, + "zwavePlusRoleType": 5, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 15, + "label": "Repeater Slave" + }, + "specific": { + "key": 1, + "label": "Repeater Slave" + }, + "mandatorySupportedCCs": [32], + "mandatoryControlledCCs": [] + }, + "commandClasses": [ + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 38, + "name": "Multilevel Switch", + "version": 2, + "isSecure": false + }, + { + "id": 51, + "name": "Color Switch", + "version": 1, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 152, + "name": "Security", + "version": 1, + "isSecure": true + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + } + ] + } diff --git a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json index 60078100caf..0f90d2ae147 100644 --- a/tests/fixtures/zwave_js/nortek_thermostat_added_event.json +++ b/tests/fixtures/zwave_js/nortek_thermostat_added_event.json @@ -2,7 +2,7 @@ "source": "controller", "event": "node added", "node": { - "nodeId": 53, + "nodeId": 67, "index": 0, "status": 0, "ready": false, @@ -17,7 +17,7 @@ "interviewAttempts": 1, "endpoints": [ { - "nodeId": 53, + "nodeId": 67, "index": 0 } ], diff --git a/tests/fixtures/zwave_js/wallmote_central_scene_state.json b/tests/fixtures/zwave_js/wallmote_central_scene_state.json new file mode 100644 index 00000000000..22eb05c9ce5 --- /dev/null +++ b/tests/fixtures/zwave_js/wallmote_central_scene_state.json @@ -0,0 +1,698 @@ +{ + "nodeId": 35, + "index": 0, + "installerIcon": 7172, + "userIcon": 7172, + "status": 1, + "ready": true, + "deviceClass": { + "basic": { + "key": 4, + "label": "Routing Slave" + }, + "generic": { + "key": 24, + "label": "Wall Controller" + }, + "specific": { + "key": 1, + "label": "Basic Wall Controller" + }, + "mandatorySupportedCCs": [], + "mandatoryControlledCCs": [] + }, + "isListening": false, + "isFrequentListening": false, + "isRouting": true, + "maxBaudRate": 40000, + "isSecure": false, + "version": 4, + "isBeaming": true, + "manufacturerId": 134, + "productId": 130, + "productType": 258, + "firmwareVersion": "2.3", + "zwavePlusVersion": 1, + "nodeType": 0, + "roleType": 4, + "name": "mbr_wallmote_quad", + "deviceDatabaseUrl": "https://devices.zwave-js.io/?jumpTo=0x0086:0x0002:0x0082:0.0", + "deviceConfig": { + "filename": "/usr/src/app/node_modules/@zwave-js/config/config/devices/0x0086/zw130.json", + "manufacturerId": 134, + "manufacturer": "AEON Labs", + "label": "ZW130", + "description": "WallMote Quad", + "devices": [ + { + "productType": 2, + "productId": 130 + }, + { + "productType": 258, + "productId": 130 + }, + { + "productType": 514, + "productId": 130 + } + ], + "firmwareVersion": { + "min": "0.0", + "max": "255.255" + }, + "paramInformation": { + "_map": {} + }, + "metadata": { + "inclusion": "To add the ZP3111 to the Z-Wave network (inclusion), place the Z-Wave primary controller into inclusion mode. Press the Program Switch of ZP3111 for sending the NIF. After sending NIF, Z-Wave will send the auto inclusion, otherwise, ZP3111 will go to sleep after 20 seconds.", + "exclusion": "To remove the ZP3111 from the Z-Wave network (exclusion), place the Z-Wave primary controller into \u201cexclusion\u201d mode, and following its instruction to delete the ZP3111 to the controller. Press the Program Switch of ZP3111 once to be excluded.", + "reset": "Remove cover to triggered tamper switch, LED flash once & send out Alarm Report. Press Program Switch 10 times within 10 seconds, ZP3111 will send the \u201cDevice Reset Locally Notification\u201d command and reset to the factory default. (Remark: This is to be used only in the case of primary controller being inoperable or otherwise unavailable.)", + "manual": "https://products.z-wavealliance.org/ProductManual/File?folder=&filename=MarketCertificationFiles/2479/ZP3111-5_R2_20170316.pdf" + }, + "isEmbedded": true + }, + "label": "ZW130", + "neighbors": [1, 14, 15, 16, 22, 30, 31, 5, 6, 7, 8], + "endpointCountIsDynamic": false, + "endpointsHaveIdenticalCapabilities": true, + "individualEndpointCount": 4, + "aggregatedEndpointCount": 0, + "interviewAttempts": 1, + "interviewStage": "NodeInfo", + "commandClasses": [ + { + "id": 89, + "name": "Association Group Information", + "version": 1, + "isSecure": false + }, + { + "id": 90, + "name": "Device Reset Locally", + "version": 1, + "isSecure": false + }, + { + "id": 91, + "name": "Central Scene", + "version": 2, + "isSecure": false + }, + { + "id": 94, + "name": "Z-Wave Plus Info", + "version": 2, + "isSecure": false + }, + { + "id": 96, + "name": "Multi Channel", + "version": 4, + "isSecure": false + }, + { + "id": 112, + "name": "Configuration", + "version": 1, + "isSecure": false + }, + { + "id": 113, + "name": "Notification", + "version": 4, + "isSecure": false + }, + { + "id": 114, + "name": "Manufacturer Specific", + "version": 2, + "isSecure": false + }, + { + "id": 122, + "name": "Firmware Update Meta Data", + "version": 2, + "isSecure": false + }, + { + "id": 128, + "name": "Battery", + "version": 1, + "isSecure": false + }, + { + "id": 132, + "name": "Wake Up", + "version": 1, + "isSecure": false + }, + { + "id": 133, + "name": "Association", + "version": 2, + "isSecure": false + }, + { + "id": 134, + "name": "Version", + "version": 2, + "isSecure": false + }, + { + "id": 142, + "name": "Multi Channel Association", + "version": 3, + "isSecure": false + } + ], + "endpoints": [ + { + "nodeId": 35, + "index": 0, + "installerIcon": 7172, + "userIcon": 7172 + }, + { + "nodeId": 35, + "index": 1, + "installerIcon": 7169, + "userIcon": 7169 + }, + { + "nodeId": 35, + "index": 2, + "installerIcon": 7169, + "userIcon": 7169 + }, + { + "nodeId": 35, + "index": 3, + "installerIcon": 7169, + "userIcon": 7169 + }, + { + "nodeId": 35, + "index": 4, + "installerIcon": 7169, + "userIcon": 7169 + } + ], + "values": [ + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "slowRefresh", + "propertyName": "slowRefresh", + "ccVersion": 2, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": true, + "label": "Send held down notifications at a slow rate", + "description": "When this is true, KeyHeldDown notifications are sent every 55s. When this is false, the notifications are sent every 200ms." + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "004", + "propertyName": "scene", + "propertyKeyName": "004", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 004", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "001", + "propertyName": "scene", + "propertyKeyName": "001", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 001", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "002", + "propertyName": "scene", + "propertyKeyName": "002", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 002", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 91, + "commandClassName": "Central Scene", + "property": "scene", + "propertyKey": "003", + "propertyName": "scene", + "propertyKeyName": "003", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Scene 003", + "states": { + "0": "KeyPressed", + "1": "KeyReleased", + "2": "KeyHeldDown" + } + } + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 1, + "propertyName": "Touch sound", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Touch sound", + "description": "Enable/disable the touch sound.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 2, + "propertyName": "Touch vibration", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Touch vibration", + "description": "Enable/disable the touch vibration.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 3, + "propertyName": "Button slide", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 1, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Disable", + "1": "Enable" + }, + "label": "Button slide", + "description": "Enable/disable the function of button slide.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 4, + "propertyName": "Notification report", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 3, + "default": 1, + "format": 0, + "allowManualEntry": false, + "states": { + "1": "Central scene", + "3": "Central scene and config" + }, + "label": "Notification report", + "description": "Which notification to be sent to the associated devices.", + "isFromConfig": true + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 39, + "propertyName": "Low battery value", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": true, + "valueSize": 1, + "min": 0, + "max": 50, + "default": 5, + "format": 0, + "allowManualEntry": true, + "label": "Low battery value", + "description": "Set the low battery value", + "isFromConfig": true + }, + "value": 20 + }, + { + "endpoint": 0, + "commandClass": 112, + "commandClassName": "Configuration", + "property": 255, + "propertyName": "Reset the WallMote Quad", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "valueSize": 4, + "min": 0, + "max": 1431655765, + "default": 0, + "format": 0, + "allowManualEntry": false, + "states": { + "0": "Reset to factory default", + "1431655765": "Reset and remove" + }, + "label": "Reset the WallMote Quad", + "description": "Reset the WallMote Quad to factory default.", + "isFromConfig": true + } + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery load status", + "propertyName": "Power Management", + "propertyKeyName": "Battery load status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery load status", + "states": { + "0": "idle", + "12": "Battery is charging" + }, + "ccSpecific": { + "notificationType": 8 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 113, + "commandClassName": "Notification", + "property": "Power Management", + "propertyKey": "Battery level status", + "propertyName": "Power Management", + "propertyKeyName": "Battery level status", + "ccVersion": 4, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 255, + "label": "Battery level status", + "states": { + "0": "idle", + "13": "Battery is fully charged", + "14": "Charge battery soon", + "15": "Charge battery now" + }, + "ccSpecific": { + "notificationType": 8 + } + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "manufacturerId", + "propertyName": "manufacturerId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Manufacturer ID" + }, + "value": 134 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productType", + "propertyName": "productType", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product type" + }, + "value": 258 + }, + { + "endpoint": 0, + "commandClass": 114, + "commandClassName": "Manufacturer Specific", + "property": "productId", + "propertyName": "productId", + "ccVersion": 2, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 65535, + "label": "Product ID" + }, + "value": 130 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "level", + "propertyName": "level", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": true, + "writeable": false, + "min": 0, + "max": 100, + "unit": "%", + "label": "Battery level" + }, + "value": 100 + }, + { + "endpoint": 0, + "commandClass": 128, + "commandClassName": "Battery", + "property": "isLow", + "propertyName": "isLow", + "ccVersion": 1, + "metadata": { + "type": "boolean", + "readable": true, + "writeable": false, + "label": "Low battery level" + }, + "value": false + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "wakeUpInterval", + "propertyName": "wakeUpInterval", + "ccVersion": 1, + "metadata": { + "type": "number", + "readable": false, + "writeable": true, + "min": 0, + "max": 864000, + "label": "Wake Up interval", + "steps": 240, + "default": 0 + }, + "value": 0 + }, + { + "endpoint": 0, + "commandClass": 132, + "commandClassName": "Wake Up", + "property": "controllerNodeId", + "propertyName": "controllerNodeId", + "ccVersion": 1, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Node ID of the controller" + }, + "value": 1 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "libraryType", + "propertyName": "libraryType", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Library type" + }, + "value": 3 + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "protocolVersion", + "propertyName": "protocolVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave protocol version" + }, + "value": "4.62" + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "firmwareVersions", + "propertyName": "firmwareVersions", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip firmware versions" + }, + "value": ["2.3"] + }, + { + "endpoint": 0, + "commandClass": 134, + "commandClassName": "Version", + "property": "hardwareVersion", + "propertyName": "hardwareVersion", + "ccVersion": 2, + "metadata": { + "type": "any", + "readable": true, + "writeable": false, + "label": "Z-Wave chip hardware version" + } + } + ] +} diff --git a/tests/hassfest/test_version.py b/tests/hassfest/test_version.py index f99ee911a69..7f12fb83fd7 100644 --- a/tests/hassfest/test_version.py +++ b/tests/hassfest/test_version.py @@ -25,10 +25,9 @@ def integration(): def test_validate_version_no_key(integration: Integration): """Test validate version with no key.""" validate_version(integration) - assert ( - "No 'version' key in the manifest file. This will cause a future version of Home Assistant to block this integration." - in [x.error for x in integration.errors] - ) + assert "No 'version' key in the manifest file." in [ + x.error for x in integration.errors + ] def test_validate_custom_integration_manifest(integration: Integration): diff --git a/tests/helpers/test_condition.py b/tests/helpers/test_condition.py index 11705502e77..2290ce9f679 100644 --- a/tests/helpers/test_condition.py +++ b/tests/helpers/test_condition.py @@ -27,7 +27,7 @@ def calls(hass): @pytest.fixture(autouse=True) def setup_comp(hass): """Initialize components.""" - dt_util.set_default_time_zone(hass.config.time_zone) + hass.config.set_time_zone(hass.config.time_zone) hass.loop.run_until_complete( async_setup_component(hass, sun.DOMAIN, {sun.DOMAIN: {sun.CONF_ELEVATION: 0}}) ) @@ -791,6 +791,34 @@ async def test_time_using_input_datetime(hass): hass, after="input_datetime.pm", before="input_datetime.am" ) + # Trigger on PM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=18, minute=0, second=0), + ): + assert condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert not condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.pm") + assert not condition.time(hass, before="input_datetime.pm") + + # Trigger on AM time + with patch( + "homeassistant.helpers.condition.dt_util.now", + return_value=dt_util.now().replace(hour=6, minute=0, second=0), + ): + assert not condition.time( + hass, after="input_datetime.pm", before="input_datetime.am" + ) + assert condition.time( + hass, after="input_datetime.am", before="input_datetime.pm" + ) + assert condition.time(hass, after="input_datetime.am") + assert not condition.time(hass, before="input_datetime.am") + with pytest.raises(ConditionError): condition.time(hass, after="input_datetime.not_existing") diff --git a/tests/helpers/test_config_entry_flow.py b/tests/helpers/test_config_entry_flow.py index edee75fe240..7f54ba99c8a 100644 --- a/tests/helpers/test_config_entry_flow.py +++ b/tests/helpers/test_config_entry_flow.py @@ -26,7 +26,7 @@ def discovery_flow_conf(hass): with patch.dict(config_entries.HANDLERS): config_entry_flow.register_discovery_flow( - "test", "Test", has_discovered_devices, config_entries.CONN_CLASS_LOCAL_POLL + "test", "Test", has_discovered_devices ) yield handler_conf @@ -344,3 +344,14 @@ async def test_webhook_create_cloudhook(hass, webhook_flow_conf): assert len(mock_delete.mock_calls) == 1 assert result["require_restart"] is False + + +async def test_warning_deprecated_connection_class(hass, caplog): + """Test that we log a warning when the connection_class is used.""" + discovery_function = Mock() + with patch.dict(config_entries.HANDLERS): + config_entry_flow.register_discovery_flow( + "test", "Test", discovery_function, connection_class="local_polling" + ) + + assert "integration is setting a connection_class" in caplog.text diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 518434cf79b..037e1aec8c2 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -1222,11 +1222,11 @@ async def test_disable_config_entry_disables_devices(hass, registry): entry1 = registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(device_registry.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entry2 = registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "34:56:AB:CD:EF:12")}, + connections={(device_registry.CONNECTION_NETWORK_MAC, "34:56:AB:CD:EF:12")}, disabled_by=device_registry.DISABLED_USER, ) diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 9ab269811f9..65a46f33cd8 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -57,6 +57,17 @@ async def test_polling_only_updates_entities_it_should_poll(hass): assert poll_ent.async_update.called +async def test_polling_disabled_by_config_entry(hass): + """Test the polling of only updated entities.""" + entity_platform = MockEntityPlatform(hass) + entity_platform.config_entry = MockConfigEntry(pref_disable_polling=True) + + poll_ent = MockEntity(should_poll=True) + + await entity_platform.async_add_entities([poll_ent]) + assert entity_platform._async_unsub_polling is None + + async def test_polling_updates_entities_with_exception(hass): """Test the updated entities that not break with an exception.""" component = EntityComponent(_LOGGER, DOMAIN, hass, timedelta(seconds=20)) @@ -821,7 +832,7 @@ async def test_device_info_called(hass): unique_id="qwer", device_info={ "identifiers": {("hue", "1234")}, - "connections": {("mac", "abcd")}, + "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, "manufacturer": "test-manuf", "model": "test-model", "name": "test-name", @@ -849,7 +860,7 @@ async def test_device_info_called(hass): device = registry.async_get_device({("hue", "1234")}) assert device is not None assert device.identifiers == {("hue", "1234")} - assert device.connections == {("mac", "abcd")} + assert device.connections == {(dr.CONNECTION_NETWORK_MAC, "abcd")} assert device.manufacturer == "test-manuf" assert device.model == "test-model" assert device.name == "test-name" @@ -864,7 +875,7 @@ async def test_device_info_not_overrides(hass): registry = dr.async_get(hass) device = registry.async_get_or_create( config_entry_id="bla", - connections={("mac", "abcd")}, + connections={(dr.CONNECTION_NETWORK_MAC, "abcd")}, manufacturer="test-manufacturer", model="test-model", ) @@ -879,7 +890,7 @@ async def test_device_info_not_overrides(hass): MockEntity( unique_id="qwer", device_info={ - "connections": {("mac", "abcd")}, + "connections": {(dr.CONNECTION_NETWORK_MAC, "abcd")}, "default_name": "default name 1", "default_model": "default model 1", "default_manufacturer": "default manufacturer 1", @@ -898,7 +909,7 @@ async def test_device_info_not_overrides(hass): assert await entity_platform.async_setup_entry(config_entry) await hass.async_block_till_done() - device2 = registry.async_get_device(set(), {("mac", "abcd")}) + device2 = registry.async_get_device(set(), {(dr.CONNECTION_NETWORK_MAC, "abcd")}) assert device2 is not None assert device.id == device2.id assert device2.manufacturer == "test-manufacturer" @@ -946,7 +957,6 @@ async def test_entity_info_added_to_entity_registry(hass): registry = er.async_get(hass) entry_default = registry.async_get_or_create(DOMAIN, DOMAIN, "default") - print(entry_default) assert entry_default.capabilities == {"max": 100} assert entry_default.supported_features == 5 assert entry_default.device_class == "mock-device-class" diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index a1050e5fc67..fe445e32c96 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -6,7 +6,8 @@ import pytest from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_START, STATE_UNAVAILABLE from homeassistant.core import CoreState, callback, valid_entity_id -from homeassistant.helpers import entity_registry as er +from homeassistant.exceptions import MaxLengthExceeded +from homeassistant.helpers import device_registry as dr, entity_registry as er from tests.common import ( MockConfigEntry, @@ -512,12 +513,12 @@ async def test_disabled_by(registry): assert entry2.disabled_by is None -async def test_disabled_by_system_options(registry): - """Test system options setting disabled_by.""" +async def test_disabled_by_config_entry_pref(registry): + """Test config entry preference setting disabled_by.""" mock_config = MockConfigEntry( domain="light", entry_id="mock-id-1", - system_options={"disable_new_entities": True}, + pref_disable_new_entities=True, ) entry = registry.async_get_or_create( "light", "hue", "AAAA", config_entry=mock_config @@ -686,7 +687,7 @@ async def test_remove_device_removes_entities(hass, registry): device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entry = registry.async_get_or_create( @@ -713,13 +714,13 @@ async def test_update_device_race(hass, registry): # Create device device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) # Update it device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, identifiers={("bridgeid", "0123")}, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) # Add entity to the device entry = registry.async_get_or_create( @@ -746,7 +747,7 @@ async def test_disable_device_disables_entities(hass, registry): device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entry1 = registry.async_get_or_create( @@ -811,7 +812,7 @@ async def test_disable_config_entry_disables_entities(hass, registry): device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entry1 = registry.async_get_or_create( @@ -877,7 +878,7 @@ async def test_disabled_entities_excluded_from_entity_list(hass, registry): device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) entry1 = registry.async_get_or_create( @@ -904,3 +905,47 @@ async def test_disabled_entities_excluded_from_entity_list(hass, registry): registry, device_entry.id, include_disabled_entities=True ) assert entries == [entry1, entry2] + + +async def test_entity_max_length_exceeded(hass, registry): + """Test that an exception is raised when the max character length is exceeded.""" + + long_entity_id_name = ( + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + ) + + with pytest.raises(MaxLengthExceeded) as exc_info: + registry.async_generate_entity_id("sensor", long_entity_id_name) + + assert exc_info.value.property_name == "generated_entity_id" + assert exc_info.value.max_length == 255 + assert exc_info.value.value == f"sensor.{long_entity_id_name}" + + # Try again but against the domain + long_domain_name = long_entity_id_name + with pytest.raises(MaxLengthExceeded) as exc_info: + registry.async_generate_entity_id(long_domain_name, "sensor") + + assert exc_info.value.property_name == "domain" + assert exc_info.value.max_length == 64 + assert exc_info.value.value == long_domain_name + + # Try again but force a number to get added to the entity ID + long_entity_id_name = ( + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567890123456789012345678901234567890" + "1234567890123456789012345678901234567" + ) + + with pytest.raises(MaxLengthExceeded) as exc_info: + registry.async_generate_entity_id( + "sensor", long_entity_id_name, [f"sensor.{long_entity_id_name}"] + ) + + assert exc_info.value.property_name == "generated_entity_id" + assert exc_info.value.max_length == 255 + assert exc_info.value.value == f"sensor.{long_entity_id_name}_2" diff --git a/tests/helpers/test_event.py b/tests/helpers/test_event.py index b8291b97efa..e134c5e327d 100644 --- a/tests/helpers/test_event.py +++ b/tests/helpers/test_event.py @@ -2908,8 +2908,8 @@ async def test_periodic_task_entering_dst(hass): specific_runs = [] now = dt_util.utcnow() - time_that_will_not_match_right_away = timezone.localize( - datetime(now.year + 1, 3, 25, 2, 31, 0) + time_that_will_not_match_right_away = datetime( + now.year + 1, 3, 25, 2, 31, 0, tzinfo=timezone ) with patch( @@ -2924,25 +2924,25 @@ async def test_periodic_task_entering_dst(hass): ) async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 25, 1, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 25, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 25, 3, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 25, 3, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 26, 1, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 26, 1, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, timezone.localize(datetime(now.year + 1, 3, 26, 2, 50, 0, 999999)) + hass, datetime(now.year + 1, 3, 26, 2, 50, 0, 999999, tzinfo=timezone) ) await hass.async_block_till_done() assert len(specific_runs) == 1 @@ -2958,8 +2958,8 @@ async def test_periodic_task_leaving_dst(hass): now = dt_util.utcnow() - time_that_will_not_match_right_away = timezone.localize( - datetime(now.year + 1, 10, 28, 2, 28, 0), is_dst=True + time_that_will_not_match_right_away = datetime( + now.year + 1, 10, 28, 2, 28, 0, tzinfo=timezone, fold=1 ) with patch( @@ -2974,46 +2974,33 @@ async def test_periodic_task_leaving_dst(hass): ) async_fire_time_changed( - hass, - timezone.localize( - datetime(now.year + 1, 10, 28, 2, 5, 0, 999999), is_dst=False - ), + hass, datetime(now.year + 1, 10, 28, 2, 5, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 0 async_fire_time_changed( - hass, - timezone.localize( - datetime(now.year + 1, 10, 28, 2, 55, 0, 999999), is_dst=False - ), + hass, datetime(now.year + 1, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=0) ) await hass.async_block_till_done() assert len(specific_runs) == 1 async_fire_time_changed( hass, - timezone.localize( - datetime(now.year + 2, 10, 28, 2, 45, 0, 999999), is_dst=True - ), + datetime(now.year + 2, 10, 28, 2, 45, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 async_fire_time_changed( hass, - timezone.localize( - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999), is_dst=True - ), + datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1), ) await hass.async_block_till_done() assert len(specific_runs) == 2 async_fire_time_changed( - hass, - timezone.localize( - datetime(now.year + 2, 10, 28, 2, 55, 0, 999999), is_dst=True - ), + hass, datetime(now.year + 2, 10, 28, 2, 55, 0, 999999, tzinfo=timezone, fold=1) ) await hass.async_block_till_done() assert len(specific_runs) == 2 @@ -3224,7 +3211,7 @@ async def test_async_track_point_in_time_cancel(hass): await asyncio.sleep(0.2) assert len(times) == 1 - assert times[0].tzinfo.zone == "US/Hawaii" + assert "US/Hawaii" in str(times[0].tzinfo) async def test_async_track_entity_registry_updated_event(hass): diff --git a/tests/helpers/test_script.py b/tests/helpers/test_script.py index 2045f8cdbbc..546f494735e 100644 --- a/tests/helpers/test_script.py +++ b/tests/helpers/test_script.py @@ -1412,8 +1412,7 @@ async def test_condition_warning(hass, caplog): { "0": [{"result": {"event": "test_event", "event_data": {}}}], "1": [{"error_type": script._StopScript, "result": {"result": False}}], - "1/condition": [{"error_type": ConditionError}], - "1/condition/entity_id/0": [{"error_type": ConditionError}], + "1/entity_id/0": [{"error_type": ConditionError}], }, expected_script_execution="aborted", ) @@ -1448,8 +1447,7 @@ async def test_condition_basic(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"result": {"result": True}}], - "1/condition": [{"result": {"entities": ["test.entity"], "result": True}}], + "1": [{"result": {"entities": ["test.entity"], "result": True}}], "2": [{"result": {"event": "test_event", "event_data": {}}}], } ) @@ -1465,8 +1463,12 @@ async def test_condition_basic(hass, caplog): assert_action_trace( { "0": [{"result": {"event": "test_event", "event_data": {}}}], - "1": [{"error_type": script._StopScript, "result": {"result": False}}], - "1/condition": [{"result": {"entities": ["test.entity"], "result": False}}], + "1": [ + { + "error_type": script._StopScript, + "result": {"entities": ["test.entity"], "result": False}, + } + ], }, expected_script_execution="aborted", ) diff --git a/tests/helpers/test_start.py b/tests/helpers/test_start.py new file mode 100644 index 00000000000..35838f1ceaa --- /dev/null +++ b/tests/helpers/test_start.py @@ -0,0 +1,39 @@ +"""Test starting HA helpers.""" +from homeassistant import core +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.helpers import start + + +async def test_at_start_when_running(hass): + """Test at start when already running.""" + assert hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 1 + + +async def test_at_start_when_starting(hass): + """Test at start when yet to start.""" + hass.state = core.CoreState.not_running + assert not hass.is_running + + calls = [] + + async def cb_at_start(hass): + """Home Assistant is started.""" + calls.append(1) + + start.async_at_start(hass, cb_at_start) + await hass.async_block_till_done() + assert len(calls) == 0 + + hass.bus.async_fire(EVENT_HOMEASSISTANT_START) + await hass.async_block_till_done() + assert len(calls) == 1 diff --git a/tests/helpers/test_template.py b/tests/helpers/test_template.py index 46098917b0e..48ef6f25b67 100644 --- a/tests/helpers/test_template.py +++ b/tests/helpers/test_template.py @@ -5,7 +5,6 @@ import random from unittest.mock import patch import pytest -import pytz import voluptuous as vol from homeassistant.components import group @@ -19,7 +18,7 @@ from homeassistant.const import ( VOLUME_LITERS, ) from homeassistant.exceptions import TemplateError -from homeassistant.helpers import template +from homeassistant.helpers import device_registry as dr, template from homeassistant.setup import async_setup_component import homeassistant.util.dt as dt_util from homeassistant.util.unit_system import UnitSystem @@ -763,7 +762,7 @@ def test_render_with_possible_json_value_non_string_value(hass): hass, ) value = datetime(2019, 1, 18, 12, 13, 14) - expected = str(pytz.utc.localize(value)) + expected = str(value.replace(tzinfo=dt_util.UTC)) assert tpl.async_render_with_possible_json_value(value) == expected @@ -1504,7 +1503,7 @@ async def test_device_entities(hass): # Test device without entities device_entry = device_registry.async_get_or_create( config_entry_id=config_entry.entry_id, - connections={("mac", "12:34:56:AB:CD:EF")}, + connections={(dr.CONNECTION_NETWORK_MAC, "12:34:56:AB:CD:EF")}, ) info = render_to_info(hass, f"{{{{ device_entities('{device_entry.id}') }}}}") assert_result_info(info, []) diff --git a/tests/helpers/test_translation.py b/tests/helpers/test_translation.py index 8f555914682..3a08c423d76 100644 --- a/tests/helpers/test_translation.py +++ b/tests/helpers/test_translation.py @@ -33,25 +33,18 @@ def test_recursive_flatten(): } -async def test_component_translation_path(hass): +async def test_component_translation_path(hass, enable_custom_integrations): """Test the component translation file function.""" assert await async_setup_component( hass, "switch", {"switch": [{"platform": "test"}, {"platform": "test_embedded"}]}, ) - assert await async_setup_component(hass, "test_standalone", {"test_standalone"}) assert await async_setup_component(hass, "test_package", {"test_package"}) - ( - int_test, - int_test_embedded, - int_test_standalone, - int_test_package, - ) = await asyncio.gather( + (int_test, int_test_embedded, int_test_package,) = await asyncio.gather( async_get_integration(hass, "test"), async_get_integration(hass, "test_embedded"), - async_get_integration(hass, "test_standalone"), async_get_integration(hass, "test_package"), ) @@ -71,13 +64,6 @@ async def test_component_translation_path(hass): ) ) - assert ( - translation.component_translation_path( - "test_standalone", "en", int_test_standalone - ) - is None - ) - assert path.normpath( translation.component_translation_path("test_package", "en", int_test_package) ) == path.normpath( @@ -105,7 +91,7 @@ def test_load_translations_files(hass): } -async def test_get_translations(hass, mock_config_flows): +async def test_get_translations(hass, mock_config_flows, enable_custom_integrations): """Test the get translations helper.""" translations = await translation.async_get_translations(hass, "en", "state") assert translations == {} @@ -376,9 +362,8 @@ async def test_caching(hass): assert len(mock_build.mock_calls) > 1 -async def test_custom_component_translations(hass): +async def test_custom_component_translations(hass, enable_custom_integrations): """Test getting translation from custom components.""" - hass.config.components.add("test_standalone") hass.config.components.add("test_embedded") hass.config.components.add("test_package") assert await translation.async_get_translations(hass, "en", "state") == {} diff --git a/tests/helpers/test_update_coordinator.py b/tests/helpers/test_update_coordinator.py index 244e221f53a..7023798f2b4 100644 --- a/tests/helpers/test_update_coordinator.py +++ b/tests/helpers/test_update_coordinator.py @@ -9,13 +9,14 @@ import aiohttp import pytest import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CoreState from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.helpers import update_coordinator from homeassistant.util.dt import utcnow -from tests.common import async_fire_time_changed +from tests.common import MockConfigEntry, async_fire_time_changed _LOGGER = logging.getLogger(__name__) @@ -371,3 +372,12 @@ async def test_async_config_entry_first_refresh_success(crd, caplog): await crd.async_config_entry_first_refresh() assert crd.last_update_success is True + + +async def test_not_schedule_refresh_if_system_option_disable_polling(hass): + """Test we do not schedule a refresh if disable polling in config entry.""" + entry = MockConfigEntry(pref_disable_polling=True) + config_entries.current_entry.set(entry) + crd = get_crd(hass, DEFAULT_UPDATE_INTERVAL) + crd.async_add_listener(lambda: None) + assert crd._unsub_refresh is None diff --git a/tests/mock/__init__.py b/tests/mock/__init__.py new file mode 100644 index 00000000000..acf1fe50f54 --- /dev/null +++ b/tests/mock/__init__.py @@ -0,0 +1 @@ +"""Mock helpers.""" diff --git a/tests/test_config.py b/tests/test_config.py index 76218ab5bf2..87496c566e3 100644 --- a/tests/test_config.py +++ b/tests/test_config.py @@ -218,7 +218,6 @@ def test_customize_dict_schema(): values = ({ATTR_FRIENDLY_NAME: None}, {ATTR_ASSUMED_STATE: "2"}) for val in values: - print(val) with pytest.raises(MultipleInvalid): config_util.CUSTOMIZE_DICT_SCHEMA(val) @@ -374,7 +373,7 @@ async def test_loading_configuration_from_storage(hass, hass_storage): assert hass.config.elevation == 10 assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.time_zone == "Europe/Copenhagen" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert len(hass.config.allowlist_external_dirs) == 3 @@ -405,7 +404,7 @@ async def test_loading_configuration_from_storage_with_yaml_only(hass, hass_stor assert hass.config.elevation == 10 assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.time_zone == "Europe/Copenhagen" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.media_dirs == {"mymedia": "/usr"} @@ -463,7 +462,7 @@ async def test_override_stored_configuration(hass, hass_storage): assert hass.config.elevation == 10 assert hass.config.location_name == "Home" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "Europe/Copenhagen" + assert hass.config.time_zone == "Europe/Copenhagen" assert len(hass.config.allowlist_external_dirs) == 3 assert "/etc" in hass.config.allowlist_external_dirs assert hass.config.config_source == config_util.SOURCE_YAML @@ -493,7 +492,7 @@ async def test_loading_configuration(hass): assert hass.config.elevation == 25 assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_IMPERIAL - assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.time_zone == "America/New_York" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert len(hass.config.allowlist_external_dirs) == 3 @@ -525,7 +524,7 @@ async def test_loading_configuration_temperature_unit(hass): assert hass.config.elevation == 25 assert hass.config.location_name == "Huis" assert hass.config.units.name == CONF_UNIT_SYSTEM_METRIC - assert hass.config.time_zone.zone == "America/New_York" + assert hass.config.time_zone == "America/New_York" assert hass.config.external_url == "https://www.example.com" assert hass.config.internal_url == "http://example.local" assert hass.config.config_source == config_util.SOURCE_YAML diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index b9f9424b6f0..556f06fce54 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -86,7 +86,7 @@ async def test_call_setup_entry(hass): assert result assert len(mock_migrate_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.supports_unload @@ -115,7 +115,7 @@ async def test_call_setup_entry_without_reload_support(hass): assert result assert len(mock_migrate_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert not entry.supports_unload @@ -145,7 +145,7 @@ async def test_call_async_migrate_entry(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.supports_unload @@ -173,7 +173,7 @@ async def test_call_async_migrate_entry_failure_false(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -201,7 +201,7 @@ async def test_call_async_migrate_entry_failure_exception(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -229,7 +229,7 @@ async def test_call_async_migrate_entry_failure_not_bool(hass): assert result assert len(mock_migrate_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -248,7 +248,7 @@ async def test_call_async_migrate_entry_failure_not_supported(hass): result = await async_setup_component(hass, "comp", {}) assert result assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_MIGRATION_ERROR + assert entry.state is config_entries.ConfigEntryState.MIGRATION_ERROR assert not entry.supports_unload @@ -380,7 +380,7 @@ async def test_remove_entry_raises(hass, manager): MockConfigEntry(domain="test", entry_id="test1").add_to_manager(manager) MockConfigEntry( - domain="comp", entry_id="test2", state=config_entries.ENTRY_STATE_LOADED + domain="comp", entry_id="test2", state=config_entries.ConfigEntryState.LOADED ).add_to_manager(manager) MockConfigEntry(domain="test", entry_id="test3").add_to_manager(manager) @@ -546,7 +546,6 @@ async def test_saving_and_loading(hass): """Test flow.""" VERSION = 5 - CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL async def async_step_user(self, user_input=None): """Test user step.""" @@ -562,7 +561,6 @@ async def test_saving_and_loading(hass): """Test flow.""" VERSION = 3 - CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_PUSH async def async_step_user(self, user_input=None): """Test user step.""" @@ -576,6 +574,13 @@ async def test_saving_and_loading(hass): ) assert len(hass.config_entries.async_entries()) == 2 + entry_1 = hass.config_entries.async_entries()[0] + + hass.config_entries.async_update_entry( + entry_1, + pref_disable_polling=True, + pref_disable_new_entities=True, + ) # To trigger the call_later async_fire_time_changed(hass, dt.utcnow() + timedelta(seconds=1)) @@ -597,8 +602,9 @@ async def test_saving_and_loading(hass): assert orig.title == loaded.title assert orig.data == loaded.data assert orig.source == loaded.source - assert orig.connection_class == loaded.connection_class assert orig.unique_id == loaded.unique_id + assert orig.pref_disable_new_entities == loaded.pref_disable_new_entities + assert orig.pref_disable_polling == loaded.pref_disable_polling async def test_forward_entry_sets_up_component(hass): @@ -799,7 +805,7 @@ async def test_updating_entry_data(manager): entry = MockConfigEntry( domain="test", data={"first": True}, - state=config_entries.ENTRY_STATE_SETUP_ERROR, + state=config_entries.ConfigEntryState.SETUP_ERROR, ) entry.add_to_manager(manager) @@ -815,15 +821,20 @@ async def test_updating_entry_system_options(manager): entry = MockConfigEntry( domain="test", data={"first": True}, - state=config_entries.ENTRY_STATE_SETUP_ERROR, - system_options={"disable_new_entities": True}, + state=config_entries.ConfigEntryState.SETUP_ERROR, + pref_disable_new_entities=True, ) entry.add_to_manager(manager) - assert entry.system_options.disable_new_entities + assert entry.pref_disable_new_entities is True + assert entry.pref_disable_polling is False - entry.system_options.update(disable_new_entities=False) - assert not entry.system_options.disable_new_entities + manager.async_update_entry( + entry, pref_disable_new_entities=False, pref_disable_polling=True + ) + + assert entry.pref_disable_new_entities is False + assert entry.pref_disable_polling is True async def test_update_entry_options_and_trigger_listener(hass, manager): @@ -864,14 +875,14 @@ async def test_setup_raise_not_ready(hass, caplog): assert p_hass is hass assert p_wait_time == 5 - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert entry.reason == "The internet connection is offline" mock_setup_entry.side_effect = None mock_setup_entry.return_value = True await p_setup(None) - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert entry.reason is None @@ -908,12 +919,12 @@ async def test_setup_retrying_during_unload(hass): with patch("homeassistant.helpers.event.async_call_later") as mock_call: await entry.async_setup(hass) - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert len(mock_call.return_value.mock_calls) == 0 await entry.async_unload(hass) - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert len(mock_call.return_value.mock_calls) == 1 @@ -930,7 +941,7 @@ async def test_setup_retrying_during_unload_before_started(hass): await entry.async_setup(hass) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert ( hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 1 ) @@ -938,12 +949,62 @@ async def test_setup_retrying_during_unload_before_started(hass): await entry.async_unload(hass) await hass.async_block_till_done() - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED assert ( hass.bus.async_listeners()[EVENT_HOMEASSISTANT_STARTED] == initial_listeners + 0 ) +async def test_create_entry_options(hass): + """Test a config entry being created with options.""" + + async def mock_async_setup(hass, config): + """Mock setup.""" + hass.async_create_task( + hass.config_entries.flow.async_init( + "comp", + context={"source": config_entries.SOURCE_IMPORT}, + data={"data": "data", "option": "option"}, + ) + ) + return True + + async_setup_entry = AsyncMock(return_value=True) + mock_integration( + hass, + MockModule( + "comp", async_setup=mock_async_setup, async_setup_entry=async_setup_entry + ), + ) + mock_entity_platform(hass, "config_flow.comp", None) + await async_setup_component(hass, "persistent_notification", {}) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_import(self, user_input): + """Test import step creating entry, with options.""" + return self.async_create_entry( + title="title", + data={"example": user_input["data"]}, + options={"example": user_input["option"]}, + ) + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + assert await async_setup_component(hass, "comp", {}) + + await hass.async_block_till_done() + + assert len(async_setup_entry.mock_calls) == 1 + + entries = hass.config_entries.async_entries("comp") + assert len(entries) == 1 + assert entries[0].data == {"example": "data"} + assert entries[0].options == {"example": "option"} + + async def test_entry_options(hass, manager): """Test that we can set options on an entry.""" entry = MockConfigEntry(domain="test", data={"first": True}, options=None) @@ -1010,7 +1071,9 @@ async def test_entry_options_abort(hass, manager): async def test_entry_setup_succeed(hass, manager): """Test that we can setup an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_NOT_LOADED) + entry = MockConfigEntry( + domain="comp", state=config_entries.ConfigEntryState.NOT_LOADED + ) entry.add_to_hass(hass) mock_setup = AsyncMock(return_value=True) @@ -1025,17 +1088,17 @@ async def test_entry_setup_succeed(hass, manager): assert await manager.async_setup(entry.entry_id) assert len(mock_setup.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_LOADED, - config_entries.ENTRY_STATE_SETUP_ERROR, - config_entries.ENTRY_STATE_MIGRATION_ERROR, - config_entries.ENTRY_STATE_SETUP_RETRY, - config_entries.ENTRY_STATE_FAILED_UNLOAD, + config_entries.ConfigEntryState.LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.MIGRATION_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, + config_entries.ConfigEntryState.FAILED_UNLOAD, ), ) async def test_entry_setup_invalid_state(hass, manager, state): @@ -1056,12 +1119,12 @@ async def test_entry_setup_invalid_state(hass, manager, state): assert len(mock_setup.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0 - assert entry.state == state + assert entry.state is state async def test_entry_unload_succeed(hass, manager): """Test that we can unload an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_unload_entry = AsyncMock(return_value=True) @@ -1070,15 +1133,15 @@ async def test_entry_unload_succeed(hass, manager): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_NOT_LOADED, - config_entries.ENTRY_STATE_SETUP_ERROR, - config_entries.ENTRY_STATE_SETUP_RETRY, + config_entries.ConfigEntryState.NOT_LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, ), ) async def test_entry_unload_failed_to_load(hass, manager, state): @@ -1092,14 +1155,14 @@ async def test_entry_unload_failed_to_load(hass, manager, state): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_MIGRATION_ERROR, - config_entries.ENTRY_STATE_FAILED_UNLOAD, + config_entries.ConfigEntryState.MIGRATION_ERROR, + config_entries.ConfigEntryState.FAILED_UNLOAD, ), ) async def test_entry_unload_invalid_state(hass, manager, state): @@ -1115,12 +1178,12 @@ async def test_entry_unload_invalid_state(hass, manager, state): assert await manager.async_unload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 0 - assert entry.state == state + assert entry.state is state async def test_entry_reload_succeed(hass, manager): """Test that we can reload an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1142,15 +1205,15 @@ async def test_entry_reload_succeed(hass, manager): assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_NOT_LOADED, - config_entries.ENTRY_STATE_SETUP_ERROR, - config_entries.ENTRY_STATE_SETUP_RETRY, + config_entries.ConfigEntryState.NOT_LOADED, + config_entries.ConfigEntryState.SETUP_ERROR, + config_entries.ConfigEntryState.SETUP_RETRY, ), ) async def test_entry_reload_not_loaded(hass, manager, state): @@ -1177,14 +1240,14 @@ async def test_entry_reload_not_loaded(hass, manager, state): assert len(async_unload_entry.mock_calls) == 0 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED @pytest.mark.parametrize( "state", ( - config_entries.ENTRY_STATE_MIGRATION_ERROR, - config_entries.ENTRY_STATE_FAILED_UNLOAD, + config_entries.ConfigEntryState.MIGRATION_ERROR, + config_entries.ConfigEntryState.FAILED_UNLOAD, ), ) async def test_entry_reload_error(hass, manager, state): @@ -1218,7 +1281,7 @@ async def test_entry_reload_error(hass, manager, state): async def test_entry_disable_succeed(hass, manager): """Test that we can disable an entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1243,19 +1306,19 @@ async def test_entry_disable_succeed(hass, manager): assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_NOT_LOADED + assert entry.state is config_entries.ConfigEntryState.NOT_LOADED # Enable assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_unload_entry.mock_calls) == 1 assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED async def test_entry_disable_without_reload_support(hass, manager): """Test that we can disable an entry without reload support.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -1277,14 +1340,14 @@ async def test_entry_disable_without_reload_support(hass, manager): ) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD # Enable with pytest.raises(config_entries.OperationNotAllowed): await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 0 assert len(async_setup_entry.mock_calls) == 0 - assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD async def test_entry_enable_without_reload_support(hass, manager): @@ -1309,7 +1372,7 @@ async def test_entry_enable_without_reload_support(hass, manager): assert await manager.async_set_disabled_by(entry.entry_id, None) assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED # Disable assert not await manager.async_set_disabled_by( @@ -1317,7 +1380,7 @@ async def test_entry_enable_without_reload_support(hass, manager): ) assert len(async_setup.mock_calls) == 1 assert len(async_setup_entry.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_FAILED_UNLOAD + assert entry.state is config_entries.ConfigEntryState.FAILED_UNLOAD async def test_init_custom_integration(hass): @@ -1361,7 +1424,7 @@ async def test_reload_entry_entity_registry_works(hass): registry = mock_registry(hass) config_entry = MockConfigEntry( - domain="comp", state=config_entries.ENTRY_STATE_LOADED + domain="comp", state=config_entries.ConfigEntryState.LOADED ) config_entry.supports_unload = True config_entry.add_to_hass(hass) @@ -1441,7 +1504,7 @@ async def test_unique_id_existing_entry(hass, manager): hass.config.components.add("comp") MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, unique_id="mock-unique-id", ).add_to_hass(hass) @@ -1496,7 +1559,7 @@ async def test_entry_id_existing_entry(hass, manager): MockConfigEntry( entry_id=collide_entry_id, domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, unique_id="mock-unique-id", ).add_to_hass(hass) @@ -1533,7 +1596,7 @@ async def test_unique_id_update_existing_entry_without_reload(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, ) entry.add_to_hass(hass) @@ -1577,7 +1640,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, ) entry.add_to_hass(hass) @@ -1616,7 +1679,7 @@ async def test_unique_id_update_existing_entry_with_reload(hass, manager): # Test we don't reload if entry not started updates["host"] = "2.2.2.2" - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}), patch( "homeassistant.config_entries.ConfigEntries.async_reload" ) as async_reload: @@ -1795,7 +1858,7 @@ async def test_manual_add_overrides_ignored_entry(hass, manager): domain="comp", data={"additional": "data", "host": "0.0.0.0"}, unique_id="mock-unique-id", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1838,7 +1901,7 @@ async def test_manual_add_overrides_ignored_entry_singleton(hass, manager): hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1877,7 +1940,7 @@ async def test__async_current_entries_does_not_skip_ignore_non_user(hass, manage hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1912,7 +1975,7 @@ async def test__async_current_entries_explict_skip_ignore(hass, manager): hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -1951,7 +2014,7 @@ async def test__async_current_entries_explict_include_ignore(hass, manager): hass.config.components.add("comp") entry = MockConfigEntry( domain="comp", - state=config_entries.ENTRY_STATE_LOADED, + state=config_entries.ConfigEntryState.LOADED, source=config_entries.SOURCE_IGNORE, ) entry.add_to_hass(hass) @@ -2207,7 +2270,7 @@ async def test_async_setup_init_entry(hass): entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 - assert entries[0].state == config_entries.ENTRY_STATE_LOADED + assert entries[0].state is config_entries.ConfigEntryState.LOADED async def test_async_setup_update_entry(hass): @@ -2262,7 +2325,7 @@ async def test_async_setup_update_entry(hass): entries = hass.config_entries.async_entries("comp") assert len(entries) == 1 - assert entries[0].state == config_entries.ENTRY_STATE_LOADED + assert entries[0].state is config_entries.ConfigEntryState.LOADED assert entries[0].data == {"value": "updated"} @@ -2446,6 +2509,55 @@ async def test_default_discovery_abort_on_new_unique_flow(hass, manager): assert flows[0]["context"]["unique_id"] == "mock-unique-id" +async def test_default_discovery_abort_on_user_flow_complete(hass, manager): + """Test that a flow using default discovery is aborted when a second flow completes.""" + mock_integration(hass, MockModule("comp")) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + if user_input is None: + return self.async_show_form(step_id="user") + return self.async_create_entry(title="title", data={"token": "supersecret"}) + + async def async_step_discovery(self, discovery_info=None): + """Test discovery step.""" + await self._async_handle_discovery_without_unique_id() + return self.async_show_form(step_id="mock") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow}): + # First discovery with default, no unique ID + flow1 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_DISCOVERY}, data={} + ) + assert flow1["type"] == data_entry_flow.RESULT_TYPE_FORM + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + # User sets up a manual flow + flow2 = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + assert flow2["type"] == data_entry_flow.RESULT_TYPE_FORM + + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 2 + + # Complete the manual flow + result = await hass.config_entries.flow.async_configure(flow2["flow_id"], {}) + assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY + + # Ensure the first flow is gone now + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 0 + + async def test_updating_entry_with_and_without_changes(manager): """Test that we can update an entry data.""" entry = MockConfigEntry( @@ -2454,58 +2566,28 @@ async def test_updating_entry_with_and_without_changes(manager): title="thetitle", options={"option": True}, unique_id="abc123", - state=config_entries.ENTRY_STATE_SETUP_ERROR, + state=config_entries.ConfigEntryState.SETUP_ERROR, ) entry.add_to_manager(manager) assert manager.async_update_entry(entry) is False - assert manager.async_update_entry(entry, data={"second": True}) is True - assert manager.async_update_entry(entry, data={"second": True}) is False - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is True - ) - assert ( - manager.async_update_entry(entry, data={"second": True, "third": 456}) is False - ) - assert manager.async_update_entry(entry, options={"second": True}) is True - assert manager.async_update_entry(entry, options={"second": True}) is False - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is True - ) - assert ( - manager.async_update_entry(entry, options={"second": True, "third": "123"}) - is False - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is True - ) - assert ( - manager.async_update_entry(entry, system_options={"disable_new_entities": True}) - is False - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is True - ) - assert ( - manager.async_update_entry( - entry, system_options={"disable_new_entities": False} - ) - is False - ) - assert manager.async_update_entry(entry, title="thetitle") is False - assert manager.async_update_entry(entry, title="newtitle") is True - assert manager.async_update_entry(entry, unique_id="abc123") is False - assert manager.async_update_entry(entry, unique_id="abc1234") is True + + for change in ( + {"data": {"second": True, "third": 456}}, + {"data": {"second": True}}, + {"options": {"hello": True}}, + {"pref_disable_new_entities": True}, + {"pref_disable_polling": True}, + {"title": "sometitle"}, + {"unique_id": "abcd1234"}, + ): + assert manager.async_update_entry(entry, **change) is True + assert manager.async_update_entry(entry, **change) is False async def test_entry_reload_calls_on_unload_listeners(hass, manager): """Test reload calls the on unload listeners.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) + entry = MockConfigEntry(domain="comp", state=config_entries.ConfigEntryState.LOADED) entry.add_to_hass(hass) async_setup = AsyncMock(return_value=True) @@ -2531,7 +2613,7 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): assert len(async_unload_entry.mock_calls) == 1 assert len(mock_setup_entry.mock_calls) == 1 assert len(mock_unload_callback.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED assert await manager.async_reload(entry.entry_id) assert len(async_unload_entry.mock_calls) == 2 @@ -2539,7 +2621,7 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): # Since we did not register another async_on_unload it should # have only been called once assert len(mock_unload_callback.mock_calls) == 1 - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED async def test_setup_raise_auth_failed(hass, caplog): @@ -2556,7 +2638,7 @@ async def test_setup_raise_auth_failed(hass, caplog): await hass.async_block_till_done() assert "could not authenticate: The password is no longer valid" in caplog.text - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR assert entry.reason == "The password is no longer valid" flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2564,7 +2646,7 @@ async def test_setup_raise_auth_failed(hass, caplog): assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH caplog.clear() - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED entry.reason = None await entry.async_setup(hass) @@ -2572,7 +2654,7 @@ async def test_setup_raise_auth_failed(hass, caplog): assert "could not authenticate: The password is no longer valid" in caplog.text # Verify multiple ConfigEntryAuthFailed does not generate a second flow - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2605,21 +2687,21 @@ async def test_setup_raise_auth_failed_from_first_coordinator_update(hass, caplo await hass.async_block_till_done() assert "could not authenticate: The password is no longer valid" in caplog.text - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["context"]["entry_id"] == entry.entry_id assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH caplog.clear() - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED await entry.async_setup(hass) await hass.async_block_till_done() assert "could not authenticate: The password is no longer valid" in caplog.text # Verify multiple ConfigEntryAuthFailed does not generate a second flow - assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + assert entry.state is config_entries.ConfigEntryState.SETUP_ERROR flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2653,14 +2735,14 @@ async def test_setup_raise_auth_failed_from_future_coordinator_update(hass, capl assert "Authentication failed while fetching" in caplog.text assert "The password is no longer valid" in caplog.text - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 assert flows[0]["context"]["entry_id"] == entry.entry_id assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH caplog.clear() - entry.state = config_entries.ENTRY_STATE_NOT_LOADED + entry.state = config_entries.ConfigEntryState.NOT_LOADED await entry.async_setup(hass) await hass.async_block_till_done() @@ -2668,7 +2750,7 @@ async def test_setup_raise_auth_failed_from_future_coordinator_update(hass, capl assert "The password is no longer valid" in caplog.text # Verify multiple ConfigEntryAuthFailed does not generate a second flow - assert entry.state == config_entries.ENTRY_STATE_LOADED + assert entry.state is config_entries.ConfigEntryState.LOADED flows = hass.config_entries.flow.async_progress() assert len(flows) == 1 @@ -2696,7 +2778,7 @@ async def test_setup_retrying_during_shutdown(hass): with patch("homeassistant.helpers.event.async_call_later") as mock_call: await entry.async_setup(hass) - assert entry.state == config_entries.ENTRY_STATE_SETUP_RETRY + assert entry.state is config_entries.ConfigEntryState.SETUP_RETRY assert len(mock_call.return_value.mock_calls) == 0 hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP) @@ -2708,3 +2790,92 @@ async def test_setup_retrying_during_shutdown(hass): await hass.async_block_till_done() assert len(mock_call.return_value.mock_calls) == 0 + + +@pytest.mark.parametrize( + "matchers, reason", + [ + ({}, "already_configured"), + ({"host": "3.3.3.3"}, "no_match"), + ({"host": "3.4.5.6"}, "already_configured"), + ({"host": "3.4.5.6", "ip": "3.4.5.6"}, "no_match"), + ({"host": "3.4.5.6", "ip": "1.2.3.4"}, "already_configured"), + ({"host": "3.4.5.6", "ip": "1.2.3.4", "port": 23}, "already_configured"), + ({"ip": "9.9.9.9"}, "already_configured"), + ({"ip": "7.7.7.7"}, "no_match"), # ignored + ], +) +async def test__async_abort_entries_match(hass, manager, matchers, reason): + """Test aborting if matching config entries exist.""" + MockConfigEntry( + domain="comp", data={"ip": "1.2.3.4", "host": "4.5.6.7", "port": 23} + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", data={"ip": "9.9.9.9", "host": "4.5.6.7", "port": 23} + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", data={"ip": "1.2.3.4", "host": "3.4.5.6", "port": 23} + ).add_to_hass(hass) + MockConfigEntry( + domain="comp", + source=config_entries.SOURCE_IGNORE, + data={"ip": "7.7.7.7", "host": "4.5.6.7", "port": 23}, + ).add_to_hass(hass) + + await async_setup_component(hass, "persistent_notification", {}) + + mock_setup_entry = AsyncMock(return_value=True) + + mock_integration(hass, MockModule("comp", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.comp", None) + + class TestFlow(config_entries.ConfigFlow): + """Test flow.""" + + VERSION = 1 + + async def async_step_user(self, user_input=None): + """Test user step.""" + self._async_abort_entries_match(matchers) + return self.async_abort(reason="no_match") + + with patch.dict(config_entries.HANDLERS, {"comp": TestFlow, "beer": 5}): + result = await manager.flow.async_init( + "comp", context={"source": config_entries.SOURCE_USER} + ) + await hass.async_block_till_done() + + assert result["type"] == "abort" + assert result["reason"] == reason + + +async def test_loading_old_data(hass, hass_storage): + """Test automatically migrating old data.""" + hass_storage[config_entries.STORAGE_KEY] = { + "version": 1, + "data": { + "entries": [ + { + "version": 5, + "domain": "my_domain", + "entry_id": "mock-id", + "data": {"my": "data"}, + "source": "user", + "title": "Mock title", + "system_options": {"disable_new_entities": True}, + } + ] + }, + } + manager = config_entries.ConfigEntries(hass, {}) + await manager.async_initialize() + + entries = manager.async_entries() + assert len(entries) == 1 + entry = entries[0] + assert entry.version == 5 + assert entry.domain == "my_domain" + assert entry.entry_id == "mock-id" + assert entry.title == "Mock title" + assert entry.data == {"my": "data"} + assert entry.pref_disable_new_entities is True diff --git a/tests/test_core.py b/tests/test_core.py index d3283c14b84..39c5b310537 100644 --- a/tests/test_core.py +++ b/tests/test_core.py @@ -9,7 +9,6 @@ from tempfile import TemporaryDirectory from unittest.mock import MagicMock, Mock, PropertyMock, patch import pytest -import pytz import voluptuous as vol from homeassistant.const import ( @@ -44,7 +43,7 @@ from homeassistant.util.unit_system import METRIC_SYSTEM from tests.common import async_capture_events, async_mock_service -PST = pytz.timezone("America/Los_Angeles") +PST = dt_util.get_time_zone("America/Los_Angeles") def test_split_entity_id(): @@ -234,6 +233,29 @@ async def test_async_add_job_pending_tasks_coro(hass): assert len(call_count) == 2 +async def test_async_create_task_pending_tasks_coro(hass): + """Add a coro to pending tasks.""" + call_count = [] + + async def test_coro(): + """Test Coro.""" + call_count.append("call") + + for _ in range(2): + hass.create_task(test_coro()) + + async def wait_finish_callback(): + """Wait until all stuff is scheduled.""" + await asyncio.sleep(0) + await asyncio.sleep(0) + + await wait_finish_callback() + + assert len(hass._pending_tasks) == 2 + await hass.async_block_till_done() + assert len(call_count) == 2 + + async def test_async_add_job_pending_tasks_executor(hass): """Run an executor in pending tasks.""" call_count = [] @@ -877,7 +899,7 @@ def test_config_defaults(): assert config.longitude == 0 assert config.elevation == 0 assert config.location_name == "Home" - assert config.time_zone == dt_util.UTC + assert config.time_zone == "UTC" assert config.internal_url is None assert config.external_url is None assert config.config_source == "default" diff --git a/tests/test_exceptions.py b/tests/test_exceptions.py index 959f0846cae..e353c0dd82d 100644 --- a/tests/test_exceptions.py +++ b/tests/test_exceptions.py @@ -29,7 +29,6 @@ def test_conditionerror_format(): ) error_container1 = ConditionErrorContainer("box", errors=[error_pos1, error_pos2]) - print(error_container1) assert ( str(error_container1) == """In 'box' (item 1 of 2): diff --git a/tests/test_loader.py b/tests/test_loader.py index 8acc8a7de4f..20dcf90d90e 100644 --- a/tests/test_loader.py +++ b/tests/test_loader.py @@ -1,5 +1,5 @@ """Test to verify that we can load components.""" -from unittest.mock import ANY, patch +from unittest.mock import patch import pytest @@ -97,18 +97,13 @@ async def test_helpers_wrapper(hass): assert result == ["hello"] -async def test_custom_component_name(hass): +async def test_custom_component_name(hass, enable_custom_integrations): """Test the name attribute of custom components.""" - integration = await loader.async_get_integration(hass, "test_standalone") - int_comp = integration.get_component() - assert int_comp.__name__ == "custom_components.test_standalone" - assert int_comp.__package__ == "custom_components" - - comp = hass.components.test_standalone - assert comp.__name__ == "custom_components.test_standalone" - assert comp.__package__ == "custom_components" + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_standalone") integration = await loader.async_get_integration(hass, "test_package") + int_comp = integration.get_component() assert int_comp.__name__ == "custom_components.test_package" assert int_comp.__package__ == "custom_components.test_package" @@ -128,69 +123,34 @@ async def test_custom_component_name(hass): assert TEST == 5 -async def test_log_warning_custom_component(hass, caplog): +async def test_log_warning_custom_component(hass, caplog, enable_custom_integrations): """Test that we log a warning when loading a custom component.""" - await loader.async_get_integration(hass, "test_standalone") - assert "You are using a custom integration test_standalone" in caplog.text + + await loader.async_get_integration(hass, "test_package") + assert "We found a custom integration test_package" in caplog.text await loader.async_get_integration(hass, "test") - assert "You are using a custom integration test " in caplog.text + assert "We found a custom integration test " in caplog.text -async def test_custom_integration_missing_version(hass, caplog): - """Test that we log a warning when custom integrations are missing a version.""" - test_integration_1 = loader.Integration( - hass, "custom_components.test1", None, {"domain": "test1"} - ) - test_integration_2 = loader.Integration( - hass, - "custom_components.test2", - None, - loader.manifest_from_legacy_module("test2", "custom_components.test2"), - ) - - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = { - "test1": test_integration_1, - "test2": test_integration_2, - } - - await loader.async_get_integration(hass, "test1") - assert ( - "No 'version' key in the manifest file for custom integration 'test1'." - in caplog.text - ) - - await loader.async_get_integration(hass, "test2") - assert ( - "No 'version' key in the manifest file for custom integration 'test2'." - in caplog.text - ) - - -async def test_no_version_warning_for_none_custom_integrations(hass, caplog): - """Test that we do not log a warning when core integrations are missing a version.""" - await loader.async_get_integration(hass, "hue") - assert ( - "No 'version' key in the manifest file for custom integration 'hue'." - not in caplog.text - ) - - -async def test_custom_integration_version_not_valid(hass, caplog): +async def test_custom_integration_version_not_valid( + hass, caplog, enable_custom_integrations +): """Test that we log a warning when custom integrations have a invalid version.""" - test_integration = loader.Integration( - hass, "custom_components.test", None, {"domain": "test", "version": "test"} + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") + + assert ( + "The custom integration 'test_no_version' does not have a valid version key (None) in the manifest file and was blocked from loading." + in caplog.text ) - with patch("homeassistant.loader.async_get_custom_components") as mock_get: - mock_get.return_value = {"test": test_integration} - - await loader.async_get_integration(hass, "test") - assert ( - "'test' is not a valid version for custom integration 'test'." - in caplog.text - ) + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test2") + assert ( + "The custom integration 'test_bad_version' does not have a valid version key (bad) in the manifest file and was blocked from loading." + in caplog.text + ) async def test_get_integration(hass): @@ -200,7 +160,7 @@ async def test_get_integration(hass): assert hue_light == integration.get_platform("light") -async def test_get_integration_legacy(hass): +async def test_get_integration_legacy(hass, enable_custom_integrations): """Test resolving integration.""" integration = await loader.async_get_integration(hass, "test_embedded") assert integration.get_component().DOMAIN == "test_embedded" @@ -319,13 +279,6 @@ async def test_integrations_only_once(hass): assert await int_1 is await int_2 -async def test_get_custom_components_internal(hass): - """Test that we can a list of custom components.""" - # pylint: disable=protected-access - integrations = await loader._async_get_custom_components(hass) - assert integrations == {"test": ANY, "test_package": ANY} - - def _get_test_integration(hass, name, config_flow): """Return a generated test integration.""" return loader.Integration( @@ -507,3 +460,24 @@ async def test_get_custom_components_safe_mode(hass): """Test that we get empty custom components in safe mode.""" hass.config.safe_mode = True assert await loader.async_get_custom_components(hass) == {} + + +async def test_custom_integration_missing_version(hass, caplog): + """Test trying to load a custom integration without a version twice does not deadlock.""" + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test_no_version") + + +async def test_custom_integration_missing(hass, caplog): + """Test trying to load a custom integration that is missing twice not deadlock.""" + with patch("homeassistant.loader.async_get_custom_components") as mock_get: + mock_get.return_value = {} + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") + + with pytest.raises(loader.IntegrationNotFound): + await loader.async_get_integration(hass, "test1") diff --git a/tests/test_requirements.py b/tests/test_requirements.py index ff3f5bcab87..f68601e889e 100644 --- a/tests/test_requirements.py +++ b/tests/test_requirements.py @@ -361,7 +361,7 @@ async def test_discovery_requirements_ssdp(hass): ) as mock_process: await async_get_integration_with_requirements(hass, "ssdp_comp") - assert len(mock_process.mock_calls) == 3 + assert len(mock_process.mock_calls) == 4 assert mock_process.mock_calls[0][1][2] == ssdp.requirements # Ensure zeroconf is a dep for ssdp assert mock_process.mock_calls[1][1][1] == "zeroconf" @@ -386,7 +386,7 @@ async def test_discovery_requirements_zeroconf(hass, partial_manifest): ) as mock_process: await async_get_integration_with_requirements(hass, "comp") - assert len(mock_process.mock_calls) == 2 # zeroconf also depends on http + assert len(mock_process.mock_calls) == 3 # zeroconf also depends on http assert mock_process.mock_calls[0][1][2] == zeroconf.requirements diff --git a/tests/testing_config/custom_components/test/manifest.json b/tests/testing_config/custom_components/test/manifest.json index 70882fece05..125136e70b5 100644 --- a/tests/testing_config/custom_components/test/manifest.json +++ b/tests/testing_config/custom_components/test/manifest.json @@ -4,5 +4,6 @@ "documentation": "http://example.com", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "version": "1.2.3" } diff --git a/tests/testing_config/custom_components/test_bad_version/__init__.py b/tests/testing_config/custom_components/test_bad_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_bad_version/manifest.json b/tests/testing_config/custom_components/test_bad_version/manifest.json new file mode 100644 index 00000000000..69d322a33ad --- /dev/null +++ b/tests/testing_config/custom_components/test_bad_version/manifest.json @@ -0,0 +1,4 @@ +{ + "domain": "test_bad_version", + "version": "bad" +} \ No newline at end of file diff --git a/tests/testing_config/custom_components/test_embedded/manifest.json b/tests/testing_config/custom_components/test_embedded/manifest.json new file mode 100644 index 00000000000..72206594d0c --- /dev/null +++ b/tests/testing_config/custom_components/test_embedded/manifest.json @@ -0,0 +1,9 @@ +{ + "domain": "test_embedded", + "name": "Test Embedded", + "documentation": "http://test-package.io", + "requirements": [], + "dependencies": [], + "codeowners": [], + "version": "1.2.3" +} diff --git a/tests/testing_config/custom_components/test_no_version/__init__.py b/tests/testing_config/custom_components/test_no_version/__init__.py new file mode 100644 index 00000000000..e39053682e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/__init__.py @@ -0,0 +1 @@ +"""Provide a mock integration.""" diff --git a/tests/testing_config/custom_components/test_no_version/manifest.json b/tests/testing_config/custom_components/test_no_version/manifest.json new file mode 100644 index 00000000000..9054cf4f5e3 --- /dev/null +++ b/tests/testing_config/custom_components/test_no_version/manifest.json @@ -0,0 +1,3 @@ +{ + "domain": "test_no_version" +} \ No newline at end of file diff --git a/tests/testing_config/custom_components/test_package/manifest.json b/tests/testing_config/custom_components/test_package/manifest.json index 320d2768d27..660d0aef1a5 100644 --- a/tests/testing_config/custom_components/test_package/manifest.json +++ b/tests/testing_config/custom_components/test_package/manifest.json @@ -4,5 +4,6 @@ "documentation": "http://test-package.io", "requirements": [], "dependencies": [], - "codeowners": [] + "codeowners": [], + "version": "1.2.3" } diff --git a/tests/util/test_dt.py b/tests/util/test_dt.py index 50013012201..628cb533681 100644 --- a/tests/util/test_dt.py +++ b/tests/util/test_dt.py @@ -16,17 +16,12 @@ def teardown(): def test_get_time_zone_retrieves_valid_time_zone(): """Test getting a time zone.""" - time_zone = dt_util.get_time_zone(TEST_TIME_ZONE) - - assert time_zone is not None - assert time_zone.zone == TEST_TIME_ZONE + assert dt_util.get_time_zone(TEST_TIME_ZONE) is not None def test_get_time_zone_returns_none_for_garbage_time_zone(): """Test getting a non existing time zone.""" - time_zone = dt_util.get_time_zone("Non existing time zone") - - assert time_zone is None + assert dt_util.get_time_zone("Non existing time zone") is None def test_set_default_time_zone(): @@ -35,8 +30,7 @@ def test_set_default_time_zone(): dt_util.set_default_time_zone(time_zone) - # We cannot compare the timezones directly because of DST - assert time_zone.zone == dt_util.now().tzinfo.zone + assert dt_util.now().tzinfo is time_zone def test_utcnow(): @@ -239,35 +233,111 @@ def test_find_next_time_expression_time_dst(): return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) # Entering DST, clocks are rolled forward - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == find( - tz.localize(datetime(2018, 3, 25, 1, 50, 0)), 2, 30, 0 + assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + datetime(2018, 3, 25, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == find( - tz.localize(datetime(2018, 3, 25, 3, 50, 0)), 2, 30, 0 + assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + datetime(2018, 3, 25, 3, 50, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 3, 26, 2, 30, 0)) == find( - tz.localize(datetime(2018, 3, 26, 1, 50, 0)), 2, 30, 0 + assert datetime(2018, 3, 26, 2, 30, 0, tzinfo=tz) == find( + datetime(2018, 3, 26, 1, 50, 0, tzinfo=tz), 2, 30, 0 ) # Leaving DST, clocks are rolled back - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == find( - tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=False), 2, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=False) == find( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), 2, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 28, 4, 30, 0), is_dst=False) == find( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=True), 4, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 28, 2, 30, 0), is_dst=True) == find( - tz.localize(datetime(2018, 10, 28, 2, 5, 0), is_dst=True), 2, 30, 0 + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) - assert tz.localize(datetime(2018, 10, 29, 2, 30, 0)) == find( - tz.localize(datetime(2018, 10, 28, 2, 55, 0), is_dst=False), 2, 30, 0 + assert datetime(2018, 10, 28, 4, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 + ) + + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 + ) + + assert datetime(2018, 10, 28, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2018, 10, 28, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + +def test_find_next_time_expression_time_dst_chicago(): + """Test daylight saving time for find_next_time_expression_time.""" + tz = dt_util.get_time_zone("America/Chicago") + dt_util.set_default_time_zone(tz) + + def find(dt, hour, minute, second): + """Call test_find_next_time_expression_time.""" + seconds = dt_util.parse_time_expression(second, 0, 59) + minutes = dt_util.parse_time_expression(minute, 0, 59) + hours = dt_util.parse_time_expression(hour, 0, 23) + + return dt_util.find_next_time_expression_time(dt, seconds, minutes, hours) + + # Entering DST, clocks are rolled forward + assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 3, 50, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 3, 15, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 3, 14, 3, 30, 0, tzinfo=tz) == find( + datetime(2021, 3, 14, 1, 50, 0, tzinfo=tz), 3, 30, 0 + ) + + # Leaving DST, clocks are rolled back + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 10, 0, tzinfo=tz), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 + ) + + assert datetime(2021, 11, 7, 4, 30, 0, tzinfo=tz, fold=0) == find( + datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=1), 4, 30, 0 + ) + + assert datetime(2021, 11, 7, 2, 30, 0, tzinfo=tz, fold=1) == find( + datetime(2021, 11, 7, 2, 5, 0, tzinfo=tz, fold=1), 2, 30, 0 + ) + + assert datetime(2021, 11, 8, 2, 30, 0, tzinfo=tz) == find( + datetime(2021, 11, 7, 2, 55, 0, tzinfo=tz, fold=0), 2, 30, 0 ) diff --git a/tests/util/test_location.py b/tests/util/test_location.py index 9eb2dc70561..21531a59194 100644 --- a/tests/util/test_location.py +++ b/tests/util/test_location.py @@ -1,5 +1,5 @@ """Test Home Assistant location util methods.""" -from unittest.mock import Mock, patch +from unittest.mock import Mock import aiohttp import pytest @@ -72,76 +72,25 @@ def test_get_miles(): assert round(miles, 2) == DISTANCE_MILES -async def test_detect_location_info_ipapi(aioclient_mock, session): - """Test detect location info using ipapi.co.""" - aioclient_mock.get(location_util.IPAPI, text=load_fixture("ipapi.co.json")) +async def test_detect_location_info_whoami(aioclient_mock, session): + """Test detect location info using whoami.home-assistant.io.""" + aioclient_mock.get(location_util.WHOAMI_URL, text=load_fixture("whoami.json")) info = await location_util.async_detect_location_info(session, _test_real=True) assert info is not None assert info.ip == "1.2.3.4" - assert info.country_code == "CH" - assert info.country_name == "Switzerland" - assert info.region_code == "BE" - assert info.region_name == "Bern" - assert info.city == "Bern" - assert info.zip_code == "3000" - assert info.time_zone == "Europe/Zurich" - assert info.latitude == 46.9480278 - assert info.longitude == 7.4490812 + assert info.country_code == "XX" + assert info.region_code == "00" + assert info.city == "Gotham" + assert info.zip_code == "12345" + assert info.time_zone == "Earth/Gotham" + assert info.latitude == 12.34567 + assert info.longitude == 12.34567 assert info.use_metric -async def test_detect_location_info_ipapi_exhaust(aioclient_mock, session): - """Test detect location info using ipapi.co.""" - aioclient_mock.get(location_util.IPAPI, json={"latitude": "Sign up to access"}) - aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - - info = await location_util.async_detect_location_info(session, _test_real=True) - - assert info is not None - # ip_api result because ipapi got skipped - assert info.country_code == "US" - assert len(aioclient_mock.mock_calls) == 2 - - -async def test_detect_location_info_ip_api(aioclient_mock, session): - """Test detect location info using ip-api.com.""" - aioclient_mock.get(location_util.IP_API, text=load_fixture("ip-api.com.json")) - - with patch("homeassistant.util.location._get_ipapi", return_value=None): - info = await location_util.async_detect_location_info(session, _test_real=True) - - assert info is not None - assert info.ip == "1.2.3.4" - assert info.country_code == "US" - assert info.country_name == "United States" - assert info.region_code == "CA" - assert info.region_name == "California" - assert info.city == "San Diego" - assert info.zip_code == "92122" - assert info.time_zone == "America/Los_Angeles" - assert info.latitude == 32.8594 - assert info.longitude == -117.2073 - assert not info.use_metric - - -async def test_detect_location_info_both_queries_fail(session): - """Ensure we return None if both queries fail.""" - with patch("homeassistant.util.location._get_ipapi", return_value=None), patch( - "homeassistant.util.location._get_ip_api", return_value=None - ): - info = await location_util.async_detect_location_info(session, _test_real=True) - assert info is None - - -async def test_freegeoip_query_raises(raising_session): - """Test ipapi.co query when the request to API fails.""" - info = await location_util._get_ipapi(raising_session) - assert info is None - - -async def test_ip_api_query_raises(raising_session): - """Test ip api query when the request to API fails.""" - info = await location_util._get_ip_api(raising_session) +async def test_whoami_query_raises(raising_session): + """Test whoami query when the request to API fails.""" + info = await location_util._get_whoami(raising_session) assert info is None